一、爲什麼我們需要高端內存
我們知道在x86_32架構下,linux中的進程的虛擬地址空間大小是4GB,其中的用戶空間佔用其中的低3GB,而內核空間佔用其中的高1GB。而實際上內核的物理空間是從地址0開始的。所以內核空間的物理地址和虛擬地址可以根據右式轉換 PA = VA -0xC000 0000。根據這種計算方式,我們可以得到以下的表格:
虛擬地址 物理地址
0xC000 0000 0x0000 0000
0xFFFF 8FFF 0x3FFF 8FFF
…
0xFFFF FFFF 0x4000 0000
圖1-1 無高端內存的映射關係
這裏就出現了問題,內核空間只能映射到前1GB的物理空間,爲了解決這個問題。內核將每個節點的物理內存空間分成了三個部分:①zone_dma ②zone_normal ③zone_highmem。zone_dma和zone_normal佔用其中的896MB,而 zone_highmem佔用的是>896MB的空間。而內核虛擬地址空間的高128MB用來專門映射高端內存。不過這種映射是動態的,也就是說該區域沒有辦法永久映射到內核的虛擬地址空間。
圖1-2 有高端內存之後的映射關係
二、建立高端內存的映射
圖2-1 內核虛擬地址空間的結構
1.永久內核映射
1.1數據結構
1page_address_htable
在函數page_address()中,爲了加速從頁框指針到線性地址的轉換,內核使用哈希表保存頁框指針和線性地址的關係。桶中的每一項都是一個page_address_map結構。
圖2-2 page_address_htable
2 pkmap_count數組
永久映射區間的起始線性地址爲 PKMAP_BASE。內核利用主內核頁目錄表(swapper_pg_dir)的中的一個頁表項所指向的頁表來建立永久映射,該頁表由指針
pte_t * pkmap_page_table 來表示。頁表中的頁表項個數有宏LAST_PKMAP表示。PAE開啓時,頁表項個數爲1024,反之則爲512。
與臨時映射不同爲了保證映射的持久,內核建立了一個數組 int pkmap_count[LAST_PKMAP],該數組元素的個數就是頁表項的個數。數組是一個計數器的集合。
count = 0 : 表示該頁表項可用,相關映射還未建立,在TLB刷新前,TLB還沒有相關頁表項的存在。
count = 1 : 表示該頁目前沒有映射到任何頁框,但是TLB中的上次映射的表項還沒有被flush。所以該頁的映射無法創建。簡單來說,就是該映射還存儲在TLB中。
count = n : 表示相關的頁表項已經建立,並且有 n-1 個進程在使用該映射。
當然,僅僅一個計數器還不夠,爲了防止對頁表項的併發訪問,創建映射的過程需要用鎖進行控制。
永久映射由 kmap(struct page *page) 創建,該函數接收參數page作爲被映射的頁框指針。該函數返回一個線性地址。kmap的核心是函數kmap_high()。
3 kmap_high
kmap_high 先調用page_address得到頁框對應的線性地址,如果該頁框還沒有被映射,則調用 map_new_virtual。在map_new_virtual中,如果發現一個count爲0的映射,則將count置爲1,隨後將count加一,此時,count值等於2。
否則不調用map_new_virtual,直接將count加一,此時count應該大於2。
4map_new_virtual
當一個頁框還沒有被映射到一個虛擬頁時,就會調用map_new_virtual。爲了防止對pkmap_count數組的重複遍歷,函數使用last_pkmap_nr記錄上次映射結束時頁表項的索引。map_new_virtual其實大致上做了三件事:
第一,如果pkmap_count中有計數器爲0的索引,則建立映射並令其count = 1。
第二,如果last_pkmap_nr=0,也就是整個頁表沒有可用的頁表項了,則調用flush_all_zero_pkmaps將所有的計數器爲1 的映射(也就是說映射僅僅在TLB中)的計數器置爲0,沖刷TLB。
第三,如果pkmap_count都大於1,則阻塞當前進程,將當前進程狀態置爲 TASK_UNINTERRUPTIBLE 並加入等待隊列。之後調度其他進程,其他進程的時間片完後,再將原進程從等待隊列移出。如果當前沒有其他進程映射該頁框,則進行下次循環。
map_new_virtual等價於以下代碼(摘自ULK)
2.臨時內核映射
2.1 臨時內核映射
臨時內核映射又稱爲原子映射,這裏先拋個問題:爲什麼臨時映射要稱作原子映射。
臨時內核映射區域位於固定映射區內,固定映射區內的線性地址可以隨意映射到任意一個物理地址,而不是使用物理地址 = 線性地址 - 0xC000_0000 得到。
臨時內核映射區的起始和終止的線性地址的索引(關於什麼是線性地址的索引,後面會說明)由 enumfixed_address 中的常量 FIX_KMAP_BEGIN FIX_KMAP_END 分別指定。其中FIX_KMAP_END =FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。內核根據CPU核心數劃分臨時映射區。
而在每個CPU獨有的塊內部又根據頁面的用途分成了13個窗口,舉個例子 KM_USER0 和 KM_USER1就是內核用來存儲來自用戶上下文的(通常是系統調用傳遞的局部變量和參數)。每個窗口其實就是一個頁面。這13個窗口在內核中由 enum km_type 表示,而每個窗口的線性地址由km_type 中的常量作爲索引來計算。KM_TYPE_NR是窗口的分類個數,等於13。於是,臨時映射區變成了這樣:
圖2-4 細化的臨時內核映射區結構
臨時映射由kmap_atomic 創建相比於 kmap,kmap_atomic 不阻塞當前進程,不刷新TLB,從而帶來了速度上的提升。但是由於kmap_atomic並不阻塞當前進程,如果同一個CPU 上先後有兩個進程都要在同一個window上建立映射,並且前一個進程還沒有釋放映射,那麼後一個進程創建的映射就會覆蓋前一個進程所創建的映射(其實質是頁表項的覆蓋)。所以必須原子性的創建和釋放映射,這就是kmap_atomic名字的由來。
2.2 kmap_atomic
kmap_atomic接收兩個參數,page是被映射的頁面指針,type表明此次映射位於臨時區間的那個window。
函數返回一個線性地址。
2.3 __fix_to_virt 宏
關於fix_to_virt需要重點說明一下,fix_to_virt宏將索引轉換爲線性地址,注意此處使用的是位於固定映射區間的絕對索引FIXADDR_TOP 是固定映射區間的結束線性地址。固定映射位於線性地址 FIXADDR_START 與 FIXADDR_TOP之間,FIXADDR_TOP =0xFFFF_F000 。在固定映射區間與虛擬地址空間的頂端(4G)之間還有一個1個頁大小的空洞稱爲 FIX_HOLE ,更重要的是固定映射區間是向下拓展的(類似於棧)。
內核中使用宏 #define __fix_to_virt(x) (FIXADDR_TOP -((x) << PAGE_SHIFT)) 完成從索引到線性地址的轉換,結合下圖可得區域 FIX_VSYSCALL的起始線性地址爲
0xFFFF_E000 = 0xFFFF_F000 - 1 * 0x1000
圖2-5 固定映射區間的結構圖
enumkm_type {
D(0) KM_BOUNCE_READ,
D(1) KM_SKB_SUNRPC_DATA,
D(2) KM_SKB_DATA_SOFTIRQ,
D(3) KM_USER0,
D(4) KM_USER1,
D(5) KM_BIO_SRC_IRQ,
D(6) KM_BIO_DST_IRQ,
D(7) KM_PTE0,
D(8) KM_PTE1,
D(9) KM_IRQ0,
D(10) KM_IRQ1,
D(11) KM_SOFTIRQ0,
D(12) KM_SOFTIRQ1,
D(13) KM_TYPE_NR
};
fixmap.h
#ifdefCONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
FIX_KMAP_END= FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#define__FIXADDR_SIZE (__end_of_permanent_fixed_addresses <<PAGE_SHIFT)
#defineFIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE)
#define__fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#defineFIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
#define__FIXADDR_TOP 0xfffff000
以上所有的內容都基於linux-2.6.11
參考:
深入理解Linux內核
http://bbs.chinaunix.net/thread-1920551-1-1.html 關於pkmap_count的討論
https://yq.aliyun.com/articles/130909 關於pkmap_count很直觀的描述
http://bbs.chinaunix.net/thread-1938084-1-1.html 關於高端內存的討論