在前面的博文裏,我們講解了基於80x86體系的Linux內核分段和分頁機制,並詳細地討論了Linux的內存佈局。有了這些基本概念以後,我們就來詳細討論內核如何動態地管理那些可用的內存空間。
對於80386這種32位的處理器結構,Linux採用4KB頁框大小作爲標準的內存分配單元。內核必須記錄每個頁框的當前狀態,例如,區分哪些頁 框包含的是屬於進程的頁,而哪些頁框包含的是內核代碼或內核數據。內核還必須能夠確定動態內存中的頁框是否空閒,如果動態內存中的頁框不包含有用的數據, 那麼這個頁框就是空閒的。在以下情況下頁框是不空閒的:包含用戶態進程的數據、某個軟件高速緩存的數據、動態分配的內核數據結構、設備驅動程序緩衝的數 據、內核模塊的代碼等等。
內核用數據結構page描述一個頁框的狀態信息,所有的頁描述符存放在全局mem_map數組中,其數組的下標爲頁框號(pfn)。 因爲每個描述符長度爲32字節,所以mem_map所需要的空間略小於整個RAM的1%。
那麼一個頁描述符怎樣與一個佔據4k的頁框相聯繫(映射)呢?有了mem_map數組,這個問題就很簡單了。因爲如果知道了page數據的地址pd,用pd去減去mem_map就得到了pd的頁框號pfn。那麼這個物理頁的物理地址是physAddr = pfn << PAGE_SHIFT 。
在得知該物理頁的物理地址是physAddr後,就可以視physAddr的大小得到它的虛擬地址:
1.physAddr < 896M 對應虛擬地址是 physAddr + PAGE_OFFSET (PAGE_OFFSET=3G)
2.physAddr >= 896M 對應虛擬地址不是靜態映射的,通過內核的高端虛擬地址映射得到一個虛擬地址。
在得到該頁的虛擬地址之後,內核就可以正常訪問這個物理頁了。
內核提供一個virt_to_page(addr)宏來產生線性地址addr對應的頁描述符地址。pfn_to_page(pfn)宏產生與頁框號 pfn對應的頁描述符地址。相反,也提供page_to_pfn(pg)宏來產生頁描述符對應的頁的頁框號pfn。注意,針對80x86結構,上述宏並不 是直接通過men_map數組來確定頁框號的,而是通過內存管理區的zone_mem_map來確定的,不過原理是一樣的:
#define page_to_pfn(pg) /
({ /
struct page *__page = pg; /
struct zone *__zone = page_zone(__page); /
(unsigned long)(__page - __zone->zone_mem_map) /
+ __zone->zone_start_pfn; /
})
這裏千萬要注意!不要混淆一個概念。這裏的physAddr雖然表示物理地址,但是並不能說明該地址的數據就一定存在於物理內存中。那麼如何判斷這個頁到底在不在內存中呢?你看,前面的知識就用到了——分頁機制。 也就是說,如果這個頁因爲各種各樣五花八門的原因被交換出去了,那麼它對應的頁的Present標誌就爲0。這裏就牽涉到缺頁異常了,要深入瞭解,請關注筆者後面的博文。
在這裏我們只需要對數據結構page詳細討論以下兩個字段:
1、_count:頁的引用計數器。如果該字段爲-1,則相應頁框空閒,並可被 分配給任一進程或內核本身;如果該字段的值大於或等於0,則說明頁框被分配給一個或多個進程,或用於存放一些內核數據結構。page_count()函數 返回_count加1後的值,也就是該頁的使用者的數目。
2、flags:包含多達32個用來描述頁框狀態的標誌。對於每個PG_xyz標誌,內核都定義了操縱其值的一些宏。通常,PageXyz宏返回標誌的值,而SetPageXyz和ClearPageXyz宏分別設置和清除相應的位。
含義 |
|
PG_locked |
頁被鎖定,例如,在磁盤I/O 操作中涉及的頁。 |
PG_error |
在傳輸頁時發生錯誤 |
PG_referenced |
剛剛訪問過的頁 |
PG_uptodate |
在完成讀操作後置位,除非發生磁盤I/O 錯誤 |
PG_dirty |
頁已經被修改 |
PG_lru |
頁在活動或非活動頁鏈表中 |
PG_active |
頁在活動頁鏈表中 |
PG_slab |
包含在slab 中的頁框 |
PG_highmem |
頁框屬於ZONE_HIGHMEM 管理區 |
PG_checked |
由一些文件系統(如Ext2 和Ext3 )使用的標誌 |
PG_arch_1 |
在80x86 體系結構上沒有使用 |
PG_reserved |
頁框留給內核代碼或沒有使用 |
PG_private |
頁描述符的private 字段存放了有意義的數據 |
PG_writeback |
正在使用writepage 方法將頁寫到磁盤上 |
PG_nosave |
系統掛起 / 喚醒時使用 |
PG_compound |
通過擴展分頁機制處理頁框 |
PG_swapcache |
頁屬於對換高速緩存 |
PG_mappedtodisk |
頁框中的所有數據對應於磁盤上分配的塊 |
PG_reclaim |
爲回收內存對頁已經做了寫入磁盤的標記 |
PG_nosave_free |
系統掛起 / 恢復時使用 |
系統是怎麼爲進程或內核分配一個內存空間,或者說怎麼給他們分配一個線性頁描述符所對應線性地址的頁面呢?這個需要藉助內核的分區頁框分配器和夥伴系統算法。在討論這些細節之前,先介紹一些必要的概念。
1 非統一內存訪問(NUMA)架構
Linux2.6支持非統一內存訪問(NUMA)模型,在這種模型中,給定CPU對不同內存單元的訪問時間可能不一樣。系統的物理內存被劃分爲幾個 節點(node)。在一個單獨的節點內,任一給定CPU訪問頁面所需要的時間都是相同的,而對於不同的CPU,這個時間就不同。對每個CPU而言,內核都 試圖把耗時節點的訪問次數減到最少,這就必須要將那些CPU最常引用的內核數據結構的存放位置選好。
每個節點由一個類型爲pg_data_t的描述符表示,所有節點的描述符存放在一個單向鏈表中,它的第一個元素由內核全局變量pgdat_list 指向。在x86體系中,即使是多核,內存訪問時間也是相同的,所以不需要NUMA,但是內核還是使用節點,不過,這只是一個單獨的節點,它包含了系統中所 有的物理內存。因此,pgdat_list變量指向一個鏈表,此鏈表只有一個元素組成的,這個元素就是節點0描述符,它被存放在 contig_page_data變量中。
pg_data_t描述符中要注意到的三個字段分別是node_zones、node_zonelists、node_mem_map,分別是 zone_t[]、zonelist_t[]和page類型。前兩個是用來描述內存管理區的,下面馬上要談到;node_mem_map是本節點所有頁的 頁描述符數組。內核將這三個字段放在裏邊,就是爲內存區、頁框建立一些列的聯繫。
2 內存管理區
由於Linux內核必須處理80x86體系結構中的兩種硬件約束:
(1)ISA總線的直接內存存取(DMA)處理器有一個嚴格的限制:他們只能對RAM的前16MB尋址。
(2)在具有較大容量RAM的現代32位計算機中,CPU不能直接訪問所有的物理內存,因爲線性地址空間太小。
爲了應對這兩種限制,Linux2.6把每個內存節點的物理內存劃分成3個管理區(zone)。在80x86的UMA體系結構中的管理區分爲:
ZONE_DMA:包含低於16MB的內存頁框
ZONE_NORMAL:包含高於16MB而低於896MB的內存頁框
ZONE_HIGHMEM:包含從896MB開始高於896MB的內存頁框
ZONE_DMA和ZONE_NORMAL區包含內存“常規”頁框,通過把他們線性地址映射到線性地址空間的第4個GB,內核就可以直接進行訪問。 ZONE_HIGHMEM區包含的內存頁不能由內核直接訪問,儘管它們也可以通過高端內存內核映射,線性映射到線性地址空間的第4個GB。
每個內存管理區都有自己的描述符zone_t,其字段中很多用於回收頁框時使用。其實每個頁描述符page都有到內存節點和到內存節點管理區的鏈 接。那我們爲啥看不到呢,原因是爲了節省空間,這些鏈接的存放方式和典型的指針不同,是被編碼成索引存放在flags字段的高位。
zone_t字段如下:
類型 |
名稱 |
說明 |
unsigned long |
free_pages |
管理區中空閒頁的數量 |
unsigned long |
pages_min |
管理區中保留頁的數目 |
unsigned long |
pages_low |
回收頁框使用的下界;同時也被管理區分配器作爲閾值使用 |
unsigned long |
pages_high |
回收頁框使用的上界;同時也被管理區分配器作爲閾值使用 |
unsigned long [] |
lowmem_reserve |
指明在處理內存不足的臨界情況下每個管理區必須保留的頁框數目 |
struct per_cpu_pageset[] |
pageset |
數據結構用於實現單一頁框的特殊高速緩存 |
spinlock_t |
lock |
保護該描述符的自旋鎖 |
struct free_area [] |
free_area |
標識出管理區中的空閒頁框塊 |
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 |
管理區內回收頁框時使用的計數器 |
int |
all_unreclaimable |
在管理區中填滿不可回收頁時此標誌被置位 |
int |
temp_priority |
臨時管理區的優先級(回收頁框時使用) |
int |
prev_priority |
管理區優先級,範圍在 12 和 0 之間(由回收頁框算法使用) |
wait_queue_head_t * |
wait_table |
進程等待隊列的散列表,這些進程正在等待管理區中的某頁 |
unsigned long |
wait_table_size |
等待隊列散列表的大小 |
unsigned long |
wait_table_bits |
等待隊列散列表數組大小,值爲2order |
struct pglist_data * |
zone_pgdat |
內存節點 |
struct page * |
zone_mem_map |
指向管理區的第一個頁描述符的指針 |
unsigned long |
zone_start_pfn |
管理區第一個頁框的下標 |
unsigned long |
spanned_pages |
以頁爲單位的管理區的總大小,包括洞 |
unsigned long |
present_pages |
以頁爲單位的管理區的總大小,不包括洞 |
char * |
name |
指針指向管理區的傳統名稱:“DMA ”,“NORMAL ”或“HighMem ” |
實際上,刻畫頁框的標誌的數目是有限的,因此保留flags字段的最高位來編碼內存節點和管理區是綽綽有餘的。Linux提供 page_zone()函數用來接收一個頁描述符的地址作爲它的參數;它讀取該描述符中的flags字段的最高位,然後通過查看zone_table數組 來確定相應管理區描述符的地址。順便提一下,在系統啓動時用,內核將所有內存節點的所有管理區描述符的地址放到這個zone_table數組裏邊。
當內核調用一個內存分配函數時,必須指明請求頁框所在的管理區。內核通常指明它願意使用哪個管理區。爲了在內存分配請求中指定首選管理區,內核使用 zonelist數據結構,這就是管理區描述符指針數組,在80x86中只有三個zone,所以zonelist數據結構中指向這三個zone的指針按照 一定規則排列。如圖,則zonelist數組就是這三個zone的排列組合。
例如,要分配一個用來做DMA的頁框,則在指定zonelist數組中的某個zonelist元素中獲得首選的zone,應該是ZONE_DMA,如果該區空間已使用完,就選ZONE_NORMA區,隨後再是ZONE_HIGHMEM。