1. 程式人生 > 其它 >【轉載】 記憶體初始化(上)

【轉載】 記憶體初始化(上)

 

原文連結: http://www.wowotech.net/memory_management/mm-init-1.html  推薦

原文連結: http://www.wowotech.net/memory_management/mm-init-1.html 強烈推薦

 

 

記憶體初始化(上)

作者:linuxer 釋出於:2016-10-13 12:05 分類:記憶體管理

一、前言

一直以來,我都非常著迷於兩種電影拍攝手法:一種是慢鏡頭,將每一個細節全方位的展現給觀眾。另外一種就是快鏡頭,多半是反應一個時代的變遷,從非常長的時間段中,擷取幾個典型的snapshot,合成在十幾秒的鏡頭中,可以讓觀眾很快的瞭解一個事物的發展脈絡。對應到技術層面,慢鏡頭有點類似情景分析,把每一行程式碼都詳細的進行解析,瞭解技術的細節。快鏡頭類似資料流分析,勾勒一個過程中,資料結構的演化。本文采用了快鏡頭的方法,對記憶體初始化部分進行描述,不糾纏於具體函式的程式碼實現,只是希望能給大家一個概略性的印象(有興趣的同學可以自行研究程式碼)。BTW,在本文中我們都是基於ARM64來描述體系結構相關的內容。

 

二、啟動之前

在詳細描述linux kernel對記憶體的初始化過程之前,我們必須首先了解kernel在執行第一條語句之前所面臨的處境。這時候的記憶體狀況可以參考下圖:

bootloader有自己的方法來了解系統中memory的佈局,然後,它會將綠色的kernel image和藍色dtb image copy到了指定的記憶體位置上。kernel image最好是位於main memory起始地址偏移TEXT_OFFSET的位置,當然,TEXT_OFFSET需要和kernel協商好。kernel image是否一定位於起始的main memory(memory address最低)呢?也不一定,但是對於kernel而言,低於kernel image的記憶體,kernel是不會納入到自己的記憶體管理系統中的。對於dtb image的位置,linux並沒有特別的要求。由於這時候MMU是turn off的,因此CPU只能看到實體地址空間。對於cache的要求也比較簡單,只有一條:kernel image對應的cache必須clean to PoC,即系統中所有的observer在訪問kernel image對應記憶體地址的時候是一致性的。

 

三、彙編時代

一旦跳轉到linux kernel執行,核心則完全掌控了記憶體系統的控制權,它需要做的事情首先就是要開啟MMU,而為了開啟MMU,必須要建立linux kernel正常執行需要的頁表,這就是本節的主要內容。

在體系結構相關的彙編初始化階段,我們會準備二段地址的頁表:一段是identity mapping,其實就是把地址等於實體地址的那些虛擬地址mapping到實體地址上去,開啟MMU相關的程式碼需要這樣的mapping(別的CPU不知道,但是ARM ARCH強烈推薦這麼做的)。第二段是kernel image mapping,核心程式碼歡快的執行當然需要將kernel running需要的地址(kernel txt、rodata、data、bss等等)進行映射了。具體的對映情況可以參考下圖:

turn on MMU相關的程式碼被放入到一個特別的section,名字是.idmap.text,實際上對應上圖中實體地址空間的IDMAP_TEXT這個block。這個區域的程式碼被mapping了兩次,做為kernel image的一部分,它被對映到了__idmap_text_start開始的虛擬地址上去,此外,假設IDMAP_TEXT block的實體地址是A地址,那麼它還被對映到了A地址開始的虛擬地址上去。雖然上圖中表示的A地址似乎要大於PAGE_OFFSET,不過實際上不一定需要這樣的關係,這和具體處理器的實現有關。

編譯器感知的是kernel image的虛擬地址(左側),在核心的連結指令碼中定義了若干的符號,都是虛擬地址。但是在核心剛開始,沒有開啟MMU之前,這些程式碼實際上是執行在實體地址上的,因此,核心起始剛開始的彙編程式碼基本上是PIC的,首先需要定位到頁表的位置,然後在頁表中填入kernel image mapping和identity mapping的頁表項。頁表的起始位置比較好定(bss段之後),但是具體的size還是需要思考一下的。我們要選擇一個合適的size,確保能夠覆蓋kernel image mapping和identity mapping的地址段,然後又不會太浪費。我們以kernel image mapping為例,描述確定Tranlation table size的思考過程。假設48 bit的虛擬地址配置,4k的page size,這時候需要4級對映,地址被分成9(level 0 or PGD) + 9(level 1 or PUD) + 9(level 2 or PMD) + 9(level 3 or PTE) + 12(page offset),假設我們分配4個page分別儲存Level 0到level 3的translation table,那麼可以建立的最大的地址對映範圍是512(level 3中有512個entry) X 4k = 2M。2M這個size當然不理想,無法容納kernel image的地址區域,怎麼辦?使用section mapping,讓PMD執行block descriptor,這樣使用3個page就可以mapping 512 X 2M = 1G的地址空間範圍。當然,這種方法有一點副作用就是:PAGE_OFFSET必須2M對齊。對於16K或者64K的page size,使用section mapping就有點不合適了,因為這時候對齊的要求太高了,對於16K page size,需要32M對齊,對於64K page size,需要512M對齊。不過,這也沒有什麼,畢竟這時候page size也變大了,不使用section mapping也能覆蓋很大區域。例如,對於16K page size,一個16K page size中可以儲存2K個entry,因此能夠覆蓋2K X 16K = 32M的地址範圍。對於64K page size,一個64K page size中可以儲存8K個entry,因此能夠覆蓋8K X 64K = 512M的地址範圍。32M和512M基本是可以滿足需求的。最後的結論:swapper程序(核心空間)需要預留頁表的size是和page table level相關,如果使用了section mapping,那麼需要預留PGTABLE_LEVELS - 1個page。如果不使用section mapping,那麼需要預留PGTABLE_LEVELS 個page。

上面的結論起始是適合大部分情況下的identity mapping,但是還是有特例(需要考慮的點主要和其實體地址的位置相關)。我們假設這樣的一個配置:虛擬地址配置為39bit,而實體地址是48個bit,同時,IDMAP_TEXT這個block的地址位於高階地址(大於39 bit能表示的範圍)。在這種情況下,上面的結論失效了,因為PGTABLE_LEVELS 是和虛擬地址的bit數、PAGE_SIZE的定義相關,而是和實體地址的配置無關。linux kernel使用了巧妙的方法解決了這個問題,大家可以自己看程式碼理解,這裡就不多說了。

一旦設定完了頁表,那麼開啟MMU之後,kernel正式就會進入虛擬地址空間的世界,美中不足的是核心的虛擬世界沒有那麼大。原來擁有的整個實體地址空間都消失了,能看到的僅僅剩下kernel image mapping和identity mapping這兩段地址空間是可見的。不過沒有關係,這只是剛開始,記憶體初始化之路還很長。

 

四、看見DTB

雖然可以通過kernel image mapping和identity mapping來窺探實體地址空間,但終究是管中窺豹,不瞭解全域性,那麼核心是如何瞭解對端的物理世界呢?答案就是DTB,但是問題來了,這時候,核心還沒有為DTB這段記憶體建立對映,因此,開啟MMU之後的kernel還不能直接訪問,需要先建立dtb mapping,而要建立address mapping,就需要分配頁表記憶體,而這時候,還沒有了解記憶體佈局,記憶體管理模組還沒有初始化,如何來分配記憶體呢?

下面這張圖片給出瞭解決方案:

整個虛擬地址空間那麼大,可以被平均分成兩半,上半部分的虛擬地址空間主要各種特定的功能,而下半部分主要用於實體記憶體的直接對映。對於DTB而言,我們借用了fixed-mapped address這個概念。fixed map是被linux kernel用來解決一類問題的機制,這類問題的共同特點是:(1)在很早期的階段需要進行地址對映,而此時,由於記憶體管理模組還沒有完成初始化,不能動態分配記憶體,也就是無法動態分配建立對映需要的頁表記憶體空間。(2)實體地址是固定的,或者是在執行時就可以確定的。對於這類問題,核心定義了一段固定對映的虛擬地址,讓使用fix map機制的各個模組可以在系統啟動的早期就可以建立地址對映,當然,這種機制不是那麼靈活,因為虛擬地址都是編譯時固定分配的。

好,我們可以考慮建立第三段地址映射了,當然,要建立地址對映就要建立各個level中描述符。對於fixed-mapped address這段虛擬地址空間,由於也是位於核心空間,因此PGD當然就是複用swapper程序的PGD了(其實整個系統就一個PGD),而其他level的Translation table則是靜態定義的(arch/arm64/mm/mmu.c),位於核心bss段,由於所有的Translation table都在kernel image mapping的範圍內,因此核心可以毫無壓力的訪問,並建立fixed-mapped address這段虛擬地址空間對應的PUD、PMD和PTE的entry。所有中間level的Translation table都是在early_fixmap_init函式中完成初始化的,最後一個level則是在各個具體的模組進行的,對於DTB而言,這發生在fixmap_remap_fdt函式中。

系統對dtb的size有要求,不能大於2M,這個要求主要是要確保在建立地址對映(create_mapping)的時候不能分配其他的translation table page,也就是說,所有的translation table都必須靜態定義。為什麼呢?因為這時候記憶體管理模組還沒有初始化,即便是memblock模組(初始化階段分配記憶體的模組)都尚未初始化(沒有記憶體佈局的資訊),不能動態分配記憶體。

 

五、early ioremap

除了DTB,在啟動階段,還有其他的模組也想要建立地址對映,當然,對於這些需求,核心統一採用了fixmap的機制來應對,fixmap的具體資訊如下圖所示:

從上面這個圖片可以看出fix-mapped虛擬地址分成兩段,一段是permanent fix map,一段是temporary fixmap。所謂permanent表示對映關係永遠都是存在的,例如FDT區域,一旦完成地址對映,核心可以訪問DTB之後,這個對映關係一直都是存在的。而temporary fixmap則不然,一般而言,某個模組使用了這部分的虛擬地址之後,需要儘快釋放這段虛擬地址,以便給其他模組使用。

你可能會很奇怪,因為傳統的驅動模組中,大家通常使用ioremap函式來完成地址對映,為了還有一個early IO remap呢?其實ioremap函式的使用需要一定的前提條件的,在地址對映過程中,如果某個level的Translation tabe不存在,那麼該函式需要呼叫夥伴系統模組的介面來分配一個page size的記憶體來建立某個level的Translation table,但是在啟動階段,記憶體管理的夥伴系統還沒有ready,其實這時候,核心連繫統中有多少記憶體都不知道的。而early io remap則在early_ioremap_init之後就可以被使用了。更具體的資訊請參考mm/early_ioremap.c檔案。

結論:如果想要在夥伴系統初始化之前進行裝置暫存器的訪問,那麼可以考慮early IO remap機制。

 

六、記憶體佈局

完成DTB的對映之後,核心可以訪問這一段的記憶體了,通過解析DTB中的內容,核心可以勾勒出整個記憶體佈局的情況,為後續記憶體管理初始化奠定基礎。收集記憶體佈局的資訊主要來自下面幾條途徑:

(1)choosen node。該節點有一個bootargs屬性,該屬性定義了核心的啟動引數,而在啟動引數中,可能包括了mem=nn[KMG]這樣的引數項。initrd-start和initrd-end引數定義了initial ramdisk image的實體地址範圍。

(2)memory node。這個節點主要定義了系統中的實體記憶體佈局。主要的佈局資訊是通過reg屬性來定義的,該屬性定義了若干的起始地址和size條目。

(3)DTB header中的memreserve域。對於dts而言,這個域是定義在root node之外的一行字串,例如:/memreserve/ 0x05e00000 0x00100000;,memreserve之後的兩個值分別定義了起始地址和size。對於dtb而言,memreserve這個字串被DTC解析並稱為DTB header中的一部分。更具體的資訊可以參考device tree基礎文件,瞭解DTB的結構。

(4)reserved-memory node。這個節點及其子節點定義了系統中保留的記憶體地址區域。保留記憶體有兩種,一種是靜態定義的,用reg屬性定義的address和size。另外一種是動態定義的,只是通過size屬性定義了保留記憶體區域的長度,或者通過alignment屬性定義對齊屬性,動態定義型別的子節點的屬性不能精準的定義出保留記憶體區域的起始地址和長度。在建立地址對映方面,可以通過no-map屬性來控制保留記憶體區域的地址對映關係的建立。更具體的資訊可以閱讀參考文獻[1]。

通過對DTB中上述資訊的解析,其實核心已經基本對記憶體佈局有數了,但是如何來管理這些資訊呢?這也就是著名的memblock模組,主要負責在初始化階段用來管理實體記憶體。一個參考性的示意圖如下:

核心在收集了若干和memory相關的資訊後,會呼叫memblock模組的介面API(例如:memblock_add、memblock_reserve、memblock_remove等)來管理這些記憶體佈局的資訊。核心需要動態管理起來的記憶體資源被儲存在memblock的memory type的陣列中(上圖中的綠色block,按照地址的大小順序排列),而那些需要預留的,不需要核心管理的記憶體被儲存在memblock的reserved type的陣列中(上圖中的青色block,也是按照地址的大小順序排列)。要想了解進一步的資訊,請參考核心程式碼中的setup_machine_fdt和arm64_memblock_init這兩個函式的實現。

 

七、看到記憶體

瞭解到了當前的實體記憶體的佈局,但是核心仍然只是能夠訪問部分記憶體(kernel image mapping和DTB那兩段記憶體,上圖中黃色block),大部分的記憶體仍然處於黑暗中,等待光明的到來,也就是說需要建立這些記憶體的地址對映。

在這個時間點上,建立記憶體的地址對映有一個悖論:建立地址對映需要分配記憶體,但是這時候夥伴系統沒有ready,無法動態分配。也許你會說,memblock不是已經ready了嗎,不可以呼叫memblock_alloc進行實體記憶體的分配嗎?當然可以,memblock_alloc分配的實體記憶體仍然需要通過虛擬地址訪問,而這些記憶體都還沒有建立地址對映,因此核心一旦訪問memblock_alloc分配的實體記憶體,悲劇就會發生了。

怎麼辦呢?核心採用了一個巧妙的辦法:那就是控制建立地址對映,memblock_alloc分配頁表記憶體的順序。也就是說剛開始的時候建立的地址對映不需要頁表記憶體的分配,當核心需要呼叫memblock_alloc進行頁表實體地址分配的時候,很多已經建立對映的記憶體已經ready了,這樣,在呼叫create_mapping的時候不需要分配頁表記憶體。更具體的解釋參考下面的圖片:

我們知道,在核心編譯的時候,在BSS段之後分配了幾個page用於swapper程序地址空間(核心空間)的對映,當然,由於kernel image不需要mapping那麼多的地址,因此swapper程序translation table的最後一個level中的entry不會全部的填充完畢。換句話說:swapper程序頁表可以支援遠遠大於kernel image mapping那一段的地址區域,實際上,它可以支援的地址段的size是SWAPPER_INIT_MAP_SIZE。為(PAGE_OFFSET,PAGE_OFFSET+SWAPPER_INIT_MAP_SIZE)這段虛擬記憶體建立地址對映,mapping到(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)這段實體記憶體的時候,呼叫create_mapping不會發生記憶體分配,因為所有的頁表都已經存在了,不需要動態分配。

一旦完成了(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)這段實體記憶體的地址對映,這時候,終於可以自由使用memblock_alloc進行記憶體分配了,當然,要進行限制,確保分配的記憶體位於(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)這段實體記憶體中。完成所有memory type型別的memory region的地址對映之後,可以解除限制,任意分配memory了。而這時候,所有memory type的地址區域(上上圖中綠色block)都已經可見,而這些寶貴的記憶體資源就是記憶體管理模組需要管理的物件。具體程式碼請參考paging_init--->map_mem函式的實現。

 

八、結束語

目前為止,所有為記憶體管理做的準備工作已經完成:收集了整個記憶體佈局的資訊,memblock模組中已經儲存了所有需要管理memory region的資訊,同時,系統也為所有的記憶體(reserved除外)建立了地址對映。雖然整個記憶體管理系統沒有ready,但是通過memblock模組已經可以在隨後的初始化過程中進行動態記憶體的分配。 有了這些基礎,隨後就是真正的記憶體管理系統的初始化了,我們下回分解。

 

參考文獻:

1、Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt

2、linux4.4.6核心程式碼