內(nèi)存管理
在內(nèi)核中分配內(nèi)存不像在其他地方分配內(nèi)存那么容易。造成這種局面的因素很多扶平,根本原因是內(nèi)核本身不能像用戶空間那樣奢侈地使用內(nèi)存帆离。
1.頁
內(nèi)核把物理頁作為內(nèi)存管理的基本單位。內(nèi)存管理單元(MMU,管理內(nèi)存并把虛擬地址轉(zhuǎn)換為物理地址的硬件)通常以頁為單位结澄。體系結(jié)構(gòu)不同哥谷,支持的頁大小也不同。內(nèi)核用struct page
結(jié)構(gòu)表示每個物理頁:
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
的每一位單獨表示一個狀態(tài)麻献,標志定義在<linux/page-flags.h>
-
count
存放頁的引用次數(shù)们妥,為0則是空閑頁 -
virtual
是頁的虛擬地址
2.區(qū)
由于硬件限制,內(nèi)核對頁不能一視同仁勉吻。有些頁位于內(nèi)存特定的物理地址上监婶,不能用于一些特定的任務,因此內(nèi)核把頁劃分為不同的區(qū)(zone)齿桃。Linux必須處理如下兩種由于硬件缺陷而引起的內(nèi)存尋址問題:
- 一些硬件只能用某些特定內(nèi)存來執(zhí)行DMA(直接內(nèi)存訪問)
- 一些體系結(jié)構(gòu)的內(nèi)存物理尋址范圍比虛擬尋址范圍大的多惑惶,因此部分內(nèi)存永遠無法映射到內(nèi)核空間
因此Linux主要存在四種區(qū):
- ZONE_DMA,包含的頁可以執(zhí)行DMA
- ZONE_DMA32短纵,和ZONE_DMA不同在于带污,這些頁面只能被32位設備訪問,某些體系下該區(qū)比ZONE_DMA更大
- ZONE_NORMAL踩娘,能夠正常映射的頁
- ZONE_HIGHMEM刮刑,不能永久被映射到內(nèi)核空間地址的區(qū)
每個區(qū)都用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
是自旋鎖防止該結(jié)構(gòu)被并發(fā)訪問;watermark
數(shù)組持有該區(qū)的最小值泛烙、最低和最高水位值理卑;name
是以NULL結(jié)尾的區(qū)名字,三個區(qū)名字為DMA蔽氨,Normal和HighMem藐唠。
3.獲得頁
前面了解了頁和區(qū)的概念,下面講述如何請求和釋放頁鹉究。
請求頁
標志 | 描述 |
---|---|
alloc_page(gfp_mask) | 只分配一頁宇立,返回指向頁結(jié)構(gòu)的指針 |
alloc_pages(gfp_mask, order) | 分配2^order頁,返回指向第一頁頁結(jié)構(gòu)的指針 |
__get_free_page(gfp_mask) | 只分配一頁自赔,返回指向其邏輯地址的指針 |
__get_free_pages(gfp_mask, order) | 分配2^order頁妈嘹,返回指向第一頁邏輯地址的指針 |
get_zeroed_page(gfp_mask) | 只分配一頁,讓其內(nèi)容填充0绍妨,返回指向邏輯地址的指針 |
釋放頁
釋放頁需要謹慎润脸,只能釋放屬于你的頁柬脸。傳遞了錯誤的struct page
或地址,毙驯,用了錯誤的order
值都可能導致系統(tǒng)崩潰倒堕。
例如釋放8個頁:
free_pages(page, 3)
可以看到釋放過程與C語言的釋放內(nèi)存很相似的。
4.kmalloc()
上述的方法是對以頁為單位的連續(xù)物理頁爆价,而以字節(jié)為單位的分配垦巴,內(nèi)核提供的函數(shù)是kmalloc()
。使用方法和malloc()
類似铭段,只是多了一個flags
參數(shù)魂那,其在<linux/slab.h>
中聲明:
void * kmalloc(size_t size, gfp_t flags)
與kmalloc()
對應的函數(shù)就是kfree()
,kfree()
聲明于<linux/slab.h>
中:
void kfree(const void *ptr)
5.vmalloc()
vmalloc()
和kmalloc()
工作方式類似稠项,但是kmalloc()
使用的連續(xù)的物理地址涯雅。vmalloc()
使用非連續(xù)的物理地址,該函數(shù)為了把物理上不連續(xù)的頁轉(zhuǎn)換為虛擬地址空間上連續(xù)的頁展运,必須專門建立頁表項活逆。
大多數(shù)情況下,一般硬件設備需要使用連續(xù)的物理地址拗胜,而軟件可以使用非連續(xù)的物理地址蔗候,但是大多數(shù)情況,為了性能提升埂软,內(nèi)核往往用kmalloc()
更多锈遥。
vmalloc()
函數(shù)聲明在<linux/vmalloc.h>
中,定義在<mm/vmalloc.c>
中勘畔。用法和用戶空間的malloc()
相同:
void * vmalloc(unsigned long size)
釋放通過vmalloc()
所獲得的內(nèi)存所灸,使用下面函數(shù):
void vfree(const void *addr)
6.slab層
分配和釋放數(shù)據(jù)結(jié)構(gòu)是所有內(nèi)核中最常用操作之一。為了便于數(shù)據(jù)的頻繁分配和回收炫七,編程人員常常會用到空閑鏈表爬立。空閑鏈表包含可供使用的、已經(jīng)分配好的數(shù)據(jù)結(jié)構(gòu)塊万哪。當代名需要一個新的數(shù)據(jù)結(jié)構(gòu)實例時侠驯,就可以從空閑鏈表中抓取一個,而不需要分配內(nèi)存奕巍,再把數(shù)據(jù)存放進去吟策。不需要這個數(shù)據(jù)結(jié)構(gòu)的實例時,就放回空閑鏈表的止,而不是釋放它檩坚。空閑鏈表相對于對象的高速緩存——快速存儲頻繁使用的對象類型(這個策略簡直是awesome!)。
沒有免費的蛋糕效床,對于空閑鏈表存在的主要問題是無法全局控制睹酌。當內(nèi)存緊缺時,內(nèi)核無法通知每個空閑鏈表剩檀,讓其收縮緩存的大小憋沿,以便釋放部分內(nèi)存。實際上沪猴,內(nèi)核根本就不知道任何空閑鏈表辐啄。因此未來彌補這個缺陷,Linux內(nèi)核提供了slab層
(也就是所謂的slab分配器)运嗜。slab分配器扮演了通用數(shù)據(jù)結(jié)構(gòu)緩存層的角色壶辜。對于slab分配器設計需要考慮一下幾個原則:
- 頻繁使用的數(shù)據(jù)結(jié)構(gòu)也會頻繁分配和釋放,因此應當緩存它們担租。
- 頻繁分配和回收必然會導致內(nèi)存碎片砸民。為了避免這種情況,空閑鏈表的緩存會連續(xù)地存放奋救。因為已釋放的數(shù)據(jù)結(jié)構(gòu)又會放回空閑鏈表岭参,不會導致碎片。
- 回收的對象可以立即投入下一次分配尝艘,因此演侯,對于頻繁的分配和釋放,空閑鏈表能夠提高其性能背亥。
- 如果讓部分緩存專屬于單個處理器秒际,那么,分配和釋放就可以在不加SMP鎖的情況下進行狡汉。
- 對存放的對象進行著色娄徊,以防止多個對象映射到相同的高速緩存行。
slab層
把不同的對象劃分為所謂的高速緩存組轴猎,其中每個高速緩存都存放不同類型的對象嵌莉,每種對象類型對應一個高速緩存,例如一個高速緩存用于task_struct
捻脖,一個用于struct inode
。kmalloc()接口建立在slab層上中鼠,使用了一組通用高速緩存可婶。這些緩存又被分為slabs,slab由一個或多個物理上連續(xù)的頁組成援雇,一般情況下矛渴,slab也就僅僅由一頁組成。每個高速緩存可以由多個slab組成。每個slab都包含一些對象成員具温,這里的對象指的是被緩存的數(shù)據(jù)結(jié)構(gòu)蚕涤,每個slab處于三種狀態(tài)之一:滿,部分滿铣猩,空揖铜。當內(nèi)核的某一部分需要一個新的對象時,先從部分滿的slab中進行分配达皿。如果沒有部分滿的slab天吓,就從空的slab中進行分配。如果沒有空的slab峦椰,就要創(chuàng)建一個slab了龄寞。下圖給出高速緩存,slab及對象之間的關系:
每個緩存都使用kmem_catche
結(jié)構(gòu)表示物邑,結(jié)構(gòu)中包含3個鏈表。這些鏈表包含高速緩存所有的slab滔金。slab描述符struct slab
用來描述每個slab
:
struct slab {
struct list_head list; /*滿色解,部分滿或空鏈表*/
unsigned long colouroff; /*slab著色的偏移量*/
void *s_mem; /*在slab中的第一個對象*/
unsigned int inuse; /*已分配的對象數(shù)*/
kmem_bufctl_t free; /*第一個空閑對象*/
};
slab層負責內(nèi)存緊缺情況下所有底層的對齊、著色鹦蠕、分配冒签、釋放和回收等。
7.棧上的靜態(tài)分配
在前面討論的分配例子钟病,不少可以分配到棧上萧恕。用戶空間可以奢侈地負擔很大的棧,而且棾澹空間還可以動態(tài)增長票唆,相反內(nèi)核空間不能——棧小而固定。給每個進程分配一個固定小棧屹徘,可以減小內(nèi)存消耗和棧管理任務負擔走趋。
進程的內(nèi)核棧大小既依賴體系結(jié)構(gòu),也和編譯時的選項有關噪伊。在任何一個函數(shù)中簿煌,都必須盡量節(jié)省棧資源。讓函數(shù)所有局部變量之后不要超過幾百字節(jié)(棧上分配大量的靜態(tài)分配是不理智的)鉴吹,棧溢出就會覆蓋掉臨近堆棧末端的數(shù)據(jù)姨伟。首先就是前面講的thread_info
。
8.每個CPU使用數(shù)據(jù)
支持SMP的操作系統(tǒng)使用每個CPU上的數(shù)據(jù)豆励,對于給定的處理器其數(shù)據(jù)是唯一的夺荒。一般而言,每個CPU的數(shù)據(jù)存放在一個數(shù)組內(nèi),數(shù)組中的每一項對應著系統(tǒng)上一個存在的處理器技扼,安裝當前處理器號就能確定這個數(shù)組的當前元素伍玖。
在Linux中引入了新的操作接口稱為percpu
,頭文件<linux/percpu.h>
聲明了所有接口操作例程剿吻,可以在文件mm/slab.c
和<asm/percpu.h>
找到定義窍箍。
使用每個CPU數(shù)據(jù)的好處是:
- 減少了數(shù)據(jù)鎖定
- 大大減少了緩存失效,一個CPU操作另一個CPU的數(shù)據(jù)時和橙,必須清理另一個CPU的緩存并刷新仔燕,存在不斷的緩存失效。持續(xù)不斷的緩存失效稱為緩存抖動魔招。
這種方式的唯一安全要求就是禁止內(nèi)核搶占晰搀,同時注意進程在訪問每個CPU數(shù)據(jù)過程中不能睡眠——否則,喚醒之后可能已經(jīng)到其他處理器上了办斑。