內存管理
在內核中分配內存不像在其他地方分配內存那么容易。造成這種局面的因素很多,根本原因是內核本身不能像用戶空間那樣奢侈地使用內存。
1.頁
內核把物理頁作為內存管理的基本單位。內存管理單元(MMU,管理內存并把虛擬地址轉換為物理地址的硬件)通常以頁為單位。體系結構不同,支持的頁大小也不同。內核用struct page
結構表示每個物理頁:
struct page {
unsigned long flags;
atomic_t count;
unsigned int mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};
對上面重要變量說明:
-
flag
的每一位單獨表示一個狀態,標志定義在<linux/page-flags.h>
-
count
存放頁的引用次數,為0則是空閑頁 -
virtual
是頁的虛擬地址
2.區
由于硬件限制,內核對頁不能一視同仁。有些頁位于內存特定的物理地址上,不能用于一些特定的任務,因此內核把頁劃分為不同的區(zone)。Linux必須處理如下兩種由于硬件缺陷而引起的內存尋址問題:
- 一些硬件只能用某些特定內存來執行DMA(直接內存訪問)
- 一些體系結構的內存物理尋址范圍比虛擬尋址范圍大的多,因此部分內存永遠無法映射到內核空間
因此Linux主要存在四種區:
- ZONE_DMA,包含的頁可以執行DMA
- ZONE_DMA32,和ZONE_DMA不同在于,這些頁面只能被32位設備訪問,某些體系下該區比ZONE_DMA更大
- ZONE_NORMAL,能夠正常映射的頁
- ZONE_HIGHMEM,不能永久被映射到內核空間地址的區
每個區都用struct zone
表示,定義在<linux/mmzone.h>
:
struct zone {
spinlock_t lock;
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
unsigned long protection[MAX_NR_ZONES];
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;
int all_unreclaimable;
unsigned long pages_scanned;
struct free_area free_area[MAX_ORDER];
wait_queue_head_t * wait_table;
unsigned long wait_table_size;
unsigned long wait_table_bits;
struct per_cpu_pageset pageset[NR_CPUS];
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_pfn;
char *name;
unsigned long spanned_pages;
unsigned long present_pages;
};
其中,lock
是自旋鎖防止該結構被并發訪問;watermark
數組持有該區的最小值、最低和最高水位值;name
是以NULL結尾的區名字,三個區名字為DMA,Normal和HighMem。
3.獲得頁
前面了解了頁和區的概念,下面講述如何請求和釋放頁。
請求頁
標志 | 描述 |
---|---|
alloc_page(gfp_mask) | 只分配一頁,返回指向頁結構的指針 |
alloc_pages(gfp_mask, order) | 分配2^order頁,返回指向第一頁頁結構的指針 |
__get_free_page(gfp_mask) | 只分配一頁,返回指向其邏輯地址的指針 |
__get_free_pages(gfp_mask, order) | 分配2^order頁,返回指向第一頁邏輯地址的指針 |
get_zeroed_page(gfp_mask) | 只分配一頁,讓其內容填充0,返回指向邏輯地址的指針 |
釋放頁
釋放頁需要謹慎,只能釋放屬于你的頁。傳遞了錯誤的struct page
或地址,,用了錯誤的order
值都可能導致系統崩潰。
例如釋放8個頁:
free_pages(page, 3)
可以看到釋放過程與C語言的釋放內存很相似的。
4.kmalloc()
上述的方法是對以頁為單位的連續物理頁,而以字節為單位的分配,內核提供的函數是kmalloc()
。使用方法和malloc()
類似,只是多了一個flags
參數,其在<linux/slab.h>
中聲明:
void * kmalloc(size_t size, gfp_t flags)
與kmalloc()
對應的函數就是kfree()
,kfree()
聲明于<linux/slab.h>
中:
void kfree(const void *ptr)
5.vmalloc()
vmalloc()
和kmalloc()
工作方式類似,但是kmalloc()
使用的連續的物理地址。vmalloc()
使用非連續的物理地址,該函數為了把物理上不連續的頁轉換為虛擬地址空間上連續的頁,必須專門建立頁表項。
大多數情況下,一般硬件設備需要使用連續的物理地址,而軟件可以使用非連續的物理地址,但是大多數情況,為了性能提升,內核往往用kmalloc()
更多。
vmalloc()
函數聲明在<linux/vmalloc.h>
中,定義在<mm/vmalloc.c>
中。用法和用戶空間的malloc()
相同:
void * vmalloc(unsigned long size)
釋放通過vmalloc()
所獲得的內存,使用下面函數:
void vfree(const void *addr)
6.slab層
分配和釋放數據結構是所有內核中最常用操作之一。為了便于數據的頻繁分配和回收,編程人員常常會用到空閑鏈表。空閑鏈表包含可供使用的、已經分配好的數據結構塊。當代名需要一個新的數據結構實例時,就可以從空閑鏈表中抓取一個,而不需要分配內存,再把數據存放進去。不需要這個數據結構的實例時,就放回空閑鏈表,而不是釋放它。空閑鏈表相對于對象的高速緩存——快速存儲頻繁使用的對象類型(這個策略簡直是awesome!)。
沒有免費的蛋糕,對于空閑鏈表存在的主要問題是無法全局控制。當內存緊缺時,內核無法通知每個空閑鏈表,讓其收縮緩存的大小,以便釋放部分內存。實際上,內核根本就不知道任何空閑鏈表。因此未來彌補這個缺陷,Linux內核提供了slab層
(也就是所謂的slab分配器)。slab分配器扮演了通用數據結構緩存層的角色。對于slab分配器設計需要考慮一下幾個原則:
- 頻繁使用的數據結構也會頻繁分配和釋放,因此應當緩存它們。
- 頻繁分配和回收必然會導致內存碎片。為了避免這種情況,空閑鏈表的緩存會連續地存放。因為已釋放的數據結構又會放回空閑鏈表,不會導致碎片。
- 回收的對象可以立即投入下一次分配,因此,對于頻繁的分配和釋放,空閑鏈表能夠提高其性能。
- 如果讓部分緩存專屬于單個處理器,那么,分配和釋放就可以在不加SMP鎖的情況下進行。
- 對存放的對象進行著色,以防止多個對象映射到相同的高速緩存行。
slab層
把不同的對象劃分為所謂的高速緩存組,其中每個高速緩存都存放不同類型的對象,每種對象類型對應一個高速緩存,例如一個高速緩存用于task_struct
,一個用于struct inode
。kmalloc()接口建立在slab層上,使用了一組通用高速緩存。這些緩存又被分為slabs,slab由一個或多個物理上連續的頁組成,一般情況下,slab也就僅僅由一頁組成。每個高速緩存可以由多個slab組成。每個slab都包含一些對象成員,這里的對象指的是被緩存的數據結構,每個slab處于三種狀態之一:滿,部分滿,空。當內核的某一部分需要一個新的對象時,先從部分滿的slab中進行分配。如果沒有部分滿的slab,就從空的slab中進行分配。如果沒有空的slab,就要創建一個slab了。下圖給出高速緩存,slab及對象之間的關系:
每個緩存都使用kmem_catche
結構表示,結構中包含3個鏈表。這些鏈表包含高速緩存所有的slab。slab描述符struct slab
用來描述每個slab
:
struct slab {
struct list_head list; /*滿,部分滿或空鏈表*/
unsigned long colouroff; /*slab著色的偏移量*/
void *s_mem; /*在slab中的第一個對象*/
unsigned int inuse; /*已分配的對象數*/
kmem_bufctl_t free; /*第一個空閑對象*/
};
slab層負責內存緊缺情況下所有底層的對齊、著色、分配、釋放和回收等。
7.棧上的靜態分配
在前面討論的分配例子,不少可以分配到棧上。用戶空間可以奢侈地負擔很大的棧,而且棧空間還可以動態增長,相反內核空間不能——棧小而固定。給每個進程分配一個固定小棧,可以減小內存消耗和棧管理任務負擔。
進程的內核棧大小既依賴體系結構,也和編譯時的選項有關。在任何一個函數中,都必須盡量節省棧資源。讓函數所有局部變量之后不要超過幾百字節(棧上分配大量的靜態分配是不理智的),棧溢出就會覆蓋掉臨近堆棧末端的數據。首先就是前面講的thread_info
。
8.每個CPU使用數據
支持SMP的操作系統使用每個CPU上的數據,對于給定的處理器其數據是唯一的。一般而言,每個CPU的數據存放在一個數組內,數組中的每一項對應著系統上一個存在的處理器,安裝當前處理器號就能確定這個數組的當前元素。
在Linux中引入了新的操作接口稱為percpu
,頭文件<linux/percpu.h>
聲明了所有接口操作例程,可以在文件mm/slab.c
和<asm/percpu.h>
找到定義。
使用每個CPU數據的好處是:
- 減少了數據鎖定
- 大大減少了緩存失效,一個CPU操作另一個CPU的數據時,必須清理另一個CPU的緩存并刷新,存在不斷的緩存失效。持續不斷的緩存失效稱為緩存抖動。
這種方式的唯一安全要求就是禁止內核搶占,同時注意進程在訪問每個CPU數據過程中不能睡眠——否則,喚醒之后可能已經到其他處理器上了。