linux內核分析-內存管理

轉載請註明出處:http://blog.csdn.net/zhijianjingling00/article/details/9335333

在內核模塊中申請分配內存需要使用內核中的專用API:kmalloc、vmalloc、kzalloc、kcalloc、get_free_pages;當然,設備驅動程序也不例外;
對於提供了MMU功能的處理器而言,Linux提供了複雜的內存管理系統,使得進程所能訪問到的地址空間可以達到4GB;而這4GB的空間又被劃分爲兩個部分:0GB~3GB(PAGE_OFFSET,x86中的值是0xC0000000)的區域被用作進程的用戶空間,3GB~4GB的區域被用作內核空間;
在內核空間中,從3GB到vmalloc_start之間的這段地址區域作爲物理內存映射區使用,該段映射區域內包含了內核鏡像、物理頁框表mem_map等等,比如,我們使用的系統物理內存爲160MB,那麼,3GB~3GB+vmalloc_start之間的區域就應該是映射的物理內存;在物理內存映射區域之後,就是虛擬內存vmalloc區域;對於160MB的系統而言,vmalloc_start的位置就應該在3GB+160MB位置附近(在物理內存映射區與vmalloc_start位置之間還存在一個8M的gap來防止越界),vmalloc_end的位置接近4GB的位置(系統會在最後的位置處保留一片128KB大小的區域專用於頁面映射);
一、kmalloc
#include <linux/slab.h>
static inline void *kmalloc(size_t size, gfp_t flags);
參數:size:指定要分配的塊的大小,單位是字節;flags:指定分配內存時的控制方式;
該函數用於在內核空間中分配內存使用,它的返回速度快(除非被阻塞),並且對其分配的內存不進行任何初始化(清零)操作,分配的內存區域仍然保留有他原有的內容;
kmalloc申請得到的是物理內存,位於物理內存映射區,而且在物理地址上是連續的;但是kmalloc返回的內存地址卻是虛擬地址(線性地址),返回的這個虛擬地址(線性地址)與真實的物理地址之間僅僅相差一個固定的偏移值;因此,kmalloc申請得到的物理內存塊的首地址與其返回的虛擬地址之間存在着比較簡單的轉換關係;通過內核提供的函數virt_to_phys()可以實現該虛擬地址到真實的內核物理地址之間的轉換:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
static inline unsigned long virt_to_phys(volatile void* address)
{
 return __pa(address);
}
參數address是kmalloc返回的一個虛擬地址;該轉換過程就是虛擬地址減去3GB(PAGE_OFFSET=0xC0000000);
一般情況下,PAGE_OFFSET=3*1024*1024*1024=0xC0000000(3G);
與之對應的函數就是phys_to_virt()用於把內核物理地址轉換爲虛擬地址:
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
static inline void * phys_to_virt(unsigned long address)
{
 return __va(address);
}
這兩個函數都定義在include/asm-i386/io.h中;
kmalloc()函數用於小塊內存的申請,最小可以申請的內存是32字節或64字節,最大可以申請的內存是128KB-16,其中,被減掉的16個字節用於存儲頁描述符結構;這些都依賴於體系架構所使用的頁面大小;kmalloc申請的內存在物理地址上是連續的,這對於要進行DMA傳輸的設備來說,是非常重要的;
kmalloc()的內存分配是基於slab機制實現的,slab機制是爲分配小內存而提供的一種高效的機制;但是slab機制也不是獨立的,它本身也是在頁分配器的基礎上來劃分更細粒度的內存供調用者使用;也就是說,系統先使用頁分配器分配以頁爲最小單位的連續物理地址,然後,kmalloc()再在這個基礎上根據調用者的需要進行切分的;另外,slab機制分配的內存在物理地址和虛擬地址(線性地址/邏輯地址)上都是連續的;
對於kmalloc()申請的內存,需要使用kfree()函數來釋放;
備註:kmalloc是基於slab機制實現的;
二、get_free_pages
#include <asm/pages.h>
fastcall unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
 struct page * page;
 page = alloc_pages(gfp_mask, order);
 if (!page)
  return 0;
 return (unsigned long) page_address(page);
}
參數gfp_mask用於指定申請內存時的控制方式,order用於指定申請的頁數;它申請的內存位於(PAGE_OFFSET,HIGH_MEMORY)之間;
__get_free_pages()函數是頁面分配器提供給調用者的最底層的內存分配函數,它申請的內存也是連續的物理內存,同樣位於物理內存映射區;它是基於buddy機制實現的;在使用buddy機制實現的物理內存管理系統中,最小的分配粒度(單位)也是以頁爲單位的;在__get_free_pages()內部通過調用alloc_pages()來分配物理內存頁;
__get_free_page()函數分配的是連續的物理內存,處理的是連續的物理地址,但是返回的也是虛擬地址(線性地址);如果想要得到正確的物理地址,也需要使用virt_to_phys()可進行轉換;
對於__get_free_pages()函數申請的內存,需要使用__free_pages()函數來釋放;
備註:__get_free_pages是基於buddy機制實現的;
三、vmalloc
#include <linux/vmalloc.h>
void* vmalloc(unsigned long size)
{
 return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
void* __vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot)
{
 return __vmalloc_node(size, gfp_mask, prot, -1);
}
void* __vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot, int node)
{
 struct vm_struct *area;
 size = PAGE_ALIGN(size);
 if(!size || (size >> PAGE_SHIFT) > num_physpages)
   return NULL;
 area = get_vm_area_node(size, VM_ALLOC, node);
 if(!area)
   return NULL;
 return __vmalloc_area_node(area, gfp_mask, prot, node);
}
void* __vmalloc_area_node(struct vm_struct* area, gfp_t gfp_mask, pgprot_t prot, int node);
void* __vmalloc_area(struct vm_struct* area, gfp_t gfp_mask, pgprot_t prot)
{
 return __vmalloc_area_node(area, gfp_mask, prot, -1);
}
vmalloc()函數也是用於申請內存的,但是它申請的內存是位於vmalloc_start到vmalloc_end之間的虛擬內存;它申請的內存在虛擬地址(線性地址/邏輯地址)上是連續的,但是並不要求在物理地址上連續,並且返回的地址與物理地址之間沒有簡單的轉換關係;
vmalloc()函數適用於大塊內存的申請環境中;但是它申請的內存不能直接用於DMA傳輸;因爲DMA傳輸需要使用物理地址連續的內存塊;
對於vmalloc()申請的內存,需要使用vfree()函數來釋放;
備註:vmalloc是基於slab機制實現的;
四、比較
1).kmalloc/__get_free_pages申請的內存塊都在物理內存映射區,即在(PAGE_OFFSET,HIGH_MEMORY)之間,處理的都是物理地址,且保證在物理地址空間上是連續的;二者返回的都是虛擬地址,如果需要得到正確的物理地址,需要使用virt_to_phys()進行轉換;但是,kmalloc和vmalloc都是以字節爲單位進行申請,而__get_free_pages()則是以頁爲單位進行申請;
2).vmalloc函數申請的內存塊位於虛擬內存映射區,即在(VMALLOC_START,VMALLOC_END)之間,處理的都是虛擬內存,且保證在虛擬地址空間上是連續的,但是在物理地址空間上不要求連續;一般作爲交換區、模塊的內存使用;
3).kmalloc和vmalloc都是基於slab機制實現的,但是kmalloc的速度比vmalloc的速度快;__get_free_pages是基於buddy機制實現的,速度也較快;
4).kmalloc用於小塊內存的申請,通常,一次所能申請的內存塊的大小在(32/64字節,128KB-16)之間;而vmalloc可以用於分配大塊內存的場合;
5).kmalloc申請的內存塊在物理地址空間上是連續的,所以它申請的內存塊可以直接用於DMA傳輸;vmalloc申請的內存塊在虛擬地址空間上連續,但是在物理地址空間上不要求連續,所以它申請的內存塊不能直接用於DMA傳輸;
6).kmalloc申請的內存塊用kfree釋放;vmalloc申請的內存塊用vfree釋放;__get_free_pages申請的內存頁用__free_pages釋放;
7).kmalloc申請得到的地址稱爲內核邏輯地址,vmalloc申請得到的地址稱爲內核虛擬地址;
五、其它函數
1).static inline void *kzalloc(size_t size, gfp_t flags);
 該函數比kmalloc多了一個功能,就是會把申請得到的內存塊初始化爲0;
2).static inline void* kcalloc(size_t n, size_t size, gfp_t flags)
   {
     if(n != 0 && size > ULONG_MAX / n)
        return NULL;
     return kzalloc(n * size, flags);
   }
   該函數用於申請一個數組的內存空間,並把申請得到的內存都初始化爲0;
六、GFP標記
kmalloc、kzalloc、kcalloc、vmalloc、get_free_pages函數在調用時都有一個gfp_t類型的控制標記flags;這個標記用於控制申請內存時的內存分配控制方式; #include <linux/gfp.h>
GFP的標記有兩種:帶雙下劃線前綴的和不帶雙下劃線前綴的;
不帶雙下劃線前綴的GFP標誌:
GFP_ATOMIC:用於在中斷上下文和進程上下文之外的其它代碼中分配內存;從不睡眠;
GFP_KERNEL:內核正常分配內存;可能睡眠;
GFP_USER  :用於爲用戶空間頁分配內存;可能睡眠;
GFP_HIGHUSER:如同GFP_USER,但它是從高端內存中申請;
GFP_NOIO和GFP_NOFS:功能如同GFP_KERNEL,但是它倆增加限制到內核能做的來滿足請求;GFP_NOFS分配不允許進行任何文件系統調用,而GFP_NOIO分配根本不允許進行任何IO初始化;它倆主要用於文件系統和虛擬內存代碼,那裏允許一個分配睡眠,但是遞歸的文件系統調用會是個壞主意;
帶有雙下劃線前綴的GFP標誌:
__GFP_DMA:這個標誌要求分配的內存在能夠進行DMA的內存區;平臺依賴的;
__GFP_HIGHMEM:這個標誌指示分配的內存可以位於高端內存區;平臺依賴的;
__GFP_COLD:正常地,內存分配器盡力返回"緩衝熱"的頁---可能在處理器緩衝中找到的頁;相反,這個標誌請求一個"冷"頁---在一段時間內沒被使用的頁;它對分配頁做DMA讀是很有用的,此時在處理器緩衝中出現是沒用的;
__GFP_NOWARN:這個標誌用於分配內存時阻止內核發出警告,當一個分配請求無法滿足時;
__GFP_HIGH:這個標誌標識了一個高優先級請求,它被允許來消耗甚至被內核保留給緊急狀況的最後的內存頁;
__GFP_REPEAT:分配器的動作;當分配器有困難滿足一個分配請求時,通過重複嘗試的方式來"盡力嘗試",但是分配操作仍然有可能失敗;
__GFP_NOFAIL:分配器的動作;當分配器有困難滿足一個分配請求時,這個標誌告訴分配器不要失敗,盡最大努力來滿足分配請求;
__GFP_NORETRY:分配器的動作;當分配器有困難滿足一個分配請求時,這個標誌告訴分配器立即放棄,不再做任何嘗試;
通常,一個或多個帶雙下劃線前綴的標記相或,即可得到對應的不帶雙下劃線前綴的標記;
最常用的標記就是GFP_KERNEL,它的意思就是當前的這個分配代表運行在內核空間的進程而進行的;換句話說,這意味着調用函數是代表一個進程在執行一個系統調用;使用GFP_KERNEL標記,就意味着kmalloc能夠使當前進程在少內存的情況下通過睡眠來等待一個內存頁;因此,一個使用GFP_KERNEL的函數必須是可重入的,且不能在原子上下文中運行;噹噹前進程睡眠,內核採取正確的動作來定位一些空閒的內存頁,或者通過刷新緩存到磁盤或者交換出去一個用戶進程的內存頁;
如果一個內存分配動作發生在中斷處理或內核定時器的上下文中時,當前進程就不能被設置爲睡眠,也就不能再使用GFP_KERNEL標誌了,此時應該使用GFP_ATOMIC標誌來代替;正常地,內核試圖保持一些空閒頁以便來滿足原子的分配;當使用GFP_ATOMIC標誌時,kmalloc標誌能夠使用甚至最後一個空閒頁;如果這最後一個空閒頁不存在,那分配就會失敗


linux驅動程序一般工作在內核空間,但也可以工作在用戶空間。下面我們將詳細解析,什麼是內核空間,什麼是用戶空間,以及如何判斷他們。
    
    Linux簡化了分段機制,使得虛擬地址與線性地址總是一致,因此,Linux的虛擬地址空間也爲0~4G.Linux內核將這4G字節的空間分爲兩部分。將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲"內核空間".而將較低的3G字節(從虛擬地址 0x00000000到0xBFFFFFFF),供各個進程使用,稱爲"用戶空間)。因爲每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬空間。
    
    Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用。從圖中可以看出(這裏無法表示圖),每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的。最高的1GB字節虛擬內核空間則爲所有進程以及內核所共享。
    
    內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。
    
    雖然內核空間佔據了每個虛擬空間中的最高1GB字節,但映射到物理內存卻總是從最低地址(0x00000000)開始。對內核空間來說,其地址映射是很簡單的線性映射,0xC0000000就是物理地址與線性地址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET.
    
    內核空間和用戶空間之間如何進行通訊?
    
    內核空間和用戶空間一般通過系統調用進行通信。
    
    如何判斷一個驅動是用戶模式驅動還是內核模式驅動?  判斷的標準是什麼?
    
    用戶空間模式的驅動一般通過系統調用來完成對硬件的訪問,如通過系統調用將驅動的io空間映射到用戶空間等。因此,主要的判斷依據就是系統調用。
    
    內核空間和用戶空間上不同太多了,說不完,比如用戶態的鏈表和內核鏈表不一樣;用戶態用printf,內核態用printk;用戶態每個應用程序空間是虛擬的,相對獨立的,內核態中卻不是獨立的,所以編程要非常小心。等等。
    
    還有用戶態和內核態程序通訊的方法很多,不單單是系統調用,實際上系統調用是個不好的選擇,因爲需要系統調用號,這個需要統一分配。
    
    可以通過ioctl、sysfs、proc等來完成。 

內存管理,不用多說,言簡意賅。在內核裏分配內存還真不是件容易的事情,從根本上說,是因爲內核不能像用戶空間那樣奢侈的使用內存。

     先來說說內存管理。內核把物理頁作爲內存管理的基本單位。儘管處理器的最小可尋址單位通常是字,但是,內存管理單元MMU通常以頁爲單位進行處理。因此,從虛擬內存的交代來看,頁就是最小單位。內核用struct page(linux/mm.h)結構表示系統中的每個物理頁:

01 structpage {
02          unsignedlongflags;                                                      
03          atomic_tcount;                
04          unsignedintmapcount;          
05          unsignedlongprivate;          
06          structaddress_space*mapping;  
07          pgoff_tindex;                  
08          structlist_headlru;  
09      union{
10         structpte_chain;
11         pte_addr_t;
12      }         
13          void*virtual;                  
14 };

     flag用來存放頁的狀態,每一位代表一種狀態,所以至少可以同時表示出32中不同的狀態,這些狀態定義在linux/page-flags.h中。count記錄了該頁被引用了多少次。mapping指向與該頁相關的address_space對象。virtual是頁的虛擬地址,它就是頁在虛擬內存中的地址。要理解的一點是page結構與物理頁相關,而並非與虛擬頁相關。因此,該結構對頁的描述是短暫的。內核僅僅用這個結構來描述當前時刻在相關的物理頁中存放的東西。這種數據結構的目的在於描述物理內存本身,而不是描述包含在其中的數據。

     在linux中,內核也不是對所有的也都一視同仁,內核而是把頁分爲不同的區,使用區來對具有相似特性的頁進行分組。Linux必須處理如下兩種硬件存在缺陷而引起的內存尋址問題:

1.一些硬件只能用某些特定的內存地址來執行DMA
2.一些體系結構其內存的物理尋址範圍比虛擬尋址範圍大的多。這樣,就有一些內存不能永久地映射在內核空間上。
爲了解決這些制約條件,Linux使用了三種區:
1.ZONE_DMA:這個區包含的頁用來執行DMA操作。
2.ZONE_NOMAL:這個區包含的都是能正常映射的頁。
3.ZONE_HIGHEM:這個區包"高端內存",其中的頁能不永久地映射到內核地址空間。

     區的實際使用與體系結構是相關的。linux把系統的頁劃分區,形成不同的內存池,這樣就可以根據用途進行分配了。需要說明的是,區的劃分沒有任何物理意義,只不過是內核爲了管理頁而採取的一種邏輯上的分組。儘管某些分配可能需要從特定的區中獲得頁,但這並不是說,某種用途的內存一定要從對應的區來獲取,如果這種可供分配的資源不夠用了,內核就會佔用其他可用去的內存。下表給出每個區及其在X86上所佔的列表:

    image

     每個區都用定義在linux/mmzone.h中的struct zone表示,如下:

01 structzone {
02          spinlock_t             lock;
03          unsignedlong          free_pages;
04          unsignedlong          pages_min, pages_low, pages_high;
05          unsignedlong          protection[MAX_NR_ZONES];
06          spinlock_t             lru_lock;       
07          structlist_head       active_list;
08          structlist_head       inactive_list;
09          unsignedlong          nr_scan_active;
10          unsignedlong          nr_scan_inactive;
11          unsignedlong          nr_active;
12          unsignedlong          nr_inactive;
13          int                    all_unreclaimable; 
14          unsignedlong          pages_scanned;    
15          structfree_area       free_area[MAX_ORDER];
16          wait_queue_head_t      * wait_table;
17          unsignedlong          wait_table_size;
18          unsignedlong          wait_table_bits;
19          structper_cpu_pageset pageset[NR_CPUS];
20          structpglist_data     *zone_pgdat;
21          structpage            *zone_mem_map;
22          unsignedlong          zone_start_pfn;
23   
24          char                   *name;
25          unsignedlong          spanned_pages;  
26          unsignedlong          present_pages;  
27 };

     其中的lock域是一個自旋鎖,這個域只保護結構,而不是保護駐留在這個區中的所有頁。沒有特定的鎖來保護單個頁。free_pages域是這個區中空閒頁的個數。內核儘可能的保護有pages_min個空閒頁可用。name域是一個以NULL結束的字符串,表示這個區的名字。內核啓動期間初始化這個值,其代碼位於mm/page_alloc.h中,三個區的名字分別是"DMA","Normal","HighMem"。

內核提供了一種請求內層的底層機制,並提供了對它進行訪問的幾個接口。所有這些接口都是以頁爲單位進行操作的。下表給出所有底層的頁分配方法:

    image

     當你不再需要頁時可以用下列函數釋放它們,只是提醒:僅能釋放屬於你的頁,否則可能導致系統崩潰。內核是完全信任自己的,如果有非法操作,內核會開心的把自己掛起來,停止運行。列表如下:

     image

     上面提到都是以頁爲單位的分配方式,那麼對於常用的以字節爲單位的分配來說,內核通供的函數是kmalloc(),和mallloc很像吧,其實還真是這樣,只不過多了一個flags參數。用它可以獲得以字節爲單位的一塊內核內存。如果需要的是頁----尤其是在你的需求總量接近2的冪次方的時候----那麼,前面討論的頁分配接口可能是更好的選擇。

     接下來,注意的話,可能會發現無論是頁分配接口還是kmalloc都有一個分配器標誌(如GFP_KERNEL這樣的)。這些標誌可分爲三類:行爲修飾符,區修飾符及類型.下面就來討論個問題.

     1.行爲修飾符(linux/gfp.h):表示內核應當如何分配所需的內存。在某些特定的情況下,只能使用某些特定的方法分配內存。可以同時使用這些標誌,用|鏈接。列表如下:

      image

     2.區分配符:它只關心去應當從何處分配。通常,分配可以從任何區開始。不過,內核優先從ZONE_NORMAL開始,這樣可以確保其他區在需要時有足夠的空閒頁可以使用。區修飾符如下:

      image

      不能給_get_free_pages()指定ZONE_HIGHMEM,因爲這個函數返回都是邏輯地址,而不是page結構。這兩個函數分配的內存當前可能有可能還沒有映射到內核的虛擬地址空間,因此,也可能根本就沒有邏輯地址。只有alloc_pages()才能分配高端內存。實際上,大多數ZONE_NORMAL就已經足夠了。

     3.類型標誌:指定所需的行爲和區描述符以完成特殊類型的處理。正因爲這點,內核代碼趨向於使用正確的類型標誌,而不是一味地指定它可能需要用到的多個描述符。下面兩個表分別給出了類型標誌的列表和每個類型標誌與哪些修飾符相關聯:

     image               image

     上表中,左邊是類型標誌,右邊是每種類型標誌後隱含的修飾符列表。在編寫的大多數代碼中,用到的要麼是GFP_KERNEL,要麼是GFP_ATOMIC。下表是通常情形和所用標誌的列表,不管使用那種分配類型,你都必須進行檢查,並對錯誤進行處理:

    image

     有了kmalloc,當然就有kfree()(linux/slab.h),釋放由kmalloc()分配出來的內存塊。如果想要釋放的內存不是由kmalloc()分配的,或者想要釋放的內存早就被釋放了,在這種情況下調用這個函數會導致嚴重的後果。特別說明kfree(NULL)是安全的。

     vmalloc()和kmalloc是一樣的作用,不同在於前者分配的內存虛擬地址是連續的,而物理地址則無需連續。這也是用戶空間分配函數的工作方式,如malloc().kmalloc()可以保證在物理地址上都是連續的(當然,虛擬地址當然也是連續的)。vmalloc()函數只確保頁在虛擬機地址空間內是連續的。它通過分配非聯繫的物理內存塊,再“修正”頁表,把內存映射到邏輯地址空間的連續區域中,就能做到這點。但很顯然這樣會降低處理性能,因爲內核不得不做“拼接”的工作。所以這也是爲什麼不得已才使用vmalloc()的原因(比如獲得大內存時)。大多數情況下,只有硬件設備需要得到物理地址連續的內存。硬件設備存在於內存管理單元以外,它根本不懂什麼是虛擬地址。因此,硬件設備用到的任何內存區都必須是物理上連續的塊,而不僅僅是虛地址連續的塊。最後需要說明的是,vmalloc()可能睡眠,不能從中斷上下文中進行調用,也不能從其他不允許阻塞的情況下進行調用。釋放時必須使用vfree().

     分配和釋放數據結構是所有內核中最普遍的操作之一。爲了便於數據的頻繁分配和回收,常常會用到一個空間鏈表。它就相當於對象高速緩存以便快速存儲頻繁使用的對象類型。在內核中,空閒鏈表面臨的主要問題之一是不能全局控制。當可用內存變得緊張的時候,內核無法通知每個空閒鏈表,讓其收縮緩存的大小以便釋放一些內存來。實際上,內核根本不知道有這樣的空閒離岸邊。爲了彌補這一缺陷,也爲了是代碼更加穩固,linux內核提供了slab層(也就是所謂的slab分類器),slab分類器扮演了通用數據結構緩存層的角色。slab分配器試圖在如下幾個原則中尋求一種平衡:

1.頻繁使用的數據結構也會頻繁分配和釋放,因此應當緩存它們。
2.頻繁分配和回收必然會導致內存碎片。爲了避免這種情況,空閒鏈表的緩存會連續地存放。因爲已釋放的數據結構又會放回空閒鏈表,不會導致碎片。
3.回收的對象可以立即投入下一次分配,因此,對於頻繁的分配和釋放,空閒鏈表能夠提高其性能。
4.如果讓部分緩存專屬於單個處理器,那麼,分配和釋放就可以在不加SMP鎖的情況下進行。
5.對存放的對象進行着色,以防止多個對象映射到相同的高速緩存行。

     slab層把不同的對象劃分爲所謂的高速緩存組,其中每個高速緩存都存放不同類型的對象,每種對象類型對應一個高速緩存。kmalloc()接口建立在slab層上,使用了一組通用高速緩存。這些緩存又被分爲slabs,slab由一個或多個物理上連續的頁組成,一般情況下,slab也就僅僅由一頁組成。每個高速緩存可以由多個slab組成。每個slab都包含一些對象成員,這裏的對象指的是被緩存的數據結構,每個slab處於三種狀態之一:滿,部分滿,空。當內核的某一部分需要一個新的對象時,先從部分滿的slab中進行分配。如果沒有部分滿的slab,就從空的slab中進行分配。如果沒有空的slab,就要創建一個slab了。下圖給出高速緩存,slab及對象之間的關係:

     image

     上圖中的每個cache由kmem_cache_s結構表示,這個結構包含三個鏈表slabs_full,slab_partial和slabs_empty,均存放在kmem_list3結構內,這些鏈表包含高速緩存中的所有slab,slab描述符structslab:

1 structslab {
2         structlist_head list;      
3         unsignedlong    colouroff; 
4         void             *s_mem;    
5         unsignedint     inuse;     
6         kmem_bufctl_t    free;      
7 };

     slab描述符要麼在slab之外另行分配,要麼就在slab自身最開始的地方。如果slab很小或者slab內核有足夠的空間容納slab描述符,那麼描述符就存放在slab裏面.slab分配器創建新的slab是通過__get_free_pages()低級內存分配器進行的:

1 staticinlinevoid*kmem_getpages(kmem_cache_t *cachep, unsignedlongflags)
2 {
3         void*addr;
4         flags|= cachep->gfpflags;
5         addr= (void*)__get_free_pages(flags,cachep->gfporder);
6         returnaddr;
7 }

     上面的是一個描述原理的簡化版。接着,調用kmem_freepages()釋放內存,而對給定的高速緩存頁,kmem_freepages()最終調用的是free_pages().當然,slab層的關鍵就是避免頻繁分配和釋放頁。由此可知,slab頁只有當給定的高速緩存中既沒有部分滿也沒有空的slab時候纔會調用頁分配函數。而只有在下列情況下才會調用釋放函數:當可用內存變得緊缺時,系統試圖釋放出更多內存以供使用,或者當高速緩存顯式地被銷燬時。slab層的管理是在每個高速緩存的基礎上,通過提供個整個內核一個簡單的接口來完成的。通過接口就可以創建和銷燬新的高速緩存,並在高速緩存內分配和釋放對象。高速緩存及slab的複雜管理完全通過slab層的內部機制來處理。當創建一個高速緩存後,slab層所起的作用就像一個專用的分配器,可以爲具體的對象類型進行分配。一個新的高速緩存是通過一下接口進行創建的:

1 kmem_cache_t *kmem_cache_create(constchar*name,size_tsize,size_talign,unsignedlongflags,
1 void(*ctor)(void*, kmem_cache_t *,unsignedlong),
2 void(*dtor)(void*, kmem_cache_t *,unsignedlong));
1 <FONTface=微軟雅黑>     有關這個函數的說明,我就省略了,需要的網上一大堆。這個函數成功時會返回一個執行所創建高速緩存的指針,否則,返回空。這個函數由於會睡眠,因此不能在中斷上下文中使用。要銷燬一個高速緩存,調用:intkmem_cache_destroy(kmem_cache_t*cachep),同樣,也是不能在中斷上下文中使用。調用該函數之前必須確保存在以下兩個條件:</FONT>
1.高速緩存中的所有slab都必須爲空。
2.在調用kmem_cache_destory()期間不再訪問這個高速緩存,調用者必須確保這種同步。

     創建了高速緩存以後,就可以通過下列函數從中獲取對象:void * kmem_cache_alloc(kmem_cache_t*cachep, intflags)。該函數從高速緩存cachep中返回一個指向對象的指針。如果高速緩存的所有slab中都沒有空閒的對象,那麼slab層必須通過kmem_getpages()獲取新的頁,flags的值傳遞給__get_free_pages().最後,釋放一個對象,並把它返回給原來的slab,可以使用下面的函數:

1 voidkmem_cache_free(kmem_cache_t*cachep,void*objp)

     這樣就能把cachep中的對象objp標記爲空閒了,關於slab分配器的使用實例,參考資料上有,我就不說了。相比較以前的用戶空間棧而言,內核棧是非常小的。每個進程都有自己的內核棧進程在內核執行期間的整個調用鏈必須放在自己的內核棧上。中斷處理程序也使用被它們打斷的進程的堆棧。這就意味着,在最惡劣的情況下,8kB的內核棧可能會由多個函數的嵌套調用鏈和幾個中斷處理程序來共享。顯然,深度的嵌套會導致溢出。

     根據定義,在高端內存中的頁不能永久地映射到內核地址空間上。因此,通過alloc_pages()函數以__GFP_HIGHMEM標誌獲得的頁不可能有邏輯地址。一旦這些頁被分配,就必須映射到內核的邏輯地址空間上。要映射一個給定的page結構到內核地址空間,可以使用void*kmap(struct page *page)這個函數在高端內存或低端內存上都能用。如果page結構對應的是低端內存中的一頁,函數只會單純地返回該頁的虛擬地址,如果頁位於高端內存,則會建立一個永久映射,在返回地址。這個函數可以睡眠,所以kmap()只能用在進程上下文中。當不再需要內存映射的時候,就用下列函數進行解除映射:

1 voidkunmem(structpage*page)

     當必須創建一個映射而當前的上下文又不能睡眠時,內核提供了臨時睡眠(也就是原子睡眠)。只要有一組保留的永久映射,它們就可以臨時持有新創建的一個映射。內核可以原子地把高端內存中的一個頁映射到某個保留的映射中。因此,臨時映射可以用在不能睡眠的地方。建立臨時映射:void*kmap_atomic(struct page *page,enum km_typetype).參數type是下列枚舉類型之一,描述了臨時映射的目的,如下:

    image

     這個函數不會阻塞,它也禁止內核搶佔,通過函數void *kunmap_atomic(void *kvaddr,enum km_typetype).這個函數還是不會映射。

     最後,我們總結一下,說說分配函數的選擇吧,總結如下:

1.如果需要連續的物理頁,就可以使用某個低級頁分配器或kmalloc().
2.如果想從高端內存進行分配,使用alloc_pages().
3.如果不需要物理上連續的頁,而僅僅是虛擬地址上連續的頁,那麼就是用vmalloc
4.如果要創建和銷燬很多大的數據結構,那麼考慮建立slab高速緩存。
     好了,有關內存管理基本概述完畢。可以選擇幾本書籍在做補充。
發佈了68 篇原創文章 · 獲贊 45 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章