1. 程式人生 > >記憶體管理(Linux核心原始碼分析)

記憶體管理(Linux核心原始碼分析)

背景

本篇部落格試圖通過linux核心原始碼分析linux的記憶體管理機制,並且對比核心提供的幾個分配記憶體的介面函式。然後聊下slab層的用法以及介面函式。

核心分配記憶體與使用者態分配記憶體

核心分配記憶體與使用者態分配記憶體顯然是不同的,核心不可以像使用者態那樣奢侈的使用記憶體,核心使用記憶體一定是謹小慎微的。並且,在使用者態如果出現記憶體溢位因為有記憶體保護機制,可能只是一個報錯或警告,而在核心態若出現記憶體溢位後果就會嚴重的多(畢竟再沒有管理者了)。

我們知道處理器處理資料的基本單位是字。而核心把頁作為記憶體管理的基本單位。那麼,頁在記憶體中是如何描述的?
核心用struct page結構體表示系統中的每一個物理頁:
這裡寫圖片描述


flags存放頁的狀態,如該頁是不是髒頁。
_count域表示該頁的使用計數,如果該頁未被使用,就可以在新的分配中使用它。
要注意的是,page結構體描述的是物理頁而非邏輯頁,描述的是記憶體頁的資訊而不是頁中資料。
實際上每個物理頁面都由一個page結構體來描述,有的人可能會驚訝說那這得需要多少記憶體呢?我們可以來算一下,若一個struct page佔用40位元組記憶體,一個頁有8KB,記憶體大小為4G的話,共有524288個頁面,需要剛好20MB的大小來存放結構體。這相對於4G的記憶體根本九牛一毛。

有些頁是有特定用途的。比如記憶體中有些頁是專門用於DMA的。
核心使用區的概念將具有相似特性的頁進行分組。區是一種邏輯上的分組的概念,而沒有物理上的意義。
區的實際使用和分佈是與體系結構相關的。在x86體系結構中主要分為3個區:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA區中的頁用來進行DMA時使用。ZONE_HIGHMEM是高階記憶體,其中的頁不能永久的對映到核心地址空間,也就是說,沒有虛擬地址。剩餘的記憶體就屬於ZONE_NORMAL區。
我們可以看一下描述區的結構體struct zone(在linux/mmzone.h中定義)。
這裡寫圖片描述


這個結構體比較長,我只截取了一部分出來。
實際上不是所有的體系結構都定義了全部區,有些64位的體系結構,比如Intel的x86-64體系結構可以對映和處理64位的記憶體空間,所以其沒有ZONE_HIGHMEM區。而有些體系結構中的所有地址都可用於DMA,所以這些體系結構就沒有ZONE_DMA區。

核心中記憶體分配介面

我們現在已經大體瞭解了核心中的頁與區的概念及描述。接下來我們就可以來看看核心中有哪些記憶體分配與釋放的介面。在核心中,我們正是通過這些介面來分配與釋放記憶體的。首先我們來看看以頁為單位進行分配的介面函式。

獲得頁與釋放頁

獲得頁

獲得頁使用的介面是alloc_pages函式,我們來看下它的原始碼(位於linux/gfp.h中)
這裡寫圖片描述


可以看到,該函式返回值是指向page結構體的指標,引數gfp_mask是一個標誌,簡單來講就是獲得頁所使用的行為方式。order引數規定分配多少頁面,該函式分配2的order次方個連續的物理頁面。返回的指標指向的是第一page頁面。
獲得頁的方式不只一種,我們還可以使用__get_free_pages函式來獲得頁,該函式和alloc_pages的引數一樣,然而它會返回一個虛擬地址。原始碼如下:
這裡寫圖片描述
可以看到,這個函式其實也是呼叫了alloc_pages函式,只不過在獲得了struct page結構體後使用page_address函式獲得了虛擬地址。
另外還有alloc_page函式與__get_free_page函式,都是獲得一個頁,其實就是將前面兩個函式的order分別置為了0而已。這裡不贅述了。

我們在使用這些介面獲取頁的時候可能會面對一個問題,我們獲得的這些頁若是給使用者態用,雖然這些頁中的資料都是隨機產生的垃圾資料,不過,雖然概率很低,但是也有可能會包含某些敏感資訊。所以,更謹慎些,我們可以將獲得的頁都填充為0。這會用到get_zeroed_page函式。看下它的原始碼:
這裡寫圖片描述
這個函式也用到了__get_free_pages函式。只是加了一種叫做__GFP_ZERO的gfp_mask方式。所以,這些獲得頁的函式最終呼叫的都是alloc_pages函式。alloc_pages函式是獲得頁的核心函式。

釋放頁

當我們不再需要某些頁時可以使用下面的函式釋放它們:
__free_pages(struct page *page, unsigned int order)
__free_page
free_pages
free_page(unsigned long addr, unsigned int order)
這些介面都在linux/gfp.h中。
釋放頁的時候一定要小心謹慎,核心中操作不同於在使用者態,若是將地址寫錯,或是order寫錯,那麼都可能會導致系統的崩潰。若是在使用者態進行非法操作,核心作為管理者還會阻止併發出警告,而核心是完全信賴自己的,若是在核心態中有非法操作,那麼核心可能會掛掉的。

kmalloc與vmalloc

前面講的那些介面都是以頁為單位進行記憶體分配與釋放的。而在實際中核心需要的記憶體不一定是整個頁,可能只是以位元組為單位的一片區域。這兩個函式就是實現這樣的目的。不同之處在於,kmalloc分配的是虛擬地址連續,實體地址也連續的一片區域,vmalloc分配的是虛擬地址連續,實體地址不一定連續的一片區域。這裡依然需要特別注意的就是使用釋放記憶體的函式kfree與vfree時一定要注意準確釋放,否則會發生不可預測的嚴重後果。

slab層

分配和釋放資料結構是核心中的基本操作。有些多次會用到的資料結構如果頻繁分配記憶體必然導致效率低下。slab層就是用於解決頻繁分配和釋放資料結構的問題。為便於理解slab層的層次結構,請看下圖
這裡寫圖片描述
簡單的說,實體記憶體中有多個快取記憶體,每個快取記憶體都是一個結構體型別,一個快取記憶體中會有一個或多個slab,slab通常為一頁,其中存放著資料結構型別的例項化物件。
分配快取記憶體的介面是struct kmem_cache kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void ))。
它返回的是kmem_cache結構體。第一個引數是快取的名字,第二個引數是快取記憶體中每個物件的大小,第三個引數是slab內第一個物件的偏移量。剩下的就不細說。
總之,這個介面函式為一個結構體分配了快取記憶體,那麼快取記憶體有了,是不是就要為快取中分配例項化的物件呢?這個介面是
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
引數是kmem_cache結構體,也就是分配好的快取記憶體,flags是標誌位。
抽象的介紹看著不直觀, 我們看個具體的例子。之前我寫過一個關於jbd2日誌系統的部落格,介紹過jbd2的模組初始化過程。其中就提到過jbd2在進行模組初始化的時候是會建立幾個高速緩衝區的。如下:
這裡寫圖片描述
我們看看第一個建立緩衝區的函式。
這裡寫圖片描述
首先是斷言緩衝區一定為空的。然後用kmem_cache_create建立了兩個緩衝區。兩個高速緩衝區就這麼建立好了。看下圖
這裡寫圖片描述
這裡用kmem_cache結構體,也就是jbd2_revoke_record_cache快取記憶體例項化了一個物件。

總結

記憶體管理的linux核心原始碼我只分析了一小部分,主要是總結了一下核心分配與回收記憶體的介面函式及其用法。