內(nèi)存管理(Linux內(nèi)核源碼分析)
背景
本文試圖通過(guò)linux內(nèi)核源碼分析linux的內(nèi)存管理機(jī)制,并且對(duì)比內(nèi)核提供的幾個(gè)分配內(nèi)存的接口函數(shù)。然后聊下slab層的用法以及接口函數(shù)。
內(nèi)核分配內(nèi)存與用戶(hù)態(tài)分配內(nèi)存
內(nèi)核分配內(nèi)存與用戶(hù)態(tài)分配內(nèi)存顯然是不同的,內(nèi)核不可以像用戶(hù)態(tài)那樣奢侈的使用內(nèi)存,內(nèi)核使用內(nèi)存一定是謹(jǐn)小慎微的。并且,在用戶(hù)態(tài)如果出現(xiàn)內(nèi)存溢出因?yàn)橛袃?nèi)存保護(hù)機(jī)制,可能只是一個(gè)報(bào)錯(cuò)或警告,而在內(nèi)核態(tài)若出現(xiàn)內(nèi)存溢出后果就會(huì)嚴(yán)重的多(畢竟再?zèng)]有管理者了)。
頁(yè)
我們知道處理器處理數(shù)據(jù)的基本單位是字。而內(nèi)核把頁(yè)作為內(nèi)存管理的基本單位。那么,頁(yè)在內(nèi)存中是如何描述的?
內(nèi)核用struct page結(jié)構(gòu)體表示系統(tǒng)中的每一個(gè)物理頁(yè):

flags存放頁(yè)的狀態(tài),如該頁(yè)是不是臟頁(yè)。
_count域表示該頁(yè)的使用計(jì)數(shù),如果該頁(yè)未被使用,就可以在新的分配中使用它。
要注意的是,page結(jié)構(gòu)體描述的是物理頁(yè)而非邏輯頁(yè),描述的是內(nèi)存頁(yè)的信息而不是頁(yè)中數(shù)據(jù)。
實(shí)際上每個(gè)物理頁(yè)面都由一個(gè)page結(jié)構(gòu)體來(lái)描述,有的人可能會(huì)驚訝說(shuō)那這得需要多少內(nèi)存呢?我們可以來(lái)算一下,若一個(gè)struct page占用40字節(jié)內(nèi)存,一個(gè)頁(yè)有8KB,內(nèi)存大小為4G的話(huà),共有524288個(gè)頁(yè)面,需要?jiǎng)偤?0MB的大小來(lái)存放結(jié)構(gòu)體。這相對(duì)于4G的內(nèi)存根本九牛一毛。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。∏?00名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ?


區(qū)
有些頁(yè)是有特定用途的。比如內(nèi)存中有些頁(yè)是專(zhuān)門(mén)用于DMA的。
內(nèi)核使用區(qū)的概念將具有相似特性的頁(yè)進(jìn)行分組。區(qū)是一種邏輯上的分組的概念,而沒(méi)有物理上的意義。
區(qū)的實(shí)際使用和分布是與體系結(jié)構(gòu)相關(guān)的。在x86體系結(jié)構(gòu)中主要分為3個(gè)區(qū):ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA區(qū)中的頁(yè)用來(lái)進(jìn)行DMA時(shí)使用。ZONE_HIGHMEM是高端內(nèi)存,其中的頁(yè)不能永久的映射到內(nèi)核地址空間,也就是說(shuō),沒(méi)有虛擬地址。剩余的內(nèi)存就屬于ZONE_NORMAL區(qū)。
我們可以看一下描述區(qū)的結(jié)構(gòu)體struct zone(在linux/mmzone.h中定義)。

這個(gè)結(jié)構(gòu)體比較長(zhǎng),我只截取了一部分出來(lái)。
實(shí)際上不是所有的體系結(jié)構(gòu)都定義了全部區(qū),有些64位的體系結(jié)構(gòu),比如Intel的x86-64體系結(jié)構(gòu)可以映射和處理64位的內(nèi)存空間,所以其沒(méi)有ZONE_HIGHMEM區(qū)。而有些體系結(jié)構(gòu)中的所有地址都可用于DMA,所以這些體系結(jié)構(gòu)就沒(méi)有ZONE_DMA區(qū)。
內(nèi)核中內(nèi)存分配接口
我們現(xiàn)在已經(jīng)大體了解了內(nèi)核中的頁(yè)與區(qū)的概念及描述。接下來(lái)我們就可以來(lái)看看內(nèi)核中有哪些內(nèi)存分配與釋放的接口。在內(nèi)核中,我們正是通過(guò)這些接口來(lái)分配與釋放內(nèi)存的。首先我們來(lái)看看以頁(yè)為單位進(jìn)行分配的接口函數(shù)。
獲得頁(yè)與釋放頁(yè)
獲得頁(yè)
獲得頁(yè)使用的接口是alloc_pages函數(shù),我們來(lái)看下它的源碼(位于linux/gfp.h中)

可以看到,該函數(shù)返回值是指向page結(jié)構(gòu)體的指針,參數(shù)gfp_mask是一個(gè)標(biāo)志,簡(jiǎn)單來(lái)講就是獲得頁(yè)所使用的行為方式。order參數(shù)規(guī)定分配多少頁(yè)面,該函數(shù)分配2的order次方個(gè)連續(xù)的物理頁(yè)面。返回的指針指向的是第一page頁(yè)面。
獲得頁(yè)的方式不只一種,我們還可以使用__get_free_pages函數(shù)來(lái)獲得頁(yè),該函數(shù)和alloc_pages的參數(shù)一樣,然而它會(huì)返回一個(gè)虛擬地址。源碼如下:

可以看到,這個(gè)函數(shù)其實(shí)也是調(diào)用了alloc_pages函數(shù),只不過(guò)在獲得了struct page結(jié)構(gòu)體后使用page_address函數(shù)獲得了虛擬地址。
另外還有alloc_page函數(shù)與__get_free_page函數(shù),都是獲得一個(gè)頁(yè),其實(shí)就是將前面兩個(gè)函數(shù)的order分別置為了0而已。這里不贅述了。
我們?cè)谑褂眠@些接口獲取頁(yè)的時(shí)候可能會(huì)面對(duì)一個(gè)問(wèn)題,我們獲得的這些頁(yè)若是給用戶(hù)態(tài)用,雖然這些頁(yè)中的數(shù)據(jù)都是隨機(jī)產(chǎn)生的垃圾數(shù)據(jù),不過(guò),雖然概率很低,但是也有可能會(huì)包含某些敏感信息。所以,更謹(jǐn)慎些,我們可以將獲得的頁(yè)都填充為0。這會(huì)用到get_zeroed_page函數(shù)??聪滤脑创a:

這個(gè)函數(shù)也用到了__get_free_pages函數(shù)。只是加了一種叫做__GFP_ZERO的gfp_mask方式。所以,這些獲得頁(yè)的函數(shù)最終調(diào)用的都是alloc_pages函數(shù)。alloc_pages函數(shù)是獲得頁(yè)的核心函數(shù)。
釋放頁(yè)
當(dāng)我們不再需要某些頁(yè)時(shí)可以使用下面的函數(shù)釋放它們:
__free_pages(struct page *page, unsigned int order) __free_page free_pages free_page(unsigned long addr, unsigned int order)
這些接口都在linux/gfp.h中。
釋放頁(yè)的時(shí)候一定要小心謹(jǐn)慎,內(nèi)核中操作不同于在用戶(hù)態(tài),若是將地址寫(xiě)錯(cuò),或是order寫(xiě)錯(cuò),那么都可能會(huì)導(dǎo)致系統(tǒng)的崩潰。若是在用戶(hù)態(tài)進(jìn)行非法操作,內(nèi)核作為管理者還會(huì)阻止并發(fā)出警告,而內(nèi)核是完全信賴(lài)自己的,若是在內(nèi)核態(tài)中有非法操作,那么內(nèi)核可能會(huì)掛掉的。
kmalloc與vmalloc
前面講的那些接口都是以頁(yè)為單位進(jìn)行內(nèi)存分配與釋放的。而在實(shí)際中內(nèi)核需要的內(nèi)存不一定是整個(gè)頁(yè),可能只是以字節(jié)為單位的一片區(qū)域。這兩個(gè)函數(shù)就是實(shí)現(xiàn)這樣的目的。不同之處在于,kmalloc分配的是虛擬地址連續(xù),物理地址也連續(xù)的一片區(qū)域,vmalloc分配的是虛擬地址連續(xù),物理地址不一定連續(xù)的一片區(qū)域。這里依然需要特別注意的就是使用釋放內(nèi)存的函數(shù)kfree與vfree時(shí)一定要注意準(zhǔn)確釋放,否則會(huì)發(fā)生不可預(yù)測(cè)的嚴(yán)重后果。
slab層
分配和釋放數(shù)據(jù)結(jié)構(gòu)是內(nèi)核中的基本操作。有些多次會(huì)用到的數(shù)據(jù)結(jié)構(gòu)如果頻繁分配內(nèi)存必然導(dǎo)致效率低下。slab層就是用于解決頻繁分配和釋放數(shù)據(jù)結(jié)構(gòu)的問(wèn)題。為便于理解slab層的層次結(jié)構(gòu),請(qǐng)看下圖

簡(jiǎn)單的說(shuō),物理內(nèi)存中有多個(gè)高速緩存,每個(gè)高速緩存都是一個(gè)結(jié)構(gòu)體類(lèi)型,一個(gè)高速緩存中會(huì)有一個(gè)或多個(gè)slab,slab通常為一頁(yè),其中存放著數(shù)據(jù)結(jié)構(gòu)類(lèi)型的實(shí)例化對(duì)象。
分配高速緩存的接口是struct kmem_cache kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void ))。
它返回的是kmem_cache結(jié)構(gòu)體。第一個(gè)參數(shù)是緩存的名字,第二個(gè)參數(shù)是高速緩存中每個(gè)對(duì)象的大小,第三個(gè)參數(shù)是slab內(nèi)第一個(gè)對(duì)象的偏移量。剩下的就不細(xì)說(shuō)。
總之,這個(gè)接口函數(shù)為一個(gè)結(jié)構(gòu)體分配了高速緩存,那么高速緩存有了,是不是就要為緩存中分配實(shí)例化的對(duì)象呢?這個(gè)接口是
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
參數(shù)是kmem_cache結(jié)構(gòu)體,也就是分配好的高速緩存,flags是標(biāo)志位。
抽象的介紹看著不直觀, 我們看個(gè)具體的例子。之前我寫(xiě)過(guò)一個(gè)關(guān)于jbd2日志系統(tǒng)的博客,介紹過(guò)jbd2的模塊初始化過(guò)程。其中就提到過(guò)jbd2在進(jìn)行模塊初始化的時(shí)候是會(huì)創(chuàng)建幾個(gè)高速緩沖區(qū)的。如下:

我們看看第一個(gè)創(chuàng)建緩沖區(qū)的函數(shù)。

首先是斷言緩沖區(qū)一定為空的。然后用kmem_cache_create創(chuàng)建了兩個(gè)緩沖區(qū)。兩個(gè)高速緩沖區(qū)就這么創(chuàng)建好了??聪聢D

這里用kmem_cache結(jié)構(gòu)體,也就是jbd2_revoke_record_cache高速緩存實(shí)例化了一個(gè)對(duì)象。
總結(jié)
內(nèi)存管理的linux內(nèi)核源碼我只分析了一小部分,主要是總結(jié)了一下內(nèi)核分配與回收內(nèi)存的接口函數(shù)及其用法。
