深入分析Linux內核源代碼6-Linux 內存管理(2)

在這裏插入圖片描述每天十五分鐘,熟讀一個技術點,水滴石穿,一切只爲渴望更優秀的你!

————零聲學院

6.3 內存的分配和回收

在內存初始化完成以後,內存中就常駐有內核映像(內核代碼和數據)。以後,隨着用
戶程序的執行和結束,就需要不斷地分配和釋放物理頁面。內核應該爲分配一組連續的頁面
而建立一種穩定、高效的分配策略。爲此,必須解決一個比較重要的內存管理問題,即外碎
片問題。頻繁地請求和釋放不同大小的一組連續頁面,必然導致在已分配的內存塊中分散許
多小塊的空閒頁面。由此帶來的問題是,即使這些小塊的空閒頁面加起來足以滿足所請求的
頁面,但是要分配一個大塊的連續頁面可能就根本無法滿足。Linux 採用著名的夥伴(Buddy)
系統算法來解決外碎片問題。
但是請注意,在 Linux 中,CPU 不能按物理地址來訪問存儲空間,而必須使用虛擬地址;
因此,對於內存頁面的管理,通常是先在虛存空間中分配一個虛存區間,然後才根據需要爲
此區間分配相應的物理頁面並建立起映射,也就是說,虛存區間的分配在前,而物理頁面的
分配在後,但是爲了承接上一節的內容,我們先介紹內存的分配和回收,然後再介紹用戶進
程虛存區間的建立。

6.3.1 夥伴算法

1.原理
Linux 的夥伴算法把所有的空閒頁面分爲 10 個塊組,每組中塊的大小是 2 的冪次方個頁
面,例如,第 0 組中塊的大小都爲 20 (1 個頁面),第 1 組中塊的大小都爲 21
(2 個頁面),
第 9 組中塊的大小都爲 29
(512 個頁面)。也就是說,每一組中塊的大小是相同的,且這同樣
大小的塊形成一個鏈表。
我們通過一個簡單的例子來說明該算法的工作原理。
假設要求分配的塊的大小爲 128 個頁面(由多個頁面組成的塊我們就叫做頁面塊)。該
算法先在塊大小爲 128 個頁面的鏈表中查找,看是否有這樣一個空閒塊。如果有,就直接分
配;如果沒有,該算法會查找下一個更大的塊,具體地說,就是在塊大小 256 個頁面的鏈表
中查找一個空閒塊。如果存在這樣的空閒塊,內核就把這 256 個頁面分爲兩等份,一份分配
出去,另一份插入到塊大小爲 128 個頁面的鏈表中。如果在塊大小爲 256 個頁面的鏈表中也
沒有找到空閒頁塊,就繼續找更大的塊,即 512 個頁面的塊。如果存在這樣的塊,內核就從
512 個頁面的塊中分出 128 個頁面滿足請求,然後從 384 個頁面中取出 256 個頁面插入到塊
大小爲 256 個頁面的鏈表中。然後把剩餘的 128 個頁面插入到塊大小爲 128 個頁面的鏈表中。
如果 512 個頁面的鏈表中還沒有空閒塊,該算法就放棄分配,併發出出錯信號。
以上過程的逆過程就是塊的釋放過程,這也是該算法名字的來由。滿足以下條件的兩個
塊稱爲夥伴:
(1)兩個塊的大小相同;
(2)兩個塊的物理地址連續。
夥伴算法把滿足以上條件的兩個塊合併爲一個塊,該算法是迭代算法,如果合併後的塊
還可以跟相鄰的塊進行合併,那麼該算法就繼續合併。
2.數據結構
在 6.2.6 節中所介紹的管理區數據結構 struct zone_struct 中,涉及到空閒區數據結
構:
free_area_t free_area[MAX_ORDER];
我們再次對 free_area_t 給予較詳細的描述。

#difine MAX_ORDER 10 
 type struct free_area_struct { 
 struct list_head free_list 
 unsigned int *map 
 } free_area_t 

其中 list_head 域是一個通用的雙向鏈表結構,鏈表中元素的類型將爲 mem_map_t(即
struct page 結構)。Map 域指向一個位圖,其大小取決於現有的頁面數。free_area 第 k 項
位圖的每一位,描述的就是大小爲 2k
個頁面的兩個夥伴塊的狀態。如果位圖的某位爲 0,表
示一對兄弟塊中或者兩個都空閒,或者兩個都被分配,如果爲 1,肯定有一塊已被分配。當
兄弟塊都空閒時,內核把它們當作一個大小爲 2k+1的單獨快來處理。如圖 6.9 給出該數據結
構的示意圖。
在這裏插入圖片描述
圖 6.9 中,free_aea 數組的元素 0 包含了一個空閒頁(頁面編號爲 0);而元素 2 則包
含了兩個以 4 個頁面爲大小的空閒頁面塊,第一個頁面塊的起始編號爲 4,而第二個頁面塊
的起始編號爲 56。
我們曾提到,當需要分配若干個內存頁面時,用於 DMA 的內存頁面必須是連續的。其實
爲了便於管理,從夥伴算法可以看出,只要請求分配的塊大小不超過 512 個頁面(2KB),內
核就儘量分配連續的頁面。

6.3.2 物理頁面的分配和釋放

當一個進程請求分配連續的物理頁面時,可以通過調用 alloc_pages()來完成。Linux 2.4
版本中有兩個 alloc_pages(),一個在 mm/numa.c 中,另一個在 mm/page_alloc,c 中,編譯
時根據所定義的條件選項 CONFIG_DISCONTIGMEM 來進行取捨。
1.非一致存儲結構(NUMA)中頁面的分配
CONFIG_DISCONTIGMEM 條件編譯的含義是“不連續的存儲空間”,Linux 把不連續的存儲
空間也歸類爲非一致存儲結構(NUMA)。這是因爲,不連續的存儲空間本質上是一種廣義的
NUMA,因爲那說明在最低物理地址和最高物理地址之間存在着空洞,而有空洞的空間當然是
“不一致”的。所以,在地址不連續的物理空間也要像結構不一樣的物理空間那樣劃分出若干
連續且均勻的“節點”。因此,在存儲結構不連續的系統中,每個模塊都有若干個節點,因而
都有個 pg_data_t 數據結構隊列。我們先來看 mm/numa.c 中的 alloc_page()函數:

/* 
 * This can be refined. Currently, tries to do round robin, instead 
 * should do concentratic circle search, starting from current node. 
 */ 
struct page * _alloc_pages(unsigned int gfp_mask, unsigned int order) 
{ 
 struct page *ret = 0; 
 pg_data_t *start, *temp; 
#ifndef CONFIG_NUMA 
 unsigned long flags; 
 static pg_data_t *next = 0; 
#endif 
 if (order >= MAX_ORDER) 
 return NULL; 
#ifdef CONFIG_NUMA 
 temp = NODE_DATA(numa_node_id()); 
#else 
 spin_lock_irqsave(&node_lock, flags); 
 if!next) next = pgdat_list; 
 temp = next; 
 next = next->node_next; 
 spin_unlock_irqrestore(&node_lock, flags); 
#endif 
 start = temp; 
 while (temp) { 
 if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) 
 return(ret); 
 temp = temp->node_next; 
 } 
 temp = pgdat_list; 
 while (temp != start) { 
 if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) 
 return(ret); 
 temp = temp->node_next; 
 } 
 return(0); 
 } 

對該函數的說明如下。
該函數有兩個參數。gfp_mask 表示採用哪種分配策略。參數 order 表示所需物理塊的大
小,可以是 1、2、3 直到 2MAX_ORDER-1。
如果定義了 CONFIG_NUMA,也就是在 NUMA 結構的系統中,可以通過 NUMA_DATA()宏找
到 CPU 所在節點的 pg_data_t 數據結構隊列,並存放在臨時變量 temp 中。
如果在不連續的 UMA 結構中,則有個 pg_data_t 數據結構的隊列 pgdat_list,pgdat_list
就是該隊列的首部。因爲隊列一般都是臨界資源,因此,在對該隊列進行兩個以上的操作時
要加鎖。
分配時輪流從各個節點開始,以求各節點負荷的平衡。函數中有兩個循環,其形式基本
相同,也就是,對節點隊列基本進行兩遍掃描,直至在某個節點內分配成功,則跳出循環,
否則,則徹底失敗,從而返回 0。對於每個節點,調用 alloc_pages_pgdat()函數試圖分配
所需的頁面。
2.一致存儲結構(UMA)中頁面的分配
連續空間 UMA 結構的 alloc_page()是在 include/linux/mm.h 中定義的:

#ifndef CONFIG_DISCONTIGMEM 
 static inline struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) 
{ 
 /* 
 * Gets optimized away by the compiler. 
 */ 
 if (order >= MAX_ORDER) 
 return NULL; 
 return __alloc_pages(gfp_mask, order, 
 contig_page_data.node_zonelists+(gfp_mask & GFP_ZONEMASK)); 
 } 
 #endif 

從這個函數的定義可以看出, alloc_page()是 _alloc_pages()的封裝函數,而
_alloc_pages()纔是夥伴算法的核心。這個函數定義於 mm/page_alloc.c 中,我們先對此
函數給予概要描述。
_alloc_pages()在管理區鏈表 zonelist 中依次查找每個區,從中找到滿足要求的區,
然後用夥伴算法從這個區中分配給定大小(2 order個)的頁面塊。如果所有的區都沒有足夠的
空閒頁面,則調用 swapper 或 bdflush 內核線程,把髒頁寫到磁盤以釋放一些頁面。
在__alloc_pages()和虛擬內存(簡稱 VM)的代碼之間有一些複雜的接口(後面會詳細
描述)。每個區都要對剛剛被映射到某個進程 VM 的頁面進行跟蹤,被映射的頁面也許僅僅做
了標記,而並沒有真正地分配出去。因爲根據虛擬存儲的分配原理,對物理頁面的分配要盡
量推遲到不能再推遲爲止,也就是說,當進程的代碼或數據必須裝入到內存時,纔給它真正
分配物理頁面。
搞清楚頁面分配的基本原則後,我們對其代碼具體分析如下:

/* 
 * This is the 'heart' of the zoned buddy allocator: 
 */ 
struct page * __alloc_pages(unsigned int gfp_mask, unsigned int order, zonelist_t *zonelist)
{ 
 unsigned long min; 
 zone_t **zone, * classzone; 
 struct page * page;
 int freed; 
 zone = zonelist->zones; 
 classzone = *zone; 
 min = 1UL << order; 
 for;;{ 
 zone_t *z = *(zone++); 
 if!z) 
 break; 
 min += z->pages_low; 
 if (z->free_pages > min) { 
 page = rmqueue(z, order); 
 if (page) 
 return page; 
 } 
 } 

這是對一個分配策略中所規定的所有頁面管理區的循環。循環中依次考察各個區中空閒
頁面的總量,如果總量尚大於“最低水位線”與所請求頁面數之和,就調用 rmqueue()試
圖從該區中進行分配。如果分配成功,則返回一個 page 結構指針,指向頁面塊中第一個頁面
的起始地址。

 classzone->need_balance = 1; 
 mb(); 
 if (waitqueue_active(&kswapd_wait)) 
 wake_up_interruptible(&kswapd_wait); 

如果發現管理區中的空閒頁面總量已經降到最低點,則把 zone_t 結構中需要重新平衡
的標誌(need_balance)置 1,而且如果內核線程 kswapd 在一個等待隊列中睡眠,就喚醒它,
讓它收回一些頁面以備使用(可以看出,need_balance 是和 kswapd 配合使用的)。

 zone = zonelist->zones; 
 min = 1UL << order; 
 for;;{ 
 unsigned long local_min; 
 zone_t *z = *(zone++); 
 if!z) 
 break; 
 local_min = z->pages_min; 
 if!(gfp_mask & __GFP_WAIT)) 
 local_min >>= 2; 
 min += local_min; 
 if (z->free_pages > min) { 
 page = rmqueue(z, order); 
 if (page) 
 return page; 
 } 
 } 

如果給定分配策略中所有的頁面管理區都分配失敗,那隻好把原來的“最低水位”再向
下調(除以 4),然後看是否滿足要求(z->free_pages > min),如果能滿足要求,則調用 rmqueue
()進行分配。

 /* here we're in the low on memory slow path */ 
rebalance: 
 if (current->flags & (PF_MEMALLOC | PF_MEMDIE)) { 
 zone = zonelist->zones; 
 for;;{ 
 zone_t *z = *(zone++); 
 if!z) 
 break; 
 page = rmqueue(z, order); 
 if (page) 
 return page; 
 } 
 return NULL; 
 } 

如果分配還不成功,這時候就要看是哪類進程在請求分配內存頁面。其中 PF_MEMALLOC
和 PF_MEMDIE 是進程的 task_struct 結構中 flags 域的值,對於正在分配頁面的進程(如
kswapd 內核線程),則其 PF_MEMALLOC 的值爲 1(一般進程的這個標誌爲 0),而對於使內存
溢出而被殺死的進程,則其 PF_MEMDIE 爲 1。不管哪種情況,都說明必須給該進程分配頁面
(想想爲什麼)。因此,繼續進行分配。

 /* Atomic allocations - we can't balance anything */ 
 if!(gfp_mask & __GFP_WAIT)) 
 return NULL; 

如果請求分配頁面的進程不能等待,也不能被重新調度,只好在沒有分配到頁面的情況
下“空手”返回。

 page = balance_classzone(classzone, gfp_mask, order, &freed); 
 if (page) 
 return page; 

如果經過幾番努力,必須得到頁面的進程(如 kswapd)還沒有分配到頁面,就要調用
balance_classzone()函數把當前進程所佔有的局部頁面釋放出來。如果釋放成功,則返回
一個 page 結構指針,指向頁面塊中第一個頁面的起始地址。

 zone = zonelist->zones; 
 min = 1UL << order; 
 for;;{ 
 zone_t *z = *(zone++); 
 if!z) 
 break; 
 min += z->pages_min; 
 if (z->free_pages > min) { 
 page = rmqueue(z, order); 
 if (page) 
 return page; 
 } 
 } 

繼續進行分配。

/* Don't let big-order allocations loop * 
 if (order > 3) 
 return NULL; 
 /* Yield for kswapd, and try again */ 
 current->policy |= SCHED_YIELD; 
 __set_current_state(TASK_RUNNING); 
 schedule(); 
 goto rebalance; 
 } 

在這個函數中,頻繁調用了 rmqueue()函數,下面我們具體來看一下這個函數內容。
(1)rmqueue()函數
該函數試圖從一個頁面管理區分配若干連續的內存頁面。這是最基本的分配操作,其具
體代碼如下:

static struct page * rmqueue(zone_t *zone, unsigned int order) 
{ 
 free_area_t * area = zone->free_area + order; 
 unsigned int curr_order = order; 
 struct list_head *head, *curr; 
 unsigned long flags; 
 struct page *page; 
 spin_lock_irqsave(&zone->lock, flags); 
 do { 
 head = &area->free_list; 
 curr = memlist_next(head); 
 if (curr != head) { 
 unsigned int index; 
 page = memlist_entry(curr, struct page, list); 
 if (BAD_RANGE(zone,page)) 
 BUG(); 
 memlist_del(curr); 
 index = page - zone->zone_mem_map; 
 if (curr_order != MAX_ORDER-1) 
 MARK_USED(index, curr_order, area); 
 zone->free_pages -= 1UL << order; 
 page = expand(zone, page, index, order, curr_order, area); 
 spin_unlock_irqrestore(&zone->lock, flags); 
 set_page_count(page, 1); 
 if (BAD_RANGE(zone,page)) 
 BUG(); 
 if (PageLRU(page)) 
 BUG(); 
 if (PageActive(page)) 
 BUG(); 
 return page;
 } 
 curr_order++; 
 area++; 
 } while (curr_order < MAX_ORDER); 
 spin_unlock_irqrestore(&zone->lock, flags); 
 return NULL; 
 } 

對該函數的解釋如下。
參數 zone 指向要分配頁面的管理區,order 表示要求分配的頁面數爲 2 order

do 循環從 free_area 數組的第 order 個元素開始,掃描每個元素中由 page 結構組成的
雙向循環空閒隊列。如果找到合適的頁塊,就把它從隊列中刪除,刪除的過程是不允許其他
進程、其他處理器來打擾的。所以要用 spin_lock_irqsave()將這個循環加上鎖。
首先在恰好滿足大小要求的隊列裏進行分配。其中 memlist_entry(curr, struct page,
list)獲得空閒塊的第 1 個頁面的地址,如果這個地址是個無效的地址,就陷入 BUG()。如果
有效,memlist_del(curr)從隊列中摘除分配出去的頁面塊。如果某個頁面塊被分配出去,就
要在 frea_area 的位圖中進行標記,這是通過調用 MARK_USED()宏來完成的。
如果分配出去後還有剩餘塊,就通過 expand()獲得所分配的頁塊,而把剩餘塊鏈入適
當的空閒隊列中。
如果當前空閒隊列沒有空閒塊,就從更大的空閒塊隊列中找。
(2)expand()函數
該函數源代碼如下。

 static inline struct page * expand (zone_t *zone, struct page *page, 
 unsigned long index, int low, int high, free_area_t * area) 
 { 
 unsigned long size = 1 << high; 
 while (high > low) { 
 if (BAD_RANGE(zone,page)) 
 BUG(); 
 area--; 
 high--; 
 size >>= 1; 
 memlist_add_head(&(page)->list, &(area)->free_list); 
 MARK_USED(index, high, area); 
 index += size; 
 page += size; 
 } 
 if (BAD_RANGE(zone,page)) 
 BUG(); 
 return page; 
 } 

對該函數解釋如下。
參數 zone 指向已分配頁塊所在的管理區;page 指向已分配的頁塊;index 是已分配的
頁面在 mem_map 中的下標; low 表示所需頁面塊大小爲 2 low,而 high 表示從空閒隊列中實
際進行分配的頁面塊大小爲 2 high;area 是 free_area_struct 結構,指向實際要分配的頁塊。
通過上面介紹可以知道,返回給請求者的塊大小爲 2low 個頁面,並把剩餘的頁面放入合
適的空閒隊列,且對夥伴系統的位圖進行相應的修改。例如,假定我們需要一個 2 頁面的塊,
但是,我們不得不從 order 爲 3(8 個頁面)的空閒隊列中進行分配,又假定我們碰巧選擇物
理頁面 800 作爲該頁面塊的底部。在我們這個例子中,這幾個參數值爲:
page == mem_map+800
index == 800
low == 1
high == 3
area == zone->free_area+high ( 也就是 frea_area 數組中下標爲 3 的元素)
首先把 size 初始化爲分配塊的頁面數(例如,size = 1<<3 == 8)
while 循環進行循環查找。每次循環都把 size 減半。如果我們從空閒隊列中分配的一個
塊與所要求的大小匹配,那麼 low = high,就徹底從循環中跳出,返回所分配的頁塊。
如果分配到的物理塊所在的空閒塊大於所需塊的大小(即 2 high>2low),那就將該空閒塊
分爲兩半(即 area–;high–; size >>= 1),然後調用 memlist_add_head()把剛分配出去
的頁面塊又加入到低一檔(物理塊減半)的空閒隊列中,準備從剩下的一半空閒塊中重新進
行分配,並調用 MARK_USED()設置位圖。
在上面的例子中,第 1 次循環,我們從頁面 800 開始,把頁面大小爲 4(即 2high–)的塊
其首地址插入到 frea_area[2]中的空閒隊列;因爲 low<high,又開始第 2 次循環,這次從頁
面 804 開始,把頁面大小爲 2 的塊插入到 frea_area[1]中的空閒隊列,此時,page=806,
high=low=1,退出循環,我們給調用者返回從 806 頁面開始的一個 2 頁面塊。
從這個例子可以看出,這是一種巧妙的分配算法。
3.釋放頁面
從上面的介紹可以看出,頁面塊的分配必然導致內存的碎片化,而頁面塊的釋放則可以
將頁面塊重新組合成大的頁面塊。頁面的釋放函數爲__free_pages(page struct *page,
unsigned long order) ,該函數從給定的頁面開始,釋放的頁面塊大小爲 2order。原函數爲:

void __free_pages(page struct *page, unsigned long order) 
{ 
 if!PageReserved(page) && put_page_testzero(page)) 
 __free_pages_ok(page, order); 
} 

其中比較巧妙的部分就是調用 put_page_testzero()宏,該函數把頁面的引用計數減 1,
如果減 1 後引用計數爲 0,則該函數返回 1。因此,如果調用者不是該頁面的最後一個用戶,
那麼,這個頁面實際上就不會被釋放。另外要說明的是不可釋放保留頁 PageReserved,這是
通過 PageReserved()宏進行檢查的。
如果調用者是該頁面的最後一個用戶,則__free_pages() 再調用 __free_pages_ok()。
__free_pages_ok()纔是對頁面塊進行釋放的實際函數,該函數把釋放的頁面塊鏈入空閒鏈
表,並對夥伴系統的位圖進行管理,必要時合併夥伴塊。這實際上是 expand()函數的反操作,
我們對此不再進行詳細的討論。

6.3.3 Slab 分配機制

採用夥伴算法分配內存時,每次至少分配一個頁面。但當請求分配的內存大小爲幾十個
字節或幾百個字節時應該如何處理?如何在一個頁面中分配小的內存區,小內存區的分配所
產生的內碎片又如何解決?
Linux 2.0 採用的解決辦法是建立了 13 個空閒區鏈表,它們的大小從 32 字節到 132056
字節。從 Linux 2.2 開始,MM 的開發者採用了一種叫做 Slab 的分配模式,該模式早在 1994
年就被開發出來,用於 Sun Microsystem Solaris 2.4 操作系統中。Slab 的提出主要是基於
以下考慮。
內核對內存區的分配取決於所存放數據的類型。例如,當給用戶態進程分配頁面時,內
核調用 get_free_page()函數,並用 0 填充這個頁面。而給內核的數據結構分配頁面時,事
情沒有這麼簡單,例如,要對數據結構所在的內存進行初始化、在不用時要收回它們所佔用
的內存。因此,Slab 中引入了對象這個概念,所謂對象就是存放一組數據結構的內存區,其
方法就是構造或析構函數,構造函數用於初始化數據結構所在的內存區,而析構函數收回相
應的內存區。但爲了便於理解,你也可以把對象直接看作內核的數據結構。爲了避免重複初
始化對象,Slab 分配模式並不丟棄已分配的對象,而是釋放但把它們依然保留在內存中。當
以後又要請求分配同一對象時,就可以從內存獲取而不用進行初始化,這是在 Solaris 中引
入 Slab 的基本思想。
實際上,Linux 中對 Slab 分配模式有所改進,它對內存區的處理並不需要進行初始化或
回收。出於效率的考慮,Linux 並不調用對象的構造或析構函數,而是把指向這兩個函數的
指針都置爲空。Linux 中引入 Slab 的主要目的是爲了減少對夥伴算法的調用次數。
實際上,內核經常反覆使用某一內存區。例如,只要內核創建一個新的進程,就要爲該
進程相關的數據結構(task_struct、打開文件對象等)分配內存區。當進程結束時,收回這
些內存區。因爲進程的創建和撤銷非常頻繁,因此,Linux 的早期版本把大量的時間花費在
反覆分配或回收這些內存區上。從 Linux 2.2 開始,把那些頻繁使用的頁面保存在高速緩存
中並重新使用。
可以根據對內存區的使用頻率來對它分類。對於預期頻繁使用的內存區,可以創建一組
特定大小的專用緩衝區進行處理,以避免內碎片的產生。對於較少使用的內存區,可以創建
一組通用緩衝區(如 Linux 2.0 中所使用的 2 的冪次方)來處理,即使這種處理模式產生碎
片,也對整個系統的性能影響不大。
硬件高速緩存的使用,又爲儘量減少對夥伴算法的調用提供了另一個理由,因爲對夥伴
算法的每次調用都會“弄髒”硬件高速緩存,因此,這就增加了對內存的平均訪問次數。
Slab 分配模式把對象分組放進緩衝區(儘管英文中使用了 Cache 這個詞,但實際上指的
是內存中的區域,而不是指硬件高速緩存)。因爲緩衝區的組織和管理與硬件高速緩存的命中
率密切相關,因此,Slab 緩衝區並非由各個對象直接構成,而是由一連串的“大塊(Slab)”
構成,而每個大塊中則包含了若干個同種類型的對象,這些對象或已被分配,或空閒,如圖
6.10 所示。一般而言,對象分兩種,一種是大對象,一種是小對象。所謂小對象,是指在一
在這裏插入圖片描述
個頁面中可以容納下好幾個對象的那種。例如,一個 inode 結構大約佔 300 多個字節,因此,
一個頁面中可以容納 8 個以上的 inode 結構,因此,inode 結構就爲小對象。Linux 內核中把
小於 512 字節的對象叫做小對象。
實際上,緩衝區就是主存中的一片區域,把這片區域劃分爲多個塊,每塊就是一個 Slab,
每個 Slab 由一個或多個頁面組成,每個 Slab 中存放的就是對象。
因爲 Slab 分配模式的實現比較複雜,我們不準備對其進行詳細的分析,只對主要內容
給予描述。
1.Slab 的數據結構
Slab 分配模式有兩個主要的數據結構,一個是描述緩衝區的結構 kmem_cache_t,一個
是描述 Slab 的結構 kmem_slab_t,下面對這兩個結構給予簡要討論。
(1)Slab
Slab 是 Slab 管理模式中最基本的結構。它由一組連續的物理頁面組成,對象就被順序
放在這些頁面中。其數據結構在 mm/slab.c 中定義如下:

 /* 
 * slab_t 
 * 
 * Manages the objs in a slab. Placed either at the beginning of mem allocated 
 * for a slab, or allocated from an general cache. 
 * Slabs are chained into three list: fully used, partial, fully free slabs. 
 */ 
 typedef struct slab_s { 
 struct list_head list; 
 unsigned long colouroff; 
 void *s_mem; /* including colour offset */ 
 unsigned int inuse; /* num of objs active in slab */ 
 kmem_bufctl_t free; 
 } slab_t; 

這裏的鏈表用來將前一個 Slab 和後一個 Slab 鏈接起來形成一個雙向鏈表,colouroff
爲該 Slab 上着色區的大小,指針 s_mem 指向對象區的起點,inuse 是 Slab 中所分配對象的
個數。最後,free 的值指明瞭空閒對象鏈中的第一個對象,kmem_bufctl_t 其實是一個整數。
Slab 結構的示意圖如圖 6.11 所示。
對於小對象,就把 Slab 的描述結構 slab_t 放在該 Slab 中;對於大對象,則把 Slab 結
構遊離出來,集中存放。關於 Slab 中的着色區再給予具體描述。
每個 Slab 的首部都有一個小小的區域是不用的,稱爲“着色區(Coloring Area)”。
着色區的大小使 Slab 中的每個對象的起始地址都按高速緩存中的“緩存行(Cache Line)”
大小進行對齊(80386 的一級高速緩存行大小爲 16 字節,Pentium 爲 32 字節)。因爲 Slab
是由 1 個頁面或多個頁面(最多爲 32)組成,因此,每個 Slab 都是從一個頁面邊界開始的,
它自然按高速緩存的緩衝行對齊。但是,Slab 中的對象大小不確定,設置着色區的目的就是
將 Slab 中第一個對象的起始地址往後推到與緩衝行對齊的位置。因爲一個緩衝區中有多個
Slab,因此,應該把每個緩衝區中的各個 Slab 着色區的大小盡量安排成不同的大小,這樣可
以使得在不同的 Slab 中,處於同一相對位置的對象,讓它們在高速緩存中的起始地址相互錯
開,這樣就可以改善高速緩存的存取效率。
在這裏插入圖片描述
每個 Slab 上最後一個對象以後也有個小小的廢料區是不用的,這是對着色區大小的補
償,其大小取決於着色區的大小,以及 Slab 與其每個對象的相對大小。但該區域與着色區的
總和對於同一種對象的各個 Slab 是個常數。
每個對象的大小基本上是所需數據結構的大小。只有當數據結構的大小不與高速緩存中
的緩衝行對齊時,才增加若干字節使其對齊。所以,一個 Slab 上的所有對象的起始地址都必
然是按高速緩存中的緩衝行對齊的。
(2)緩衝區
每個緩衝區管理着一個 Slab 鏈表,Slab 按序分爲 3 組。第 1 組是全滿的 Slab(沒有空
閒的對象),第 2 組 Slab 中只有部分對象被分配,部分對象還空閒,最後一組 Slab 中的對象
全部空閒。只所以這樣分組,是爲了對 Slab 進行有效的管理。每個緩衝區還有一個輪轉鎖
(Spinlock),在對鏈表進行修改時用這個輪轉鎖進行同步。類型 kmem_cache_s 在 mm/slab.c
中定義如下:

 struct kmem_cache_s { 
/* 1) each alloc & free */ 
 /* full, partial first, then free */ 
 struct list_head slabs_full; 
 struct list_head slabs_partial; 
 struct list_head slabs_free; 
 unsigned int objsize; 
 unsigned int flags; /* constant flags */ 
 unsigned int num; /* # of objs per slab */ 
 spinlock_t spinlock; 
#ifdef CONFIG_SMP 
 unsigned int batchcount; 
#endif 
/* 2) slab additions /removals */ 
 /* order of pgs per slab (2^n) */ 
 unsigned int gfporder; 
 /* force GFP flags, e.g. GFP_DMA */ 
 unsigned int gfpflags; 
 size_t colour; /* cache colouring range */ 
 unsigned int colour_off; /* colour offset */ 
 unsigned int colour_next; /* cache colouring */ 
 kmem_cache_t *slabp_cache; 
 unsigned int growing; 
 unsigned int dflags; /* dynamic flags */ 
 /* constructor func */ 
 void (*ctor)(void *, kmem_cache_t *, unsigned long); 
 /* de-constructor func */ 
 void (*dtor)(void *, kmem_cache_t *, unsigned long); 
 unsigned long failures; 
/* 3) cache creation/removal */ 
 char name[CACHE_NAMELEN]; 
 struct list_head next; 
#ifdef CONFIG_SMP 
/* 4) per-cpu data */ 
 cpucache_t *cpudata[NR_CPUS]; 
#endif .. 
}; 

然後定義了 kmem_cache_t,並給部分域賦予了初值:

static kmem_cache_t cache_cache = { 
 slabs_full: LIST_HEAD_INIT(cache_cache.slabs_full), 
 slabs_partial: LIST_HEAD_INIT(cache_cache.slabs_partial), 
 slabs_free: LIST_HEAD_INIT(cache_cache.slabs_free),
 objsize: sizeof(kmem_cache_t), 
 flags: SLAB_NO_REAP, 
 spinlock: SPIN_LOCK_UNLOCKED, 
 colour_off: L1_CACHE_BYTES, 
 name: "kmem_cache", 
}; 

對該結構說明如下。
該結構中有 3 個隊列 slabs_full、slabs_partial 以及 slabs_free,分別指向滿 Slab、
半滿 Slab 和空閒 Slab,另一個隊列 next 則把所有的專用緩衝區鏈成一個鏈表。
除了這些隊列和指針外,該結構中還有一些重要的域:objsize 是原始的數據結構的大
小,這裏初始化爲 kmem_cache_t 的大小;num 表示每個 Slab 上有幾個緩衝區;gfporder 則
表示每個 Slab 大小的對數,即每個 Slab 由 2 gfporder個頁面構成。
如前所述,着色區的使用是爲了使同一緩衝區中不同 Slab 上的對象區的起始地址相互
錯開,這樣有利於改善高速緩存的效率。colour_off 表示顏色的偏移量,colour 表示顏色的
數量;一個緩衝區中顏色的數量取決於 Slab 中對象的個數、剩餘空間以及高速緩存行的大小。
所以,對每個緩衝區都要計算它的顏色數量,這個數量就保存在 colour 中,而下一個 Slab
將要使用的顏色則保存在 colour_next 中。當 colour_next 達到最大值時,就又從 0 開始。
着色區的大小可以根據(colour_off×colour)算得。例如,如果 colour 爲 5,colour_off
爲 8,則第一個 Slab 的顏色將爲 0,Slab 中第一個對象區的起始地址(相對)爲 0,下一個
Slab 中第一個對象區的起始地址爲 8,再下一個爲 16,24,32,0……等。
cache_cache 變量實際上就是緩衝區結構的頭指針。
由此可以看出,緩衝區結構 kmem_cache_t 相當於 Slab 的總控結構,緩衝區結構與 Slab
結構之間的關係如圖 6.12 所示。
在圖 6.12 中,深灰色表示全滿的 Slab,淺灰色表示含有空閒對象的 Slab,而無色表示
空的 Slab。緩衝區結構之間形成一個單向鏈表,Slab 結構之間形成一個雙向鏈表。另外,緩
衝區結構還有分別指向滿、半滿、空閒 Slab 結構的指針。
2.專用緩衝區的建立和撤銷
專用緩衝區是通過 kmem_cache_create()函數建立的,函數原型爲:
kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset,
unsigned long c_flags,
void (*ctor) (void *objp, kmem_cache_t *cachep, unsigned long flags),
void (*dtor) (void *objp, kmem_cache_t *cachep, unsigned long flags))
在這裏插入圖片描述
對其參數說明如下。
(1)name: 緩衝區名 ( 19 個字符)。
(2)size: 對象大小。
(3)offset :所請求的着色偏移量。
(4)c_flags :對緩衝區的設置標誌。
• SLAB_HWCACHE_ALIGN:表示與第一個高速緩存中的緩衝行邊界(16 或 32 字節)對齊。
• SLAB_NO_REAP:不允許系統回收內存。
• SLAB_CACHE_DMA:表示 Slab 使用的是 DMA 內存。
(5)ctor :構造函數(一般都爲 NULL)。
(6)dtor :析構函數(一般都爲 NULL)。
(7)objp :指向對象的指針。
(8)cachep :指向緩衝區。
對專用緩衝區的創建過程簡述如下。
kmem_cache_create()函數要進行一系列的計算,以確定最佳的 Slab 構成。包括:每
個 Slab 由幾個頁面組成,劃分爲多少個對象;Slab 的描述結構 slab_t 應該放在 Slab 的外
面還是放在 Slab 的尾部;還有“顏色”的數量等等。並根據調用參數和計算結果設置
kmem_cache_t 結構中的各個域,包括兩個函數指針 ctor 和 dtor。最後,將 kmem_cache_t
結構插入到 cache_cache 的 next 隊列中。
但請注意,函數 kmem_cache_create()所創建的緩衝區中還沒有包含任何 Slab,因此,
也沒有空閒的對象。只有以下兩個條件都爲真時,纔給緩衝區分配 Slab:
(1)已發出一個分配新對象的請求;
(2)緩衝區不包含任何空閒對象。
當這兩個條件都成立時,Slab 分配模式就調用 kmem_cache_grow()函數給緩衝區分配
一個新的 Slab。其中,該函數調用 kmem_gatepages()從夥伴系統獲得一組頁面;然後又調用
kmem_cache_slabgmt()獲得一個新的 Slab結構;還要調用 kmem_cache_init_objs()爲新 Slab
中的所有對象申請構造方法(如果定義的話);最後,調用 kmem_slab_link_end()把這個 Slab
結構插入到緩衝區中 Slab 鏈表的末尾。
Slab 分配模式的最大好處就是給頻繁使用的數據結構建立專用緩衝區。但到目前的版本
爲止,Linux 內核中多數專用緩衝區的建立都用 NULL 作爲構造函數的指針,例如,爲虛存區
間結構 vm_area_struct 建立的專用緩衝區 vm_area_cachep:
vm_area_cachep = kmem_cache_create(“vm_area_struct”,
sizeof(struct vm_area_struct), 0,
SLAB_HWCACHE_ALIGN, NULL, NULL);
就把構造和析構函數的指針置爲 NULL,也就是說,內核並沒有充分利用 Slab 管理機制
所提供的好處。爲了說明如何利用專用緩衝區,我們從內核代碼中選取一個構造函數不爲空
的簡單例子,這個例子與網絡子系統有關,在 net/core/buff.c 中定義:

 void __init skb_init(void) 
{ 
 int i; 
 skbuff_head_cache = kmem_cache_create("skbuff_head_cache", 
 sizeof(struct sk_buff), 
 0, 
 SLAB_HWCACHE_ALIGN, 
 skb_headerinit, NULL); 
 if!skbuff_head_cache) 
 panic("cannot create skbuff cache"; 
 for (i=0; i<NR_CPUS; i++) 
 skb_queue_head_init(&skb_head_pool[i].list); 
 } 

從代碼中可以看出,skb_init()調用 kmem_cache_create()爲網絡子系統建立一個
sk_buff 數據結構的專用緩衝區,其名稱爲“skbuff_head_cache”( 你可以通過讀取/
proc/slabinfo/文件得到所有緩衝區的名字)。調用參數 offset 爲 0,表示第一個對象在 Slab
中的位移並無特殊要求。但是參數 flags 爲 SLAB_HWCACHE_ALIGN,表示 Slab 中的對象要與
高速緩存中的緩衝行邊界對齊。對象的構造函數爲 skb_headerinit(),而析構函數爲空,
也就是說,在釋放一個 Slab 時無需對各個緩衝區進行特殊的處理。
當從內核卸載一個模塊時,同時應當撤銷爲這個模塊中的數據結構所建立的緩衝區,這
是通過調用 kmem_cache_destroy()函數來完成的。從 Linux 2.4.16 內核代碼中進行查找可
知,對這個函數的調用非常少。
3.通用緩衝區
在內核中初始化開銷不大的數據結構可以合用一個通用的緩衝區。通用緩衝區非常類似
於物理頁面分配中的大小分區,最小的爲 32,然後依次爲 64、128、……直至 128KB(即 32
個頁面),但是,對通用緩衝區的管理又採用的是 Slab 方式。從通用緩衝區中分配和釋放緩
衝區的函數爲:
void *kmalloc(size_t size, int flags);
Void kree(const void *objp);
因此,當一個數據結構的使用根本不頻繁時,或其大小不足一個頁面時,就沒有必要給
其分配專用緩衝區,而應該調用 kmallo()進行分配。如果數據結構的大小接近一個頁面,則
乾脆通過 alloc_page()爲之分配一個頁面。
事實上,在內核中,尤其是驅動程序中,有大量的數據結構僅僅是一次性使用,而且所
佔內存只有幾十個字節,因此,一般情況下調用 kmallo()給內核數據結構分配內存就足夠了。
另外,因爲,在 Linux 2.0 以前的版本一般都調用 kmallo()給內核數據結構分配內存,因此,
調用該函數的一個優點是(讓你開發的驅動程序)能保持向後兼容。

6.3.4 內核空間非連續內存區的管理
我們說,任何時候,CPU 訪問的都是虛擬內存,那麼,在你編寫驅動程序,或者編寫模
塊時,Linux 給你分配什麼樣的內存?它處於 4GB 空間的什麼位置?這就是我們要討論的非
連續內存。
首先,非連續內存處於 3GB 到 4GB 之間,也就是處於內核空間,如圖 6.13 所示。
在這裏插入圖片描述
圖 6.13 中,PAGE_OFFSET 爲 3GB,high_memory 爲保存物理地址最高值的變量,
VMALLOC_START 爲非連續區的的起始地址,定義於 include/i386/pgtable.h 中:
#define VMALLOC_OFFSET (810241024)
#define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & ~
(VMALLOC_OFFSET-1))
在物理地址的末尾與第一個內存區之間插入了一個 8MB(VMALLOC_OFFSET)的區間,這
是一個安全區,目的是爲了“捕獲”對非連續區的非法訪問。出於同樣的理由,在其他非連
續的內存區之間也插入了 4KB 大小的安全區。每個非連續內存區的大小都是 4096 的倍數。
1.非連續區的數據結構
描述非連續區的數據結構爲 struct vm_struct,定義於 include/linux/vmalloc.h 中:

struct vm_struct { 
 unsigned long flags; 
 void * addr; 
 unsigned long size; 
 struct vm_struct * next; 
}; 

struct vm_struct * vmlist;
非連續區組成一個單鏈表,鏈表第一個元素的地址存放在變量 vmlist 中。Addr 域是內
存區的起始地址;size 是內存區的大小加 4096(安全區的大小)。
2.創建一個非連續區的結構
函數 get_vm_area()創建一個新的非連續區結構,其代碼在 mm/vmalloc.c 中:

 struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) 
 { 
 unsigned long addr; 
 struct vm_struct **p, *tmp, *area; 
 area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); 
 if!area) 
 return NULL; 
 size += PAGE_SIZE; 
 addr = VMALLOC_START; 
 write_lock(&vmlist_lock); 
 for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { 
 if ((size + addr) < addr) 
 goto out; 
 if (size + addr <= (unsigned long) tmp->addr) 
 break; 
 addr = tmp->size + (unsigned long) tmp->addr; 
 if (addr > VMALLOC_END-size) 
 goto out; 
 } 
 area->flags = flags; 
 area->addr = (void *)addr; 
 area->size = size; 
 area->next = *p; 
 *p = area; 
 write_unlock(&vmlist_lock); 
 return area; 
out: 
 write_unlock(&vmlist_lock); 
 kfree(area); 
 return NULL; 
 } 

這個函數比較簡單,就是在單鏈表中插入一個元素。其中調用了 kmalloc()和 kfree()
函數,分別用來爲 vm_struct 結構分配內存和釋放所分配的內存。
3.分配非連續內存區
vmalloc()函數給內核分配一個非連續的內存區,在/include/linux/vmalloc.h 中定
義如下:

 static inline void * vmalloc (unsigned long size) 
 { 
 return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); 
 }
 vmalloc()最終調用的是__vmalloc()函數,該函數的代碼在 mm/vmalloc.c 中: 
 void * __vmalloc (unsigned long size, int gfp_mask, pgprot_t prot) 
 { 
 void * addr; 
 struct vm_struct *area; 
 size = PAGE_ALIGN(size); 
 if!size || (size >> PAGE_SHIFT) > num_physpages) { 
 BUG(); 
 return NULL; 
 } 
 area = get_vm_area(size, VM_ALLOC); 
 if!area) 
 return NULL; 
 addr = area->addr; 
 if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) { 
 vfree(addr); 
 return NULL; 
 } 
 return addr; 
 } 

函數首先把 size 參數取整爲頁面大小(4096)的一個倍數,也就是按頁的大小進行對
齊,然後進行有效性檢查,如果有大小合適的可用內存,就調用 get_vm_area()獲得一個
內存區的結構。但真正的內存區還沒有獲得,函數 vmalloc_area_pages()真正進行非連續
內存區的分配:

 inline int vmalloc_area_pages (unsigned long address, unsigned long size, 
 int gfp_mask, pgprot_t prot) 
 { 
 pgd_t * dir; 
 unsigned long end = address + size; 
 int ret; 
 dir = pgd_offset_k(address); 
 spin_lock(&init_mm.page_table_lock); 
 do { 
 pmd_t *pmd; 
 
 pmd = pmd_alloc(&init_mm, dir, address); 
 ret = -ENOMEM; 
 if!pmd) 
 break; 
 ret = -ENOMEM; 
 if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) 
 break; 
 address = (address + PGDIR_SIZE) & PGDIR_MASK;
 dir++; 
 ret = 0; 
 } while (address && (address < end));
 spin_unlock(&init_mm.page_table_lock); 
 return ret; 
 } 

該函數有兩個主要的參數,address 表示內存區的起始地址,size 表示內存區的大小。
內存區的末尾地址賦給了局部變量 end。其中還調用了幾個主要的函數或宏。
(1)pgd_offset_k()宏導出這個內存區起始地址在頁目錄中的目錄項。
(2)pmd_alloc()爲新的內存區創建一箇中間頁目錄。
(3)alloc_area_pmd()爲新的中間頁目錄分配所有相關的頁表,並更新頁的總目錄;
該函數調用 pte_alloc_kernel()函數來分配一個新的頁表,之後再調用 alloc_area_pte()
爲頁表項分配具體的物理頁面。
(4)從 vmalloc_area_pages()函數可以看出,該函數實際建立起了非連續內存區到物
理頁面的映射。
4.kmalloc()與 vmalloc()的區別
kmalloc()與 vmalloc() 都是在內核代碼中提供給其他子系統用來分配內存的函數,但
二者有何區別?
從前面的介紹已經看出,這兩個函數所分配的內存都處於內核空間,即從 3GB~4GB;但位
置不同,kmalloc()分配的內存處於 3GB~high_memory 之間,而 vmalloc()分配的內存在
VMALLOC_START~4GB 之間,也就是非連續內存區。一般情況下在驅動程序中都是調用 kmalloc()
來給數據結構分配內存,而 vmalloc()用在爲活動的交換區分配數據結構,爲某些 I/O 驅動程
序分配緩衝區,或爲模塊分配空間,例如在 include/asm-i386/module.h 中定義瞭如下語句:
#define module_map(x) vmalloc(x)
其含義就是把模塊映射到非連續的內存區。
與 kmalloc()和 vmalloc()相對應,兩個釋放內存的函數爲 kfree()和 vfree()。

每日分享15分鐘技術摘要選讀,關注一波,一起保持學習動力!

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章