本文以arm64架構為背景。
一 背景
計算機中的物理內存本來是沒有沒有頁/page的概念的,Linux為了各種冠冕堂皇的理由,硬生生的將計算機中的物理內存以page為單位划分成一個一個的小方塊,稱作頁框,每個頁框有一個編號叫做PFN;有了PFN,就能夠計算出這個頁框對應的物理地址,有了物理地址CPU就能夠通過總線訪問到對應的內存。這不盡讓我想起了成都綿延不盡的天府大道被划分為了Y段、X號:天府大道北段xxx號、天府大道中斷xxx號、天府大道南段xxx號......人類對於化整為散的單位划分法有着與生俱來的癖好。
為了把物理頁框用軟件的方式管理起來,Linux定義了*struct page*這樣一個數據結構,每一個struct page數據結構就對應着一個實實在在的頁框,就好像一朵花生花就會在土壤里長出一顆花生 一樣。
軟件的struct page有了,物理層面的PFN也有了,還差點啥? 就差把二者聯系到一起的公式罷了。這就是內核中既平凡,又深沉的pfn_to_page()與page_to_pfn()兩個表達式了。名字接地氣,一看就懂:將PFN轉換為struct page(指針,即struct page的虛擬地址,下同)或者將sruct page轉換為PFN。
1.1 FLAT內存模型
我們把時光拉回到Linux夢想開始的地方,那時的物理內存地址總是從0開始,然后綿延若干MB到達終點;struct page也才剛剛誕生,那時的Linux剛滿1.3.50歲,整個世界滿是單純與童真。一個搭載着一串struct page指針的mem_map數組,一溜連續的數組index,struct page與PFN就這樣產生了微妙的聯系。沒有花哨的調料,也沒有做作的烹飪,最簡單的線性運算就能夠原滋原味的pfn_to_page和page_to_pfn。
用PFN作餌,放置到mem_map中,二者結合后生成的mem_map[PFN]變得出了對應的struct page指針,pfn_to_page()的功效應運而生;
反之,先采得struct page指針,再減去mem_map[0],二者強烈的味覺反差后便得到了PFN,一道page_to_pfn()就此出鍋;就連隔壁3歲的狗蛋子也能夠享受到這對表達式帶來的干脆與直接。
1.2 SPARSEMEM模型
當時光的列車緩緩駛過kernel.org社區,51歲的Linus Torvalds就在那里,深情的目光望過去都是自己22歲風馳電掣的影子;
當歲月含淚悄悄轉身,51歲的Linus 就在那里,深情的目光望去勾勒出自己老去的樣子。
FLAT內存模型雖然簡單,但是世界在變,計算機也在改變,而內存也不再是我們認識的那個單純的內存:
內存不一定再是從物理地址"0"開始;物理內存也不一定都是連續的,相反物理內存之間可能存在空洞;NUMA的出現,讓FLAT模型中的mem_map[]更是無以為繼;hotplug memory更是讓FLAT模型徹底淡出公眾視線。
整個內存模型的發展過程經歷了FLAT、DISCONTIGMEM直到現在的SPARSEMEM,內存模型定型下來。
Sparse是稀疏的意思,也就是說內存不是連續、平坦的。FLAT內存模型要為整個物理內存區間建立連續的struct page數組,這勢必會再物理內存中間有空洞存在的情況下造成巨大的浪費;DISCONTIGMEM雖然解決了這個問題,但是對於hot plug/remove和numa支持有弱點;最終SPARSEMEM解決了DISCONTIGMEM存在的問題。
坐穩了, 下面開始嚴肅的講講技術。
二 經典SPARSEMEM模型pfn_to_page
這里是以"經典"SPARSEMEM模型進行分析的。什么是"經典"模型呢?也就是SPARSEMEM最初的發明的模型,具體一點就是CONFIG_SPARSEMEM_EXTREME=n、CONFIG_SPARSEMEM_VMEMMAP=n,這兩個后面再分析。
接下來看看SPARSEMEM模型是如何來管理頁框的,pfn_to_page在是一個宏,在SPARSEMEM的實現如下所示:
這個宏表達式中,pfn就是頁框號,這個前面有講,這里不必多說。
這里有一個struct mem_section數據結構是干什么的呢?
2.1 SPARSEMEM中的mem_section
在FLAT模型中使用一個數組來裝下物理內存空間對應的所有struct page;而在SPARSEMEM中,則是將物理空間分割為多個"區"進行管理,一個"區"管理若干數量的struct pages,linux中將這個"區"稱為mem_section。拿經典的48bit物理地為例,bit[0,29]這30個bit用來表示一個mem_section。即一個物理地址的低30bit作為一個mem_section內部offset,經典模型中高18bit用來作mem_sections集合中的索引。(不過在linux5.11版本中arm64/sparsemem: reduce SECTION_SIZE_BITS 這個補丁為了降低memory hotplug粒度,將一個section的粒度進行了降低,為了說明方便,這里還是以30bit作為粒度來進行分析)。
圖1 48位物理地址划分
如上圖所示:按照mem_section這樣的划分粒度,一個mem section中最多可以表示1G大小的物理空間,對於經典的4KB的大小頁面而言,即一個mem section中最多可以表示2^18個PFN,即需要有2^18個strut page來進行管理,這些struct pages就是一個mem section所要管理的內容;而要將48bit物理地址所表示的物理內存都能夠囊括進來,就需要2^18個mem_section,即最多可以表示的頁框數為:
ok,說完了mem_section的粒度,下面繼續看看轉換的流程。
2.2 通過pfn號找到該頁所屬的struct mem_section
上面就是通過pfn號找到該頁所屬的mem_section。
和FLAT內存模型類似, SPARSEMEM也是通過頁框號來尋址到對應的struct page指針, 只不過在SPARSEMEM內存模型中先要找到頁框所在的mem_section。
前面已經提到物理地址的高18位用以表示mem_section集合中的索引;對於經典的SPARSE內存模型來說這些mem sections數據結構是放到一個名為mem_section[NR_MEM_SECTIONS][1]數組中的。數組的第二維只有一個元素,因而你可以就理解為mem_section[NR_MEM_SECTIONS]。
其中NR_MEM_SECTIONS定義在include/linux/mmzone.h文件中
即總共有NR_MEM_SECTIONS個mem_section區域;這樣就可以通過PFN的高位bit和SECTIONS_SHIFT相與即可得到該頁框所在的mem_section[]數組中的的索引index,然后從數組取得該頁框所所屬的struct mem_sectoin。
- pfn_to_section_nr(pfn):尋找頁框所屬mem section索引
- __nr_to_section(nr):根據索引從數組中取得對應的struct mem_section結構指針。
在經典SPARSEMEM模型中,SECTION_ROOT_MASK=0,而SECTION_NR_TO_ROOT(nr)就等價於nr。
總結一下:通過PFN尋找頁框所屬的mem section,就是根據PFN高位bit獲得section index,然后再mem_section[]數組中取得struct mem_section即可。
2.3 獲取頁框對應的struct page
前面根據PFN找到了對應的struct mem_section, 但是我們的最終目標是找到struct page,在SPARSE內存模型中struct page放在哪里的呢?答案:和FLAT內存模型類似,struct page指針也放在一個數組中,這個數組的地址存放在struct mem_section成員section_mem_map中(和FLAT模型mem_map名字有幾分相似),下面是一個精簡后的mem_section結構。
2.3.1 神奇的section_mem_map
成員section_mem_map是一個unsigned long類型。可別小看這個成員,這個小小的section_mem_map容納了如下內容:(1)struct pages數組地址,(2)若干mem section狀態標志,(3)該mem section中struct pages數組第一個page元素地址與第一個該section第一個PFN的偏移。
其中(1)和(2)可能都還比較好理解,我們重點看看(3)。存放struct page的數組mem_map[]通過數組索引獲取到的指定的struct page,其中的第一個page元素的索引位0,但是它的PFN卻並不一定是0,因而需要將這個轉換關系存儲起來。Linux將首個struct page元素的地址,實際上也就是數組地址mem_map與PFN的差值(mem_map-PFN) 存放到section_mem_map中,這樣不論是pfn_to_page還是page_to_pfn都能夠隨心所欲的通過pfn作為mem_map的索引了,巧妙!
2.3.2 get到struct page
萬事俱備,只欠東風。
前面在2.2中已經獲取PFN所屬的mem_section,下面三個步驟最終獲取到對應的struct page指針。
- mem_section獲取到成員section_mem_map,
- 去掉低位bit的編碼得到了該section中struct pages數組地址(根據上面2.3.1描述,實際上里面還編碼了一個與PFN的偏移量);
- 最后用PFN的低位作為數組偏移就找到了PFN對應的struct page指針。
上面3個步驟就是__section_mem_map_addr(__sec) + __pfn的實現流程,其中__section_mem_map_addr的代碼如下:
三 經典SPARSEMEM模型page_to_pfn
如果知道了struct page指針,如何獲得對應的物理頁框號呢?下面是具體的步驟:
- 從page->flags中取得它所屬的mem section索引號__sec;
- 使用索引號mem_section[__sec][0]從數組中取出struct mem_section地址section;
- 從section->section_mem_map解碼出該section中struct pages數組地址map與PFN的差值,即(map - PFN_OFFSET)(參考上面2.3.1);
- 通過[page - (map - PFN_OFFSET) ]即可得到page對應的頁框號PFN。
如下就是與上面對應的代碼實現:
#define __page_to_pfn(pg) \ ({ const struct page *__pg = (pg); \ int __sec = page_to_section(__pg); \ (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \ })

四 SPARSEMEM增強版
4.1 SPARSEMEM_EXTREME
在SPARSEMEM加入到linux幾個月后,SPARSEMEM_EXTREME又被引入到kernel,這個特性是針對極度稀疏物理內存對SPARSEMEM模型進行的一種擴展。這種擴展的理由在於SPARSEMEM模型中使用了一個長度為NR_MEM_SECTIONS的struct mem_section數組來表示所有可能的mem sections。對於一些極度稀疏的物理內存,並不會用到這么多的mem sections,因而是一種浪費。
SPARSEMEM_EXTREME對物理地址中表示mem sections的bit位再次進行了划分,將SECTIONS_PER_ROOT個section划分為一個SECTION_ROOT。SECTIONS_PER_ROOT的值是一個頁能夠容納struct mem_strcut的個數;即:
因而在CONFIG_SPARSEMEM_EXTREME=y時mem_section不再是一個全局靜態數組,而是一個struct **mem_section指針,這個mem_secton指針數組成員在系統內存初始化階段,對於真實存在物理頁框的mem sections才分配struct mem_section:
4.2 SPARSEMEM_VMEMMAP
2007年引入了一個新的SPARSEMEM增強特性,稱之為 Generic Virtual Memmap support for SPARSEMEM, or SPARSEMEM_VMEMMAP。引入這個特性的原因是因為經典SPARSEMEM不僅在進行 pfn_to_page() 和 page_to_pfn()時頗為復雜,而且需要消耗寶貴的page->flags bit位資源用於存放section 索引。
SPARSEMEM_VMEMMAP的實現思路非常簡潔:在虛擬地址空間中划分出一個連續地址區域用於和物理頁框號一一映射,這樣一定這個虛擬區域的首地址確定下來,系統中所有物理頁框對應的struct page也就確定下來,即
上面就是CONFIG_SPARSEMEM_VMEMMAP=y時的struct page與PFN之間的轉換方法,和FLAT模式一樣簡單純潔。不過這里有一個vmemmap作為計算的基礎,它是什么呢?怎么來的呢?
我們首先看看它的定義:
memstart_addr:當前系統中實際的物理起始地址。現在物理起始地址很多都不是從0開始的。
VMEMMAP_STRAT:在arm64中虛擬地址空間布局中VMEMMAP區域的起始虛擬地址,VMEMMAP區域的具體情況可參考Memory Layout on AArch64 Linux。
至於VMEMMAP_STRAT 要減去一個(memstart_addr >> PAGE_SHIFT)的原因,就是為了確保VMEMMAP這個區域的第一個struct page指針對應的是系統中第一個實際有效物理頁框。
不過在使用vmemmap前需要建立它和struct pages數組的虛實映射關系,這個的是在如下流程中初始化的。