內存管理
- 1 頁
- 2 區
- 3 獲得頁
- 獲得填充為0的頁
- 釋放頁
- 4 kmalloc()
- gfp_mask標志
- kfree()
- 5 vmalloc()
- 6 slab層
- slab層的設計
- 7 slab分配器的接口
- 8 在棧上的靜態分配
- 9 高端內核的映射
- 永久映射
- 臨時映射
- 10 每個CPU的分配
- 11 新的每個CPU的接口
- 編譯時的每個CPU數據
- 運行時每個CPU數據
- 12 使用每個CPU數據的原因
- 13 分配函數的選擇
1 頁
內核把物理頁作為內存管理的基本單元,內存管理單元(MMU)以頁為單位來管理系統中的頁表,從虛擬內存的角度看,頁就是最小單位。
體系結構不同,支持頁的大小也不盡相同,還有些體系結構甚至支持幾種不同的頁大小。大多數32位體系結構支持4KB的頁,而64位體系結構一般會支持8KB的頁。
內核用struct page結構表示系統中的每個物理頁,該結構位于linux/mm.h中。
struct page {page_flags_t flags; /* Atomic flags, some possibly* updated asynchronously */atomic_t _count; /* Usage count, see below. */atomic_t _mapcount; /* Count of ptes mapped in mms,* to show when page is mapped* & limit reverse map searches.*/unsigned long private; /* Mapping-private opaque data:* usually used for buffer_heads* if PagePrivate set; used for* swp_entry_t if PageSwapCache*/struct address_space *mapping; /* If low bit clear, points to* inode address_space, or NULL.* If page mapped as anonymous* memory, low bit is set, and* it points to anon_vma object:* see PAGE_MAPPING_ANON below.*/pgoff_t index; /* Our offset within mapping. */struct list_head lru; /* Pageout list, eg. active_list* protected by zone->lru_lock !*//** On machines where all RAM is mapped into kernel address space,* we can simply calculate the virtual address. On machines with* highmem some memory is mapped into kernel virtual memory* dynamically, so we need a place to store that address.* Note that this field could be 16 bits on x86 ... ;)** Architectures with slow multiplication can define* WANT_PAGE_VIRTUAL in asm/page.h*/
#if defined(WANT_PAGE_VIRTUAL)void *virtual; /* Kernel virtual address (NULL ifnot kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
flags域用來存放頁的狀態,flags的每一位單獨表示一種狀態,page_flags_t是 unsigned long類型,所以它至少可以同時表示出32種不同的狀態。這些表示定義在linux/page-flags.h中。
_count域存放頁的引用計數,也就是這一頁被引用了多少次。當數值為0時,說明當前內核并沒有引用這一頁,于是,在新的分配中就可以使用它。內核代碼不應當直接檢查該域,而是調用page_count()進行檢查,該函數唯一的參數就是page結構。當頁空閑時,返回0,返回一個正整數表示頁在使用。一個頁可以由頁緩存使用,這是mapping指向和這個頁關聯的address_sapce對象,或者作為私有數據,由private指向,或者作為進程頁表中的映射。
virtual域是頁的虛擬地址。通常情況下,它就是頁在虛擬內存中的地址,高端內存并不永久地映射到內核地址空間上,在這種情況下,這個域的值為NULL。
page結構與物理頁相關,而并非與虛擬頁相關,內核僅僅用這個數據結構來描述當前時刻在相關的物理頁中存放的東西,系統中的每個物理頁都要分配一個這樣的結構
2 區
由于硬件的限制,內核并不能對所有的頁一視同仁,內核把頁劃分為不同的區。Linux使用了三種區:
- ZONE_DMA:這個區包含的頁能用來執行DMA操作
- ZONE_NOEMAL:這個區包含的都是能正常映射的頁
- ZONE_HIGHMEM:這個區包含高端內存,其中的頁能不永久映射到內核地址空間。
這些區定義位于linux/mmzone.h。區的實際使用和分布是與體系結構相關的。在x86上的區分布如下表:
注意,區的劃分沒有任何意義,這只不過是內核為了管理頁面而采取的一種邏輯上的分組。
每個區都用struct zone表示,定義在linux/mmzone.h中
struct zone {/* Fields commonly accessed by the page allocator */unsigned long free_pages;unsigned long pages_min, pages_low, pages_high;/** protection[] is a pre-calculated number of extra pages that must be* available in a zone in order for __alloc_pages() to allocate memory* from the zone. i.e., for a GFP_KERNEL alloc of "order" there must* be "(1<<order) + protection[ZONE_NORMAL]" free pages in the zone* for us to choose to allocate the page from that zone.** It uses both min_free_kbytes and sysctl_lower_zone_protection.* The protection values are recalculated if either of these values* change. The array elements are in zonelist order:* [0] == GFP_DMA, [1] == GFP_KERNEL, [2] == GFP_HIGHMEM.*/unsigned long protection[MAX_NR_ZONES];struct per_cpu_pageset pageset[NR_CPUS];/** free areas of different sizes*/spinlock_t lock;struct free_area free_area[MAX_ORDER];ZONE_PADDING(_pad1_)/* Fields commonly accessed by the page reclaim scanner */spinlock_t lru_lock; struct list_head active_list;struct list_head inactive_list;unsigned long nr_scan_active;unsigned long nr_scan_inactive;unsigned long nr_active;unsigned long nr_inactive;unsigned long pages_scanned; /* since last reclaim */int all_unreclaimable; /* All pages pinned *//** prev_priority holds the scanning priority for this zone. It is* defined as the scanning priority at which we achieved our reclaim* target at the previous try_to_free_pages() or balance_pgdat()* invokation.** We use prev_priority as a measure of how much stress page reclaim is* under - it drives the swappiness decision: whether to unmap mapped* pages.** temp_priority is used to remember the scanning priority at which* this zone was successfully refilled to free_pages == pages_high.** Access to both these fields is quite racy even on uniprocessor. But* it is expected to average out OK.*/int temp_priority;int prev_priority;ZONE_PADDING(_pad2_)/* Rarely used or read-mostly fields *//** wait_table -- the array holding the hash table* wait_table_size -- the size of the hash table array* wait_table_bits -- wait_table_size == (1 << wait_table_bits)** The purpose of all these is to keep track of the people* waiting for a page to become available and make them* runnable again when possible. The trouble is that this* consumes a lot of space, especially when so few things* wait on pages at a given time. So instead of using* per-page waitqueues, we use a waitqueue hash table.** The bucket discipline is to sleep on the same queue when* colliding and wake all in that wait queue when removing.* When something wakes, it must check to be sure its page is* truly available, a la thundering herd. The cost of a* collision is great, but given the expected load of the* table, they should be so rare as to be outweighed by the* benefits from the saved space.** __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the* primary users of these fields, and in mm/page_alloc.c* free_area_init_core() performs the initialization of them.*/wait_queue_head_t * wait_table;unsigned long wait_table_size;unsigned long wait_table_bits;/** Discontig memory support fields.*/struct pglist_data *zone_pgdat;struct page *zone_mem_map;/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */unsigned long zone_start_pfn;unsigned long spanned_pages; /* total size, including holes */unsigned long present_pages; /* amount of memory (excluding holes) *//** rarely used fields:*/char *name;
} ____cacheline_maxaligned_in_smp;
系統中只有三個區,因此也只有三個這樣的結構,frr_pages域是這個區中空閑頁的個數,name是一個以NULL結束的字符串,表示這個區的名字。內核啟動期間初始化這個值,其代碼位于mm/page-alloc.c中,三個區的名字分別為DMA、Normal、HighMem。
3 獲得頁
static inline struct page *
alloc_pages(unsigned int gfp_mask, unsigned int order);
gfp_mask會在文章的gfp_mask節介紹。
該函數分配2的order次方個連續的物理頁,并返回一個指針,該指針指向第一個頁page結構體,如果出錯,返回NULL。可以使用下面這個函數把給定的頁轉為它的邏輯地址:
void *page_address(struct page *page);
該函數返回一個指針,指向給定物理頁當前所在的邏輯地址。如果你無須用到struct page,你可以調用:
unsigned long __get_free_pages(unsigned int gfp_make,unsigned int order);
這個函數與alloc_pages()作用相同,不過它直接返回所請求的第一個頁的邏輯地址。
如果你只需要一頁,可以用下面兩個封裝好的函數:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0);
#define __get_free_page(gfp_mask) \__get_free_pages((gfp_mask),0);
獲得填充為0的頁
如果你需要讓返回的頁的內容全為0,請用下面這個函數:
extern unsigned long FASTCALL(get_zeroed_page(unsigned int gfp_mask));
這個函數與_get_free_page()工作方式相同,只不過把分配好的頁都填充成了0。
所有底層的頁分配方法如下:
釋放頁
當你不在虛擬頁時,可以用下面的一族函數來釋放它們:
釋放頁時需謹慎,只能釋放屬于自己的頁,傳遞了錯誤的struct page或地址,用錯了order值,這些都可能導致系統崩潰,內核時完全信賴自己的,上面都能執行成功。
4 kmalloc()
kmalloc()函數與用戶空間的malloc()一族函數非常類似,只不過它多了一個flags參數。kmalloc函數是一個簡單的接口,用它可以獲得以字節為單位的一塊內核內存。如果你需要整個頁,那么,前面討論的頁分配接口可能是更改的接口。
kmalloc()在linux/slab.h中申明:
void *kmalloc(size_t size, int flags);
- size:是指要分配的內存的字節數。
- flags:是分配標志,它提供了多種kmalloc( )的行為。在下面的gfp_mask標志節會介紹。
這個函數返回一個指向內存塊的指針,其內存塊至少要有size大小,所分配的內存區在物理上是連續的,在出錯時,它返回NULL,除非沒有足夠的內存可用,否則內核總能分配成功。在使用kmalloc()時,必須檢查返回值是不是NULL。
為什么是至少分配size大小?因為內核分配內存是基于頁的,因此在可用內存內,某些分配可能向下取整。
gfp_mask標志
不管在低級頁分配函數中,還是在kmalloc()中,都用到了分配器標志。現在,我們深入討論以一下這些標志。這些標志可分為三類:行為修飾符、區修飾符及類型。行為修飾符表示內核應當如何分配所需的內存,例如,中斷處理程序就要求內核在分配內存的過程中不能睡眠。區修飾符表示從哪兒分配內存。類型標志組合了行為修飾符和區修飾符,將各種可能用到的組合歸納為不同類型。
所有這些標志都是在linux/gfp.h中申明的。
-
行為修飾符
-
區描述符
內核優先從ZONE_NORMAL開始,這樣可以確保其他區在需要時有足夠的空閑頁可供使用。
你不能給_get_free_pages()或kmalloc()指定__GFP_HIGHMEM,因為這兩個函數返回的都是邏輯地址,而不是page結構,這兩個函數分配的內存當前有可能還沒有映射到內核的虛擬地址空間,因此,可能根本沒有邏輯地址,只有alloc_pages()才能分配高端地址。 -
類型標志
kfree()
kmalloc() 的另一端就是kfree(),kfree()聲明在linux/slab.h中
void kfree(const void *ptr);
kfree釋放由kmalloc分配出來的內存塊。如果想要釋放的內存不是由kmalloc()分配的,或者想要釋放的內存早就被釋放完了,調用這個函數會導致嚴重的后果。注意,調用kfree(NULL)是安全的。
5 vmalloc()
是malloc函數的工作方式類似于kmalloc(),只不過前者分配的內存虛擬地址是連續的,而物理地址無需連續。kmalloc函數確保頁在物理地址上是連續的,vmalloc函數只確保頁在虛擬地址空間內是連續的。它通過分配非連續的物理內存塊,再修正頁表。把內存映射到邏輯地址空間的連續區域中,就能做到這點。
vmalloc函數在linux/vmalloc.h中聲明,在mm/vmalloc.c中定義。:
void *vmalloc(unsigned long size);
該函數返回一個指針,指向邏輯上連續的一塊內存區,其大小至少為size。在發生錯誤時,函數返回NULL。函數可能睡眠,因此,不能從中斷上下文中進行調用,也不能從其他不允許阻塞的情況下使用。
要釋放通過vmalloc所獲得的內存,使用下面的函數:
void vfree(void *addr);
這個函數會釋放從addr開始的內存塊,addr是由vmalloc分配的內存塊地址。這個函數也可以睡眠,因此,不能從中斷上下文中調用,它沒有返回值。
6 slab層
分配和釋放數據結構時所有內核中最普通的操作之一,為了便于數據的頻繁分配和回收,編程者常常會用到一個空閑鏈表。該空閑鏈表包含有可供使用的、已經分配好的數據結構塊。當需要一個新的數據結構實例時,就可以從空閑鏈表中抓取一個,而不需要分配內存,以后,當不再需要這個數據結構的實例時,就把它放回空閑鏈表,而不是釋放它。從某種意義上說,空閑鏈表相當于數據結構實例高速緩存,以便快速存儲頻繁使用的對象實例。
Linux內核提供了slab層(slab分配器),slab分配器扮演了通用數據結構緩沖層的角色。
slab層的設計
每個高速緩存被劃分為slab,每個高速緩存可以由多個slab組成,slab由一個或多個物理上連續的頁組成,每個slab都包含一些對象成員,這里的對象是被緩存的數據結構,每個slab處于三種狀態之一:慢、部分滿或空。當內核的某一部分需要一個新的對象時,先從部分滿的slab中進行分配,如果沒有部分滿的slab,就從空的slab中進行分配,如果沒有空的slab,就要創建一個slab了。
作為一個例子,讓我們考察一下inode結構,該結構時磁盤索引節點在內存的體現,這些數據會頻繁地創建和釋放,因此,用slab分配器來管理它們就很有必要。因為struct inode就用inode_cachep進行分配的,這種高速緩存由一個或多個slab組成,每個slab包含盡可能多的struct inode對象。當內核請求分配一個新的inode結構時,內核就從部分滿的slab或空slab返回一個指向已分配但未使用的結構指針。當內核用完inode對象后,slab分配就就把該對象標記為空閑。高速緩存、slab和對象之間的關系:
每個高速緩存都是用kmem_cache_s結構來表示,這個結構包含三個鏈表slabs_full、slabs_partial和slab_empty,均放在kmem_list3結構內。這些鏈表包含高速緩存中所有的slab。slab描述符struct slab用來描述每個slab。slab分配器可以創建新的slab,這是通過__get_free_pages()低級內核頁分配器實現的。
7 slab分配器的接口
一個新的高速緩存是通過以下函數創建的:
kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags,void (*ctor)(void *, kmem_cache_t *, unsigned long),void (*dtor)(void *, kmem_cache_t *, unsigned long));
第一個參數是字符串,存放著高速緩存的名字。第二個參數是高速緩存中每個元素的大小,第三個參數就是高速緩存內第一個對象的偏移,這用來確保在頁內進行特定的對齊,通常情況,0就可以滿足要求,也就是標準對齊。flags是可選的設置項,用來控制高速緩存的行為。
最后兩個參數ctor和dtor分別是高速緩存的構造和析構函數。只有在新的頁追加到高速緩存時,構造函數才被調用。只有從高速緩存中刪去頁時,析構函數才被調用。有一個析構函數就要有一個構造函數,實際上,Linux內核的高速緩存不使用析構函數或構造函數,可以將這兩個參數都賦值為NULL。
kmem_cache_create()在成功時會返回一個指向所創建高速緩存的指針,否則,返回NULL。這個函數不能在中斷上下文中調用,因為它可能會睡眠。
要銷毀一個高速緩存,則調用:
int kmem_cache_destroy(kmem_cache_t *cachep);
同樣,也不能從中斷上下文中調用這個函數,因為它也可能會睡眠。調用該函數之前必須確保以下兩個條件:
- 高速緩存中的所有slab都必須為空
- 在調用kmem_cache_destroy()期間,不能再訪問這個高速緩存
創建高速緩存之后,就可以通過下列函數從中獲取對象:
void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);
該函數從給定的高速緩存cachep中返回一個指向對象的指針。如果高速緩存的所有slab中都沒有空閑的對象,那么slab層必須通過kmem_getpages()獲取新的頁,flags的值傳遞給__get_free_pages()。
最后釋放一個對象,并把它返回給原先的slab,可以使用下面的函數:
void kmem_cache_free(kmem_cache_t *cachep,void *objp);
這樣就能把高速緩存cachep中的對象objp標記為空閑了。
8 在棧上的靜態分配
內核棧可以是1頁,也可以是兩頁,這取決于編譯時配置選項,棧大小因此在4KB-16KB的范圍內。歷史上,中斷處理程序和被中斷進程共享一個內核棧,當1頁棧的選項被激活時,中斷處理程序就獲得了自己的棧。
9 高端內核的映射
根據定義,在高端內存中的頁不能永久映射到內核地址空間上。在x86體系結構上,高于896的所有物理內存的范圍大都是高端內存,它并不會永久地或自動映射到內核地址空間。
永久映射
要映射一個給定的page結構到內核地址空間,可以使用:
void *kmap(struct page *page);
這個函數在高端內存或低端內存都能用。如果page結構對應的是低端內存中的一頁,函數只會單純地返回該頁的虛擬地址。如果頁位于高端內存,則會建議一個永久映射,再返回地址。這個函數可以睡眠。因此kmap()只能用在進程上下文中。
當不再需要高端內存時,應該解除映射,可以通過kunmap函數完成:
void kunmap(struct page *page);
臨時映射
當必須創建一個映射而當前上下文又不能睡眠時,內核提供了臨時映射(也就是所謂的原子映射)。有一組保留的映射,它們可以存放新創建的臨時映射,內核可以原子地把高端內存中的一個頁映射到某個保留的映射中。因此,臨時映射可以用在不能睡眠的地方,比如中斷處理程序中,因為獲取映射時絕不會阻塞。
可以通過下列函數創建一個臨時映射:
void *kmap_atomic(struct page *page,enum km_type type);
可通過下列函數取消映射:
void kunmap_atomic(void *kvaddr.enum km_type type);
這個函數也不會阻塞。
10 每個CPU的分配
支持SMP的現代操作系統使用每個CPU上的數據,對于給定的處理器其數據時唯一的,每個CPU的數據存放在一個數組中,數組中的每一項對應著系統上一個存在的處理器,當前處理器號確定這個數組的當前元素,這就是2.4內核處理每個CPU數據的方式。
可以像下面這樣聲明數據:
unsigned long my_percou[NR_CPUS];
然后,按如下方式訪問它:
int cpu;
cpu = get_cput; /* 獲取當前處理器號,并禁止內核搶占 */
my_percpu[cpu]++; /* 訪問當前處理器的數據 */put_cpu(); /* 激活內核搶占 */
11 新的每個CPU的接口
2.6內核為了方便創建和操作每個CPU數據,從而引進了新的操作接口,稱作percpu。該接口歸納了前面所述的操作行為,并使每個CPU數據的創建和操作得以簡化。頭文件linux/percpu.h聲明了所有的接口操作例程,可以在文件mm/slab.c和asm/percput.h中找到它們的定義。
編譯時的每個CPU數據
在編譯時定義每個CPU變量很容易:
DEFINE_PER_CPU(type,name);
這個語句為系統中的每個處理器都創建一個類型為type,名字為name的變量實例。如果你需要在別處聲明變量,以防編譯時警告,那么下面的宏將是你的好幫手:
DECLARE_PER_CPU(type,name);
你可以用get_cpu_var()和put_cpu_var()例程操作變量。調用get_cpu_var()返回當前處理器上的指定變量,同時它將禁止搶占,另一方面put_cpu_var()將相應地重新激活搶占。
這些編譯時每個CPU數據的例子并不能在模塊內使用,因為連接程序實際上將它們創建在一個唯一的可執行段中(.data.percpu)。
運行時每個CPU數據
內核實現每個CPU數據的動態分配方法類似于kmalloc()。這些方法原型在文件linux/percpu.h中
宏alloc_percpu()給系統中的每個處理器分配一個指定類型對象的實例。它其實是宏__alloc_percpu()的一個封裝,這個原始宏接受的參數有兩個,一個是要分配的實際字節數,一個是分配時按多少字節對齊。而封裝后的alloc_percpu安裝給定類型的自然邊界對齊。比如:
相應調用free_percpu()將釋放所有處理器上指定的每個CPU數據。
無論是alloc_percpu()還是__alloc_percpu()都會返回一個指針,它用來間接引用動態創建的每個CPU數據,內核提供了兩個宏來利用指針獲取每個CPU數據:
get_cpu_ptr(ptr);
put_cpu_ptr(ptr);
get_cpu_ptr()宏返回一個指向當前處理器數據的實例,它同時會禁止內核搶占,而在put_cpu_ptr宏中重新激活內核搶占。
我們來看一個例子
12 使用每個CPU數據的原因
- 減少了數據鎖定:因為按照么個處理器訪問每個CPU數據的邏輯,你可以不在需要任何鎖。
- 使用每個CPU數據可以大大減少緩存失效。失效發生在處理器試圖使它們的緩存保持同步時,如果一個處理器操作某個數據,而該數據又存放在其他處理器緩存中,那么存放該數據的那個處理器必須清理或刷新它自己的緩存。使用每個CPU數據將使緩存影響降至最低,因為理想情況下只會訪問它自己的數據。percpu接口緩存對齊所有數據,以便確保在訪問一個處理器的數據時,不會將另一個處理器的數據帶入同一個緩存線上。
如果真決定在你的內核中使用每個CPU數據,請考慮使用新街口,但是新接口不向后兼容老內核。
13 分配函數的選擇
- 如果你需要連續的物理頁,就可以使用某個低級頁分配器或kmalloc。
- 如果你想從高端內存進行分配,就可以alloc_pages()。該函數返回一個指向struct page結構的指針,而不是一個指向某個邏輯地址的指針。
- 如果你不需要物理上的連續的頁,而僅僅需要虛擬地址上連續的頁,那么就使用vmalloc()。
- 如果你需要創建和銷毀很多較大的數據結構,那么應該考慮建立slab高速緩存。