天天看點

深入了解Linux記憶體管理機制(一)

深入了解linux記憶體管理機制(一)通過本文,您即可以:

1. 存儲器硬體結構;

2.分段以及對應的組織方式;

3.分頁以及對應的組織方式。

<a></a>

注2:本文所有的英文專有名詞都是我随便翻譯的,請對照英文原文進行了解。

注3:推薦使用source insight進行源碼分析。

記憶體組織

address)、線性位址(linear address)與實體位址(physics address)。其關系如下:

深入了解Linux記憶體管理機制(一)

另外,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,所有的程序共享一個。這樣位址空間不會重複嗎?不會,因為線性不是最終的實體位址,每個程序還有自己的頁表,是以最終映射到實體位址是不同的。

深入了解Linux記憶體管理機制(一)

圖檔來源于《understand the linux kernel》

分頁

相對于分段來說,分頁更主流更流行一些。原因是其更靈活,其能把不同的線性位址映射到同一個實體位址上,缺點是記憶體必須以頁大小的整數倍配置設定。按現在主流的4kb一頁來說,如果程式隻申請100b的資料,那記憶體浪費還是相當的大。為此,linux使用了一種稱為slab的方法來解決這個問題,後面的文章會講到。

global directory)“、第二級叫“頁上級目錄(page

upper directory)”、第三級叫”頁中間目錄(page

middle derectory)”、第四級叫”頁面表(page

深入了解Linux記憶體管理機制(一)

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-&gt;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,其指向的就是下一級頁表的位址。就這樣找下去,直到找到對應的頁為止,再加上頁内偏移,就可以進行記憶體通路了。

深入了解Linux記憶體管理機制(一)

那麼這段記憶體的解析步驟是:

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"

繼續閱讀