歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 深入理解Linux內存管理機制

深入理解Linux內存管理機制

日期:2017/2/27 15:57:46   编辑:Linux教程
通過本文,你可以了解:
1. 存儲器硬件結構;
2.分段以及對應的組織方式;
3.分頁以及對應的組織方式。
注1:本文以Linux內核2.6.32.59本版為例,其對應的代碼可以在http://www.kernel.org/pub/linux/kernel/v2.6/longterm/v2.6.32/linux-2.6.32.59.tar.bz2找到。
注2:本文所有的英文專有名詞都是我隨便翻譯的,請對照英文原文進行理解。
注3:推薦使用Source Insight進行源碼分析。

內存組織
計算機內存屬於隨機存儲器(RAM),目前PC機廣泛使用的是DDR

SDRAM,即“雙倍速率同步動態隨機存儲器”,其本質上仍然是由n bits*m KB個內存芯片組成的,比如如果我們需要8位64KB的內存,則我們就需要2*8=16塊4bits*8KB的內存塊。由於計算機通常是以字節(Byte)進行數據交換的,所以對內存的地址編碼一般使用字節,如上我們有64KB內存,則其地址編碼為0×0000~0xFFFF,稱為物理地址。對於32位機來說,由於其“地址寄存器(AR)”是32位,也就限制了其內存的最大尋址范圍是2^32=4GB。

Linux將物理地址按4KB的大小劃分成“幀(Frame)”。為什麼是4KB?因為每一個幀都需要用一個C結構體來描述,稱之為“幀描述單元(Frame Discriptor)”,如果太小,幀描述單元顯然太多了,如果太大,那麼在內存分配時又會造成“內碎片(InnerFragments)”。早些時候,計算機的內存址都是直接映射的,由於程序裡的地址是寫死的,這就意味著每段程序每次都只能映射對應的地址空間。這無論對程序設計者與系統都是相當大的負擔。Linux使用“分段”加“分頁”來解決此問題。由於它們的存在,內存地址進入了邏輯地址時代。Linux有三種地址:邏輯地址(LogicAddress)、線性地址(Linear Address)與物理地址(Physics Address)。其關系如下:

另外,Linux支持眾多CPU架構,這裡只研究X86的,對應的源代碼為:…/X86/… 路徑。

Linux中的分段
Linux 並不使用太多的分段,原因是某些RISC機器對分段的支持不好。為此Linux的分段都存在“全局描述表(GDT)”中,GDT是一個全局 desc_struct數組(位於linux-2.6.32.59\arch\x86\include\asm),其結構如下:

#define GDT_ENTRIES 16  
  
struct desc_struct gdt[GDT_ENTRIES];  
  
struct desc_struct {  
    union {  
        struct {  
            unsigned int a;  
            unsigned int b;  
        };  
        struct {  
            u16 limit0; // 段大小  
            u16 base0; // 段起始位置  
            unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段類型,占4位;dpl指的段運行權限,占2位  
            unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示內存地址位寬,占1位  
        };  
    };  
} __attribute__((packed));  
所以我們可以看出,段描述結構體占8個字節,至於裡面的a,b,那是老的方式,後來使用C++ Struts的Bit Fields後更方便了。type類型由以下幾種:
enum {  
    DESC_TSS = 0×9,  
    DESC_LDT = 0×2,  
    DESCTYPE_S = 0×10,  /* !system */  
};
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.59\arch\x86\include\asm\segment.h,裡面有非常詳細的說明。

它們都存儲在“全局描述符表(GDT)”。Linux本身並不使用“局部描述符表(LDT)”,當一個進程被創建時,其指向的是一個默認的LDT,不過系統並不阻止進程創建它。也就是說一個進程最多兩個段描述符:TSS與LDT。由於Segment Selector為16位(為什麼只有16位,這個就是歷史原因了,由於X86在Real Mode下段地址只有20位,其中有效的就是16位,詳見:x86

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,所有的進程共享一個。這樣地址空間不會重復嗎?不會,因為線性不是最終的物理地址,每個進程還有自己的頁表,所以最終映射到物理地址是不同的。

下面我們來看看段中地址是如何轉換的。假設我們需要訪問內核數據段的0×00124部分,由代碼知其GDT的入口為13,那麼其對應的內存地址=gdtr+13*8+0×00124,假設gptr為0×02000,則最終的結果為0×02228。gdtr是一個寄存器,其為48位,用來保存GDT的第一個字節線性地址與表限。其過程如圖所示: 圖片來源於《Understand The Linux Kernel》
分頁
相對於分段來說,分頁更主流更流行一些。原因是其更靈活,其能把不同的線性地址映射到同一個物理地址上,缺點是內存必須以頁大小的整數倍分配。按現在主流的4KB一頁來說,如果程序只申請100B的數據,那內存浪費還是相當的大。為此,Linux使用了一種稱為Slab的方法來解決這個問題,後面的文章會講到。

因為頁表本身也需要存儲空間,按每頁32B來算,對於4GB內存,每頁4KB,共有1M頁,則頁表的大小為32MB,這顯然不可以接受,所以後來出現了多級頁表這個概念。2004年後Linux版本使用的是四級頁表:第一級叫“全局目錄(Page

Global Directory)“、第二級叫“頁上級目錄(Page

Upper Directory)”、第三級叫”頁中間目錄(Page

Middle Derectory)”、第四級叫”頁面表(Page

Table Entry)”,最後頁內偏移量“offset”,如下圖:

圖中的cr3是一個寄存器,它存儲“Global DIR”的地址。當進程切換發生時,它將被保存在TSS中,前面說過了TSS段表是每個進程一個。分頁在Linux內使用的地方很多,特別是進程內的地址轉換。分頁有硬件支持的,特別是旁路轉換緩沖(Translation

Lookaside Buffer)的出現,使用即使使用三級頁表的Linux在地轉轉換中的實際效果也是非常好的。與段表所有的進程都共用一個的是,每個進程都擁有自己的分頁。其實也正是因為所有進程都共享一個段表,每個進程才必須有自己的頁表,否則相同的linear地址如何映射到不同的物理地址去?下面我們著重來研究一下Linux系統中是如何表示分頁中所用到的數據結構的。

每個“幀”在Linux中都是以一個名為page(位於linux-2.6.32.59\include\linux\Mm_types.h)的結構體來存儲的。所有的頁被放在一個類型為page名為mem_map的數組中(位於linux-2.6.32.59\mm\Memory.c)。代碼如下(為了顯示方便,僅列出部分:
struct page {  
unsigned long flags;          /* 幀的標志位,用枚舉pageflags(位於:linux-2.6.32.59\include\linux\Page-flags.h)表示,每個值的意義詳見注釋 */  
…  
    atomic_t _count;        /* 該幀被引用的數量 */  
    union {  
        atomic_t _mapcount; /* 所有指向該幀的頁表數量*/  
        …  
    };  
    union {  
        struct {  
        unsigned long private;      /*根據此頁的使用情況會有不同的意義,詳見源碼注釋*/  
        …  
        };  
…  
    };  
      
union {  
        pgoff_t index;      /* 重要:類型即unsinged long, 指向物理幀號 */  
…  
    };  
  
  
    struct list_head lru;       /* 指向最近被使用的頁的雙向鏈表,cache相關*/  
};
下面我們再來看看PGD頁表。每個進程的mm_struct->pgd(位於:linux-2.6.32.59\include\linux\Mm_types.h)指向自己的PGD:
struct mm_struct {  
        …  
    pgd_t * pgd;  
    …       
}
可以看出pdg實際上是一個pgd_t結構數組,pgd_t在X86系統中就是一個usinged long,其指向的就是下一級頁表的地址。就這樣找下去,直到找到對應的頁為止,再加上頁內偏移,就可以進行內存訪問了。

例如線性地址為:0x91220B01,如下圖,如果PGD、PUD、PMD以及PTE均5位。頁內偏移12位,即頁大小4KB。 那麼這段內存的解析步驟是:
  1. PGD號為24,查PGD[24]得到PUD入口;
  2. PUD號為4,再查PUD[4];
  3. PMD號為36,再查PMD[36];
  4. PTE號為2,再查PTE[2];
  5. 如果最終幀地址為a:那麼最後的物理地址就是a+0×0301
需要補充的是,並不是所有的內存都是使用“分頁”,在內核初始化的時候,有100MB內存的樣子是使用直接映射的,這是因為總是要先裝入分頁的初始化代碼才能進行頁表初始化。

總結:不知不覺也寫了不少了。這次我們介紹了操作系統最基本的內存管理概念“分段”與“分頁”在Linux中的實現,可以看出其與通過的概念還是很接近的。這正證明了基礎知識的重要性。下一次我們將介紹Linux的內存初始化過程,如頁表的建立與初始化。
轉自:http://rdc.taobao.com/team/jm/archives/2063
Copyright © Linux教程網 All Rights Reserved