高端內存

一、爲什麼我們需要高端內存

我們知道在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_highmemzone_dmazone_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中,如果發現一個count0的映射,則將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核心數劃分臨時映射區。


2-3 臨時內核映射區的結構

而在每個CPU獨有的塊內部又根據頁面的用途分成了13個窗口,舉個例子 KM_USER0 KM_USER1就是內核用來存儲來自用戶上下文的(通常是系統調用傳遞的局部變量和參數)。每個窗口其實就是一個頁面。這13個窗口在內核中由 enum km_type 表示,而每個窗口的線性地址由km_type 中的常量作爲索引來計算。KM_TYPE_NR是窗口的分類個數,等於13。於是,臨時映射區變成了這樣:

2-4 細化的臨時內核映射區結構
臨時映射由kmap_atomic 創建相比於 kmapkmap_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  
關於高端內存的討論

 

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