1. 用戶空間
通常 32 位 Linux 虛擬地址空間劃分, 0-3GB為用戶空間,3GB-4GB為內(nèi)核空間。每個進程都有4GB的虛擬地址空間,其中0-3GB是自己私有的用戶空間,最高的1GB是與所有進程共享的內(nèi)核空間。Linux 進程的內(nèi)存布局如下圖所示:
進程地址空間主要分為以下幾部分:
代碼段(Text): 程序代碼在內(nèi)存中的映射,存放函數(shù)體的二進制代碼。
數(shù)據(jù)段(Data): 在程序運行初已經(jīng)對變量進行初始化的數(shù)據(jù)。
BSS段(BSS): 在程序運行初未對變量進行初始化的數(shù)據(jù)。
棧(Stack): 存儲局部,臨時變量,函數(shù)調(diào)用時存儲函數(shù)的返回指針,用于控制函數(shù)的調(diào)用和返回。在程序塊開始時自動分配內(nèi)存,結(jié)束時自動釋放內(nèi)存,其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
堆(Heap): 存儲動態(tài)內(nèi)存分配,需要程序員手工分配,手工釋放。
注意,以上所說的地址均為虛擬地址。進程虛擬地址到物理地址的轉(zhuǎn)換會在下面"段頁式存儲管理"部分詳細講解。
2. 內(nèi)核空間
在Linux中,內(nèi)核空間是持續(xù)存在的,并且在所有進程中都映射到同樣的物理內(nèi)存,內(nèi)核代碼和數(shù)據(jù)總是可尋址的,隨時準備處理中斷和系統(tǒng)調(diào)用。
物理地址 = 邏輯地址 – 0xC0000000,這是內(nèi)核地址空間3GB - 3GB+896MB的地址轉(zhuǎn)換關(guān)系,說白了就是線性映射,偏移為0xC0000000。注意內(nèi)核的虛擬地址在“高端”,但是它映射的物理內(nèi)存地址在低端。
為什么只有3GB - 3GB+896MB是線性映射,而不是整個1GB都線性映射呢?假設(shè)按照這樣簡單的地址映射關(guān)系,那么內(nèi)核地址空間訪問為3GB-4GB,對應(yīng)的物理內(nèi)存范圍就為0-1GB,即只能訪問1GB物理內(nèi)存。若機器中安裝4G物理內(nèi)存,那么內(nèi)核就只能訪問前1G物理內(nèi)存,后面3G物理內(nèi)存將會無法訪問。為了解決這個問題,Linux引入了高端內(nèi)存的概念。
高端內(nèi)存的基本思想:借一段地址空間,建立臨時地址映射,用完后釋放。達到這段地址空間可以循環(huán)使用,訪問所有物理內(nèi)存的目的。
Linux系統(tǒng)在初始化時,會根據(jù)實際的物理內(nèi)存的大小,為每個物理頁面創(chuàng)建一個page對象,所有的page對象構(gòu)成一個mem_map數(shù)組。進而針對不同的用途,Linux內(nèi)核將所有的物理頁面劃分到3類內(nèi)存管理區(qū)中,如圖,分別為ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA的范圍是0-16MB,該區(qū)域的物理頁面專門供I/O設(shè)備的DMA使用。之所以需要單獨管理DMA的物理頁面,是因為DMA使用物理地址訪問內(nèi)存,不經(jīng)過MMU,并且需要連續(xù)的緩沖區(qū),所以為了能夠提供物理上連續(xù)的緩沖區(qū),必須從物理地址空間專門劃分一段區(qū)域用于DMA。
ZONE_NORMAL的范圍是16MB-896MB,該區(qū)域的物理頁面是內(nèi)核能夠直接使用的。
ZONE_HIGHMEM的范圍是896MB-4GB,該區(qū)域即為高端內(nèi)存,內(nèi)核不能直接使用。
3. 段頁式存儲管理
程序在執(zhí)行時,傳遞給CPU的地址是邏輯地址。它由兩部分組成,一部分是段選擇符(比如cs和ds等段寄存器的值),另一部分是偏移量(比如eip寄存器的值)。邏輯地址必須經(jīng)過段式映射轉(zhuǎn)換為線性地址,線性地址再經(jīng)過頁式映射轉(zhuǎn)為物理地址,才能訪問真正的物理內(nèi)存。轉(zhuǎn)換過程如下:
3.1 段式映射
邏輯地址是以"段寄存器:偏移地址"形式存在的。段寄存器是一個16位的寄存器, 其中第0和1位控制著將要訪問段的特權(quán)級別。第2位說明是在GDT還是LDT中尋找地址,Linux程序里用的段描述符總是選擇GDT。高13位作為一個索引值。
如下圖所示,首先從GDTR寄存器中取出段描述符表(GDT)的首地址,通過段寄存器里的索引值,可以從段描述符表(GDT)里找到段的基址。 然后用基址加上段內(nèi)的偏移量,就得到了對應(yīng)的線性地址。
3.2 頁式映射
內(nèi)核把物理頁作為內(nèi)存管理的基本單位,頁面大小為4KB,整個虛擬地址空間為4GB,則需要包含1M個頁表項,這還只是一個進程,因為每個進程都有自己獨立的頁表,這樣系統(tǒng)所有的內(nèi)存都來存放頁表項恐怕都不夠。想象一下進程的虛擬地址空間,實際上大部分是空閑的,真正映射的區(qū)域幾乎是汪洋大海中的小島,因次我們可以考慮使用多級頁表,可以減少頁表內(nèi)存使用量。Linux操作系統(tǒng)使用4級頁表,4級頁表分別為:頁全局目錄、頁上級目錄、頁中間目錄、頁表。
4. Buddy
Linux采用著名的伙伴系統(tǒng)(buddy system)算法來解決外碎片問題。把所有的空閑頁框分組為11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續(xù)的頁框。對1024個頁框的最大請求對應(yīng)著4MB大小的連續(xù)RAM塊。每個塊的第一個頁框的物理地址是該塊大小的整數(shù)倍。
伙伴算法是一種物理內(nèi)存分配和回收的方法,物理內(nèi)存所有空閑頁都記錄在BUDDY鏈表中。系統(tǒng)建立一個鏈表,鏈表中的每個元素代表一類大小的物理內(nèi)存,分別為2的0次方、1次方、2次方...個頁大小,對應(yīng)4K、8K、16K...的內(nèi)存,每一類大小的內(nèi)存又有一個鏈表,表示目前可以分配的物理內(nèi)存。例如現(xiàn)在僅存需要分配8K的物理內(nèi)存,系統(tǒng)首先從8K那個鏈表中查詢有無可分配的內(nèi)存,若有直接分配;否則查找16K大小的鏈表,若有,首先將16K一分為二,將其中一個分配給進程,另一個插入8K的鏈表中,若無,繼續(xù)查找32K,若有,首先把32K一分為二,其中一個16K大小的內(nèi)存插入16K鏈表中,然后另一個16K繼續(xù)一分為二,將其中一個插入8K的鏈表中,另一個分配給進程,以此類推。當內(nèi)存釋放時,查看相鄰內(nèi)存有無空閑,若存在兩個聯(lián)系的8K的空閑內(nèi)存,直接合并成一個16K的內(nèi)存,插入16K鏈表中。
采用伙伴算法分配內(nèi)存時,每次至少分配一個頁面。但當請求分配的內(nèi)存大小為幾十個字節(jié)或幾百個字節(jié)時應(yīng)該如何處理?如何在一個頁面中分配小的內(nèi)存區(qū),小內(nèi)存區(qū)的分配所產(chǎn)生的內(nèi)碎片又如何解決?Linux采用Slab。
5. Slab
slab向buddy“批發(fā)”一些內(nèi)存,加工切塊以后“散賣”出去。
slab分配器主要的功能就是對頻繁分配和釋放的小對象提供高效的內(nèi)存管理。它的核心思想是實現(xiàn)一個緩存池,分配對象的時候從緩存池中取,釋放對象的時候再放入緩存池。slab分配器是基于對象類型進行內(nèi)存管理的,每一種對象被劃分為一類,例如索引節(jié)點對象是一類,進程描述符又是一類,等等。每當需要申請一個特定的對象時,就從相應(yīng)的類中分配一個空白的對象出去;當這個對象被使用完畢時,就重新“插入”到相應(yīng)的類中(其實并不存在插入的動作,僅僅是將該對象重新標記為空閑而已)。
首先要查看inode_cachep的slabs_partial鏈表,如果slabs_partial非空,就從中選中一個slab,返回一個指向已分配但未使用的inode結(jié)構(gòu)的指針。完事之后,如果這個slab滿了,就把它從slabs_partial中刪除,插入到slabs_full中去,結(jié)束;
如果slabs_partial為空,也就是沒有半滿的slab,就會到slabs_empty中尋找。如果slabs_empty非空,就選中一個slab,返回一個指向已分配但未使用的inode結(jié)構(gòu)的指針,然后將這個slab從slabs_empty中刪除,插入到slabs_partial(或者slab_full)中去,結(jié)束;
如果slabs_empty也為空,那么沒辦法,cache內(nèi)存已經(jīng)不足,只能新創(chuàng)建一個slab了。
Slab分配器一直處于內(nèi)核內(nèi)存管理的核心地位,盡管如此,它還是擁有自身的缺點,最明顯的兩點就是復雜性和過多的管理數(shù)據(jù)造成的內(nèi)存上的開銷。針對這些問題,linux引入了slub分配器,
6. Slub
slub分配器保留了slab分配器的所有接口,實際上slub分配器的模型和slab分配的模型是基本一致的,只不過在一些地方進行了精簡,這也使得slub分配器工作起來更為游刃有余。兩者主要的區(qū)別如下:
- slab分配器為了增加分配速度,引入了一些管理數(shù)組,如slab管理區(qū)中的kmem_bufctl數(shù)組和緊隨本地CPU結(jié)構(gòu)后面的用來跟蹤最熱空閑對象的數(shù)組,這些結(jié)構(gòu)雖然加快了分配對象的速度,但也增加了一定的復雜性,而且隨著系統(tǒng)變得龐大,其對內(nèi)存的開銷也越明顯,而slub分配器則完全摒棄了這些管理數(shù)據(jù)。
- slab分配器針對每個緩存,根據(jù)slab的狀態(tài)劃分了3個鏈表 full,partial和free。slub分配器做了簡化,去掉了free鏈表,對于空閑的slab,slub分配器選擇直接將其釋放。
- slub分配器摒棄了slab分配器中的著色概念,在slab分配器中,由于顏色的個數(shù)有限,因此著色也無法完全解決slab之間的緩存行沖突問題,考慮到著色造成了內(nèi)存上的浪費,slub分配器沒有引入著色。
- 在NUMA架構(gòu)的支持上,slub分配器也較slab分配器做了簡化。