深入了解linux記憶體管理機制(一)通過本文,您即可以:
1. 存儲器硬體結構;
2.分段以及對應的組織方式;
3.分頁以及對應的組織方式。
<a></a>
注2:本文所有的英文專有名詞都是我随便翻譯的,請對照英文原文進行了解。
注3:推薦使用source insight進行源碼分析。
記憶體組織
address)、線性位址(linear address)與實體位址(physics address)。其關系如下:

另外,linux支援衆多cpu架構,這裡隻研究x86的,對應的源代碼為:…/x86/… 路徑。
linux中的分段
linux并不使用太多的分段,原因是某些risc機器對分段的支援不好。為此linux的分段都存在“全局描述表(gdt)”中,gdt是一個全局desc_struct數組(位于linux-2.6.32.59archx86includeasm),其結構如下:
1. #define gdt_entries 16
2.
3. struct desc_struct gdt[gdt_entries];
4.
5. struct desc_struct {
6. union {
7. struct {
8. unsigned int a;
9. unsigned int b;
10. };
11. struct {
12. u16 limit0; // 段大小
13. u16 base0; // 段起始位置
14. unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段類型,占4位;dpl指的段運作權限,占2位
15. unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示記憶體位址位寬,占1位
16. };
17. };
18. } attribute((packed));
1. enum {
2. desc_tss = 0x9,
3. desc_ldt = 0x2,
4. desctype_s = 0x10, / !system /
5. };
linux主要使用以下幾種段: 核心代碼段(kernel code segment):type=10,dpl=0
核心資料段(kernel data segment):type=2,dpl=0 使用者代碼段(user code segment):type=10,dpl=3
使用者資料段(user data segment):type=2,dpl=3 任務狀态段(task state segment),每程序一個:type=9,dpl=3
其它類型可以參見linux-2.6.32.59archx86includeasmsegment.h,裡面有非常詳細的說明。
memory segmentation,但linux段内偏移位址高達32位,是以線性位址總共是48位),其中有效的索引位僅有13位,是以gdt的最大長度為213-1=8192,除去系統保留的12個,留給程序的隻有8180個入口,那麼就意味linux程序的最大數為8180/2=4090。需要注意的是,程序在建立的時候并不會馬上建立自己的ldt,其指向的是gdt一個預設的ldt,裡面的sd為null。隻有在需要的時候程序才建立自己的ldt并把它放入gdt中。是以不管是ldt也好,tss也好,它們都存放在gdt裡面。而對于ucs與uds,所有的程序共享一個。這樣位址空間不會重複嗎?不會,因為線性不是最終的實體位址,每個程序還有自己的頁表,是以最終映射到實體位址是不同的。
圖檔來源于《understand the linux kernel》
分頁
相對于分段來說,分頁更主流更流行一些。原因是其更靈活,其能把不同的線性位址映射到同一個實體位址上,缺點是記憶體必須以頁大小的整數倍配置設定。按現在主流的4kb一頁來說,如果程式隻申請100b的資料,那記憶體浪費還是相當的大。為此,linux使用了一種稱為slab的方法來解決這個問題,後面的文章會講到。
global directory)“、第二級叫“頁上級目錄(page
upper directory)”、第三級叫”頁中間目錄(page
middle derectory)”、第四級叫”頁面表(page
lookaside buffer)的出現,使用即使使用三級頁表的linux在地轉轉換中的實際效果也是非常好的。與段表所有的程序都共用一個的是,每個程序都擁有自己的分頁。其實也正是因為所有程序都共享一個段表,每個程序才必須有自己的頁表,否則相同的linear位址如何映射到不同的實體位址去?下面我們着重來研究一下linux系統中是如何表示分頁中所用到的資料結構的。
每個“幀”在linux中都是以一個名為page(位于linux-2.6.32.59includelinuxmm_types.h)的結構體來存儲的。所有的頁被放在一個類型為page名為mem_map的數組中(位于linux-2.6.32.59mmmemory.c)。代碼如下(為了顯示友善,僅列出部分:
1. struct page {
2. unsigned long flags; / 幀的标志位,用枚舉pageflags(位于:linux-2.6.32.59includelinuxpage-flags.h)表示,每個值的意義詳見注釋 /
3. …
4. atomic_t _count; / 該幀被引用的數量 /
5. union {
6. atomic_t _mapcount; / 所有指向該幀的頁表數量/
7. …
8. };
9. union {
10. struct {
11. unsigned long private; /根據此頁的使用情況會有不同的意義,詳見源碼注釋/
12. …
13. };
14. …
15. };
16.
17. union {
18. pgoff_t index; / 重要:類型即unsinged long, 指向實體幀号 /
19. …
20. };
21.
22.
23. struct list_head lru; / 指向最近被使用的頁的雙向連結清單,cache相關/
24. };
下面我們再來看看pgd頁表。每個程序的mm_struct->pgd(位于:linux-2.6.32.59includelinuxmm_types.h)指向自己的pgd:
1. struct mm_struct {
2. …
3. pgd_t pgd;
4. …
5. }
可以看出pdg實際上是一個pgd_t結構數組,pgd_t在x86系統中就是一個usinged long,其指向的就是下一級頁表的位址。就這樣找下去,直到找到對應的頁為止,再加上頁内偏移,就可以進行記憶體通路了。
那麼這段記憶體的解析步驟是:
1. pgd号為24,查pgd[24]得到pud入口;
2. pud号為4,再查pud[4];
3. pmd号為36,再查pmd[36];
4. pte号為2,再查pte[2];
5. 如果最終幀位址為a:那麼最後的實體位址就是a+0x0301
需要補充的是,并不是所有的記憶體都是使用“分頁”,在核心初始化的時候,有100mb記憶體的樣子是使用直接映射的,這是因為總是要先裝入分頁的初始化代碼才能進行頁表初始化。
總結:不知不覺也寫了不少了。這次我們介紹了作業系統最基本的記憶體管理概念“分段”與“分頁”在linux中的實作,可以看出其與通過的概念還是很接近的。這正證明了基礎知識的重要性。下一次我們将介紹linux的記憶體初始化過程,如頁表的建立與初始化。
本文來源于"阿裡中間件團隊播客",原文發表時間" 2012-07-31"