論好名字的重要性: Linux內(nèi)核page到folio的變遷
一、引子
Once upon a time,Netscape的大拿 Phil Karlton曾經(jīng)說過:“There are only two hard things in Computer Science: cache invalidation and naming things”,成為程序界流傳甚廣的名言,可見取名是計(jì)算機(jī)科學(xué)中最難的兩件事之一。取名,要用名字恰到好處地描述其想描述的事物,要體現(xiàn)代碼注釋的最高原則——自注釋,這其實(shí)一點(diǎn)都不輕松。
取名,一般都是從生僻的變?yōu)榇蟊姷?,這樣才能朗朗上口,為人民群眾所喜聞樂見,比如陳港生更名為成龍,楊旎奧改名為楊紫,劉福榮改名為劉德華。而內(nèi)核從page到folio的一次改變,似乎是反其道而行之了。感覺有相當(dāng)數(shù)量的童鞋可能都不見得認(rèn)識folio這個(gè)單詞。金山詞霸曾經(jīng)曰過,folio是這個(gè)意思:

感覺大概意思,就是通過封面和封底夾在一起的一本書或者一套文獻(xiàn)。這個(gè)名字,有點(diǎn)古典生僻,它的目標(biāo)在于解決內(nèi)核面臨的一個(gè)糾結(jié)狀況。至于這個(gè)名字叫folio、pageset、superpage還是head_page,其實(shí)都沒有那么重要了,背后真正重要的是,它要解決什么問題。
二、亂局
下面我們來看folio出現(xiàn)之前,Linux內(nèi)核的情況。眾所周知,在Linux內(nèi)核中,我們用page來描述一頁,這一頁通常是4KB。這個(gè)世界如果所有人都是4KB的單頁,那就簡單歸一了。但是,在晴朗的天空中,卻漂浮中一朵烏云,這朵烏云就是compound page以及由compound page衍生出的hugepage,它們并非總是單頁的。
在Linux中,我們并不總是以單一的4KB basepage為單位來獲取、映射和釋放內(nèi)存。我們有時(shí)候,會把多個(gè)4KB復(fù)合在一起,進(jìn)行申請、映射和釋放:
1.用戶態(tài)的透明大頁(THP)和HugeTLB大頁
我們可能直接用PMD而不是PTE進(jìn)行映射,把一個(gè)2MB的連續(xù)物理內(nèi)存映射到用戶態(tài),這樣用戶態(tài)使用它,可以大量減小TLB miss。

在內(nèi)核中,我們描述內(nèi)存的單元是page,這個(gè)page一般是4KB。但是在THP/HugeTLB的場景下,整個(gè)2MB其實(shí)是一個(gè)整體的概念,這個(gè)時(shí)候,我們誕生了一種需求:
有時(shí)候我們關(guān)心的是這個(gè)2MB的整體,但是page其實(shí)是描述它的4KB的部分,用page來描述整體似乎不太適合;
有時(shí)候,我們確實(shí)想描述2MB整個(gè)整體里面4KB的某個(gè)部分,這個(gè)時(shí)候page似乎比較適合。
2.內(nèi)核態(tài)也可能直接申請和釋放compound page
比如一些內(nèi)核driver會通過__GFP_COMP標(biāo)記申請order大于0的連續(xù)頁,形成所謂的compound page(比如2頁,4頁,8頁,16頁等組成的復(fù)合頁,前面的THP/HugeTLB其實(shí)也是一種order較大的compound page,由2MB/4KB個(gè)頁面組成的compound page)。這樣的透過__GFP_COMP標(biāo)記,來向buddy申請內(nèi)存的driver還是比較多的:

這樣的compound page,可以透過內(nèi)核統(tǒng)一的gc機(jī)制進(jìn)行管理,比如在refcount即將歸0的時(shí)候,put_page(compound page)可以整體釋放compound page。
這顯然和前面描述的THP/HugeTLB的情況是一樣的,也存在一個(gè)在部分和整體兩種語義中糾結(jié)的問題。
由于多個(gè)page構(gòu)成了一個(gè)整體,這些page之間會有關(guān)聯(lián),我們需要某種方法解決如下的問題:
N個(gè)page是否組成了一個(gè)整體?
這些page哪些是head(第0個(gè)page)?
這些page哪些是tail(第1 ~ N - 1個(gè))?
這些page一共有多少個(gè)?
如果我是一個(gè)tail,那我的head是誰?
這些page如何整體釋放?釋放的時(shí)候需要什么析構(gòu)動(dòng)作?
....
在folio出現(xiàn)之前,內(nèi)核采用如下的方法來解決上述的問題:
在由N個(gè)4KB組成的compound page的第0個(gè)page結(jié)構(gòu)體(page[0],即head page)上安置一個(gè)PG_head標(biāo)記,邏輯如下:
page->flags |= (1UL << PG_head);
所以,如果傳給PageHead() API的是第0個(gè)page結(jié)構(gòu)體,由于PG_head為真,這個(gè)API返回true。
在由N個(gè)4KB組成的compound page的第1~N-1的page結(jié)構(gòu)體(page[1] ~ Page[N-1],即tail page)的compound_head上的最后一位設(shè)置1,邏輯如下:
page->compound_head |= ?1UL;
而除0位以外的位,則指向真正的head的page即page[0],于是邏輯上,如果傳入的是1~N-1這些page結(jié)構(gòu)體,如下兩個(gè)API分別可以取出head page和判斷相關(guān)的page是否是tail page(一個(gè)compound page除page[0]以外的page):

在page[1]這個(gè)結(jié)構(gòu)體的compound_order成員上,放置這個(gè)compound page的order,比如如果是連續(xù)4個(gè)4KB組成的復(fù)合頁,則page[1].compound_order = 2。所以,如果我們把head傳入compound_order這個(gè)API,則可以取到compound page的order數(shù):

在page[1]這個(gè)結(jié)構(gòu)體的compound_dtor成員上,放置這個(gè)compound page的析構(gòu)函數(shù),此析構(gòu)函數(shù),在put_page[page[0]]并且refcount即將歸0的時(shí)候會被執(zhí)行。不同類型的compound page的析構(gòu)函數(shù)可能會不一樣:

整個(gè)組織關(guān)系如下圖:


當(dāng)然,在HugeTLB和THP的場景下,page[2]還有更多的兼職功能(HugeTLB和THP不可能是只有2頁,它們存在2MB/4KB,所以一定存在page[2])。
比如HugeTLB借用page[2]->mapping成員:

而THP借用page[2]的deferred_list:

通過page[0]~page[n-1]中flags、compound_head、compound_dtor成員的特殊串聯(lián)關(guān)系,把這N個(gè)page結(jié)構(gòu)體聯(lián)系在了一起。這產(chǎn)生了一個(gè)混亂,很多時(shí)候,我們真正想操作的,其實(shí)只是compound page的整體,比如get_page()、put_page()、lock_page()、unlock_page()等。于是這樣的API里面,廣泛地存在這樣的compound_head()操作:

就以get_page()為例,傳入get_page()的page結(jié)構(gòu)體,其實(shí)可能是三種情況:
就是一個(gè)普通的非compound page的4KB page,這個(gè)時(shí)候,compound_head() API實(shí)際還是返回那個(gè)page;
傳入的是一個(gè)compound page的page[0](也即head page),這個(gè)時(shí)候,compound_head()返回的還是page[0];
傳入的是compound page的page[1] ~ page[n](也即tail page),這個(gè)時(shí)候,compound_head()返回的是compound_head - 1,也就是page[0]。
另外,我們一般是用操作一組page的page[0]來操作整個(gè)compound page的。

我們能不能把這些含混的語義扯清了呢?比如get_xxx(),這個(gè)xxx就是表示我要get一個(gè)整體呢?再比如get_yyy()就是表示我要操作一個(gè)basepage的yyy呢?另外,get_xxx()這個(gè)語義下,函數(shù)的參數(shù)就不可能是yyy呢?讓天堂的歸天堂,讓塵土的歸塵土,丁是丁,卯是卯,不香嗎?
get_xxx(struct xxx *x);
get_yyy(struct yyy *y);
而不是

這種混亂的局面,很容易對程序員進(jìn)行錯(cuò)誤的向?qū)?,因?yàn)槌绦騿T寫代碼的時(shí)候,究竟在操作xxx,還是yyy,自己都拎不清了。所以需要在函數(shù)體內(nèi)進(jìn)行區(qū)分操作,相似的問題還存在于lock_page()、unlock_page()之類的API,比如:

其實(shí),優(yōu)秀的代碼都是拎得清的代碼,優(yōu)先的API都是強(qiáng)迫調(diào)用者拎清的API。
如果你看最新的內(nèi)核,則可以看到兩組不同的APIs:
void folio_get(struct folio *folio);
void get_page(struct page *page);
void folio_lock(struct folio *folio);
void lock_page(struct page *page);
拎清楚的調(diào)用者,如果覺得自己在操作一個(gè)整體,它應(yīng)該調(diào)用folio_get、folio_lock,另外,我們也強(qiáng)迫它搞清楚自己的參數(shù)是folio而不是page。這對于代碼的讀者而言,也是賞心悅目的,無需猜測的。因?yàn)椋a編寫的一個(gè)基本原則就是:Don’t make me think!代碼的讀者并不想猜你究竟是想干xxx還是yyy,你直截了當(dāng)?shù)馗嬖V我就好。
當(dāng)我們明確地知道我們在操作一個(gè)整體/集合,我們在操作一個(gè)folio。那么這個(gè)folio和page是什么關(guān)系呢?page是folio的一部分。但是,folio結(jié)構(gòu)體的定義是什么呢?在最開始的patch版本里,其實(shí)它就是:

所以就數(shù)據(jù)結(jié)構(gòu)本身而言,folio本質(zhì)上還是一個(gè)page結(jié)構(gòu)體,只是被正名了。folio本質(zhì)上是一個(gè)集合的概念,比如它代表一個(gè)班級,但是它的數(shù)據(jù)結(jié)構(gòu)的字長又和表示班上每個(gè)學(xué)生的數(shù)據(jù)結(jié)構(gòu)是一樣的。比如你的名字叫黃曉明,你是一個(gè)開發(fā)組的組長你是個(gè)工程師,你這個(gè)數(shù)據(jù)結(jié)構(gòu),其實(shí)和一般的工程師是一樣的。但是,有時(shí)候,領(lǐng)導(dǎo)說,這個(gè)事情讓黃曉明這邊來干。他其實(shí)說的是黃曉明這個(gè)小組來干,黃曉明這個(gè)時(shí)候成為一個(gè)集體的概念。最終這個(gè)黃曉明其實(shí)和其他工程師的數(shù)據(jù)結(jié)構(gòu)是一樣的,但是領(lǐng)導(dǎo)說,讓黃曉明干,會比說“讓工程師干”要清晰明了的多。邏輯就是這么個(gè)邏輯,這體現(xiàn)了內(nèi)核社區(qū)的潔癖,也是代碼自注釋的原則的體現(xiàn)。
早期的patch長成這樣的話:

這多少有點(diǎn)不方便,因?yàn)槲覀優(yōu)榱瞬僮饕粋€(gè)folio的flags、LRU、private之類的成員,我們還要先來一次folio->page的操作,比如:

所以正式合入Linux 5.16 內(nèi)核的folio是長下面這樣的,把一些page里面常用字段,提取到了和page同等位置的union里面:

如果你還沒看明白呢,也許把folio和page并排列會更明白:

說白了,就是同名成員在同樣offset位置的簡單數(shù)學(xué)游戲。這樣,類似前面的folio的private的訪問,就可以直接是:

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


三、破局
由此,我們搞清楚了folio并不是什么新生事物,而是一個(gè)有著集合概念的,數(shù)據(jù)結(jié)構(gòu)與page對等的東西 。這樣我們至少破除了folio的神秘感。
我們看看內(nèi)核里面關(guān)于folio的注釋:
A folio is a physically, virtually and logically contiguous set of bytes. ?It is a power-of-two in size, and it is aligned to that same power-of-two. ?It is at least as large as %PAGE_SIZE. ?If it is in the page cache, it is at a file offset which is a multiple of that ?power-of-two. ?It may be mapped into userspace at an address which is ?at an arbitrary page offset, but its kernel virtual address is aligned ?to its size.
其實(shí)folio就是物理連續(xù)、虛擬連續(xù)的2^n次的PAGE_SIZE的一些bytes的集合,當(dāng)然這個(gè)n也是允許是0的。這個(gè)時(shí)候,有的童鞋就跳出來,為什么單頁的集合也可以叫folio?你問這個(gè)問題是傷了廣大單身群眾的心,難道單身自己一個(gè)人過就不叫一個(gè)家庭了嗎?家庭成員數(shù)量是一,儂曉得伐?
folio有一點(diǎn)是確定的,它必然不會是一個(gè)tail page。從而避免了前面的xxx、yyy的語義混亂(也就是Linux社區(qū)說的page結(jié)構(gòu)體 的mess)。
理解理念之后,在實(shí)踐環(huán)節(jié),其實(shí)就比較簡單了。Folio的開發(fā),分了好多個(gè)階段完成,而第一個(gè)階段的git pull request,就有90個(gè)patch:
簡單地看到這個(gè)數(shù)字,可能就有的童鞋直接從入門到放棄了。但是,實(shí)際點(diǎn)進(jìn)去看,真的都是非常簡單的替換游戲。比如我們隨機(jī)點(diǎn)開看一個(gè)mm: Add folio_pfn() ?【1】這個(gè)是求folio的pfn的,它究竟是個(gè)什么樣子呢?不要太簡單好吧:
所以,理解folio,最本質(zhì)的是理解什么時(shí)候用folio,把該用folio的,當(dāng)成folio用,破除心中的迷霧。
在Linux的層面,至少但是不限于如下這些應(yīng)該是一個(gè)集合:
1.加入lruvec進(jìn)行內(nèi)存回收管理的應(yīng)該是一個(gè)集合,它或者是compound page或者就是一個(gè)普通的單頁“集合”。在內(nèi)核透明大頁THP的場景下,其實(shí)THP都是以整體加入lruvec的,將lruvec的相關(guān)參數(shù)改為folio,可以適應(yīng)更廣泛的情況:THP和非THP進(jìn)入lruvec。比如,著名的shrink_page_list()函數(shù),現(xiàn)在就叫shrink_folio_list(),從lruvec里面拿到的,也是folio:
2.refcount計(jì)數(shù)、lock等的應(yīng)該是一個(gè)集合
比如:
哪怕你傳的是folio中的某一個(gè)page,我lock的還是一個(gè)集合:
3.mem_cgroup等的記賬charge應(yīng)該是一個(gè)集合;
4.wait writeback、bit等應(yīng)該是一個(gè)集合,比如:
folio_wait_bit(struct folio *folio, int bit_nr);
void folio_wait_writeback(struct folio *folio);
5.與address_space綁定的Page cache的查找、插入、刪除等操作應(yīng)該是一個(gè)集合,因?yàn)閜age cache也是可以是THP的。相關(guān)代碼比如:
6.rmap相關(guān)的單元應(yīng)該是一個(gè)集合
鑒于文件頁page cache以及進(jìn)程的匿名頁都可以是THP,所以在反向映射等API中,操作的應(yīng)該也是folio,比如,在 do_anonymous_page(struct vm_fault *vmf)這個(gè)經(jīng)典的匿名頁page fault處理函數(shù)中,最后rmap和lruvec相關(guān)的操作都是folio:
當(dāng)然,歷史的偉大變革不會在一瞬間完成。從page語義向folio語義的轉(zhuǎn)換并非是一蹴而就的,所以可以看看最新的Linux kernel提交,仍然也一些在轉(zhuǎn)義過程中。這些轉(zhuǎn)義發(fā)生在文件系統(tǒng)、內(nèi)存管理等各個(gè)領(lǐng)域:
鑒于本質(zhì)上folio和page數(shù)據(jù)結(jié)構(gòu)在內(nèi)存意義上相等,所以基于歷史原因短期內(nèi)難以改掉的代碼,如果使用的仍然是page,其實(shí)在folio和page之間還是可以比較輕松地轉(zhuǎn)換的。比如最簡單就是強(qiáng)行轉(zhuǎn)換:
struct page *p = (struct page *) folio;
這當(dāng)然是代碼的“bad smell”。
由于folio是一個(gè)集合語義,所以,在我們關(guān)心的是集合的一部分的時(shí)候,或者說一個(gè)部分是否屬于一個(gè)compound集合的時(shí)候,我們?nèi)匀魂P(guān)心的是page,比如,下面的API分別判斷page是否是一個(gè)tail,page是否屬于一個(gè)compound:
在比如下面的API,copy一整個(gè)folio集合,則需要里面的page一個(gè)部分一個(gè)部分的copy:
folio_page(folio, n)這個(gè)API可以取出一個(gè)folio中的第n個(gè)page。
四、結(jié)語
讓集合的是集合,讓個(gè)體的是個(gè)體,條分縷析,是folio設(shè)計(jì)的根本出發(fā)點(diǎn)。面向的問題根源,比怎么解決更加重要。盡管Linus Torvalds也不喜歡folio這個(gè)名,但是他認(rèn)可folio要解決的問題,這比叫folio還是劉麻子更關(guān)鍵。
原文作者:內(nèi)核工匠
