內存管理(Linux內核源碼分析)

背景

本篇博客試圖通過linux內核源碼分析linux的內存管理機制,並且對比內核提供的幾個分配內存的接口函數。然後聊下slab層的用法以及接口函數。

內核分配內存與用戶態分配內存

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

我們知道處理器處理數據的基本單位是字。而內核把也作為內存管理的基本單位。那麼,頁在內存中是如何描述的?內核用struct page結構體表示系統中的每一個物理頁:

內存管理(Linux內核源碼分析)

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中定義)。

內存管理(Linux內核源碼分析)

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

內核中內存分配接口

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

獲得頁與釋放頁

獲得頁

獲得頁使用的接口是alloc_pages函數,我們來看下它的源碼(位於linux/gfp.h中)

內存管理(Linux內核源碼分析)

可以看到,該函數返回值是指向page結構體的指針,參數gfp_mask是一個標誌,簡單來講就是獲得頁所使用的行為方式。order參數規定分配多少頁面,該函數分配2的order次方個連續的物理頁面。返回的指針指向的是第一page頁面。獲得頁的方式不止一種,我們還可以使用__get_free_pages函數來獲得頁,該函數和alloc_pages的參數一樣,然而它會返回一個虛擬地址。源碼如下:

內存管理(Linux內核源碼分析)

可以看到,這個函數其實也是調用了alloc_pages函數,只不過在獲得了struct page結構體後使用page_address函數獲得了虛擬地址。另外還有alloc_page函數與__get_free_page函數,都是獲得一個頁,其實就是將前面兩個函數的order分別置為了0而已。這裡不贅述了。

我們在使用這些接口獲取頁的時候可能會面對一個問題,我們獲得的這些頁若是給用戶使用,雖然這些頁中的數據都是隨機產生的垃圾數據,不過,雖然概率很低,但是也有可能會包含某些敏感信息。所以,更謹慎些,我們可以將獲得的頁都填充為0。這會用到get_zeroed_page函數。看下它的源碼:

內存管理(Linux內核源碼分析)

這個函數也用到了__get_free_pages函數。只是加了一種叫做__GFP_ZERO的gfp_mask方式。所以,這些獲得頁的函數最終調用的都是alloc_pages函數。alloc_pages函數是獲得頁的核心函數。

釋放頁

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

kmalloc與vmalloc

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

slab層

分配和釋放數據結構是內核中的基本操作。有些多次會用到的數據結構如果頻繁分配內存必然導致效率低下。slab層就是用於解決頻繁分配和釋放數據結構的問題。為便於理解slab層的層次結構,請看下圖

內存管理(Linux內核源碼分析)

簡單的說,物理內存中有多個高速緩存,每個高速緩存都是一個結構體類型,一個高速緩存中會有一個或多個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在進行模塊初始化的時候是會創建幾個高速緩衝區的。如下:

內存管理(Linux內核源碼分析)

我們看看第一個創建緩衝區的函數。

內存管理(Linux內核源碼分析)

首先是斷言緩衝區一定為空的。然後用kmem_cache_create創建了兩個緩衝區。兩個高速緩衝區就這麼創建好了。看下圖

內存管理(Linux內核源碼分析)

這裡用kmem_cache結構體,也就是jbd2_revoke_record_cache高速緩存實例化了一個對象。

總結

內存管理的linux內核源碼我只分析了一小部分,主要是總結了一下內核分配與回收內存的接口函數及其用法。

c/c++Linux服務器開發高階知識點視頻學習資料後臺私信【架構】獲取

內存管理(Linux內核源碼分析)


分享到:


相關文章: