1. 用戶空間
通常 32 位 Linux 虛擬地址空間劃分, 0-3GB為用戶空間,3GB-4GB為內(nèi)核空間脂新。每個進(jìn)程都有4GB的虛擬地址空間挪捕,其中0-3GB是自己私有的用戶空間,最高的1GB是與所有進(jìn)程共享的內(nèi)核空間争便。Linux 進(jìn)程的內(nèi)存布局如下圖所示:
進(jìn)程地址空間主要分為以下幾部分:
代碼段(Text): 程序代碼在內(nèi)存中的映射级零,存放函數(shù)體的二進(jìn)制代碼。
數(shù)據(jù)段(Data): 在程序運(yùn)行初已經(jīng)對變量進(jìn)行初始化的數(shù)據(jù)滞乙。
BSS段(BSS): 在程序運(yùn)行初未對變量進(jìn)行初始化的數(shù)據(jù)奏纪。
棧(Stack): 存儲局部,臨時(shí)變量斩启,函數(shù)調(diào)用時(shí)存儲函數(shù)的返回指針亥贸,用于控制函數(shù)的調(diào)用和返回。在程序塊開始時(shí)自動分配內(nèi)存浇垦,結(jié)束時(shí)自動釋放內(nèi)存炕置,其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧荣挨。
堆(Heap): 存儲動態(tài)內(nèi)存分配,需要程序員手工分配朴摊,手工釋放默垄。
注意,以上所說的地址均為虛擬地址甚纲。進(jìn)程虛擬地址到物理地址的轉(zhuǎn)換會在下面"段頁式存儲管理"部分詳細(xì)講解口锭。
2. 內(nèi)核空間
在Linux中,內(nèi)核空間是持續(xù)存在的介杆,并且在所有進(jìn)程中都映射到同樣的物理內(nèi)存鹃操,內(nèi)核代碼和數(shù)據(jù)總是可尋址的,隨時(shí)準(zhǔn)備處理中斷和系統(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)存到涂。若機(jī)器中安裝4G物理內(nèi)存脊框,那么內(nèi)核就只能訪問前1G物理內(nèi)存,后面3G物理內(nèi)存將會無法訪問养盗。為了解決這個問題缚陷,Linux引入了高端內(nèi)存的概念。
高端內(nèi)存的基本思想:借一段地址空間往核,建立臨時(shí)地址映射箫爷,用完后釋放。達(dá)到這段地址空間可以循環(huán)使用聂儒,訪問所有物理內(nèi)存的目的虎锚。
Linux系統(tǒng)在初始化時(shí),會根據(jù)實(shí)際的物理內(nèi)存的大小衩婚,為每個物理頁面創(chuàng)建一個page對象窜护,所有的page對象構(gòu)成一個mem_map數(shù)組。進(jìn)而針對不同的用途非春,Linux內(nèi)核將所有的物理頁面劃分到3類內(nèi)存管理區(qū)中柱徙,如圖缓屠,分別為ZONE_DMA,ZONE_NORMAL护侮,ZONE_HIGHMEM敌完。
ZONE_DMA的范圍是0-16MB,該區(qū)域的物理頁面專門供I/O設(shè)備的DMA使用羊初。之所以需要單獨(dú)管理DMA的物理頁面滨溉,是因?yàn)镈MA使用物理地址訪問內(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í)行時(shí)乐设,傳遞給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個頁表項(xiàng),這還只是一個進(jìn)程危号,因?yàn)槊總€進(jìn)程都有自己獨(dú)立的頁表个扰,這樣系統(tǒng)所有的內(nèi)存都來存放頁表項(xiàng)恐怕都不夠。想象一下進(jìn)程的虛擬地址空間葱色,實(shí)際上大部分是空閑的递宅,真正映射的區(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一分為二揪阿,將其中一個分配給進(jìn)程,另一個插入8K的鏈表中咆畏,若無南捂,繼續(xù)查找32K,若有旧找,首先把32K一分為二溺健,其中一個16K大小的內(nèi)存插入16K鏈表中,然后另一個16K繼續(xù)一分為二钮蛛,將其中一個插入8K的鏈表中鞭缭,另一個分配給進(jìn)程,以此類推魏颓。當(dāng)內(nèi)存釋放時(shí)岭辣,查看相鄰內(nèi)存有無空閑,若存在兩個聯(lián)系的8K的空閑內(nèi)存琼开,直接合并成一個16K的內(nèi)存易结,插入16K鏈表中枕荞。
采用伙伴算法分配內(nèi)存時(shí)柜候,每次至少分配一個頁面。但當(dāng)請求分配的內(nèi)存大小為幾十個字節(jié)或幾百個字節(jié)時(shí)應(yīng)該如何處理躏精?如何在一個頁面中分配小的內(nèi)存區(qū)渣刷,小內(nèi)存區(qū)的分配所產(chǎn)生的內(nèi)碎片又如何解決?Linux采用Slab矗烛。
5. Slab
slab向buddy“批發(fā)”一些內(nèi)存辅柴,加工切塊以后“散賣”出去。
slab分配器主要的功能就是對頻繁分配和釋放的小對象提供高效的內(nèi)存管理瞭吃。它的核心思想是實(shí)現(xiàn)一個緩存池碌嘀,分配對象的時(shí)候從緩存池中取,釋放對象的時(shí)候再放入緩存池歪架。slab分配器是基于對象類型進(jìn)行內(nèi)存管理的股冗,每一種對象被劃分為一類,例如索引節(jié)點(diǎn)對象是一類和蚪,進(jìn)程描述符又是一類止状,等等烹棉。每當(dāng)需要申請一個特定的對象時(shí),就從相應(yīng)的類中分配一個空白的對象出去怯疤;當(dāng)這個對象被使用完畢時(shí)浆洗,就重新“插入”到相應(yīng)的類中(其實(shí)并不存在插入的動作,僅僅是將該對象重新標(biāo)記為空閑而已)集峦。
首先要查看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)存管理的核心地位,盡管如此派歌,它還是擁有自身的缺點(diǎn)弯囊,最明顯的兩點(diǎn)就是復(fù)雜性和過多的管理數(shù)據(jù)造成的內(nèi)存上的開銷。針對這些問題胶果,linux引入了slub分配器匾嘱,
6. Slub
slub分配器保留了slab分配器的所有接口,實(shí)際上slub分配器的模型和slab分配的模型是基本一致的早抠,只不過在一些地方進(jìn)行了精簡霎烙,這也使得slub分配器工作起來更為游刃有余。兩者主要的區(qū)別如下:
- slab分配器為了增加分配速度,引入了一些管理數(shù)組吼过,如slab管理區(qū)中的kmem_bufctl數(shù)組和緊隨本地CPU結(jié)構(gòu)后面的用來跟蹤最熱空閑對象的數(shù)組锐秦,這些結(jié)構(gòu)雖然加快了分配對象的速度,但也增加了一定的復(fù)雜性盗忱,而且隨著系統(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)存上的浪費(fèi),slub分配器沒有引入著色租幕。
- 在NUMA架構(gòu)的支持上舷手,slub分配器也較slab分配器做了簡化。