作業系統的高端內存

一 為什麼我們需要高端內存

我們知道在x86_32架構下,linux中的進程的虛擬地址空間大小是4GB,其中的用戶空間佔用其中的低3GB,而內核空間佔用其中的高1GB。而實際上內核的物理空間是從地址0開始的。所以 PA = VA - 0xC000 0000。

從下圖可以得出:

虛擬地址 物理地址

0xC000 0000 0x0000 0000

0xFFFF 8FFF 0x3FFF 8FFF

0xFFFF FFFF 0x4000 0000

操作系統的高端內存

這裡就出現了問題,內核空間只能映射到前1GB的物理空間,為了解決這個問題。內核將每個節點的物理內存空間分成了三個部分:zone_dma zone_normal zone_highmem。zone_dma和zone_normal佔用其中的896MB,而 zone_highmem佔用的是>896MB的空間。而內核虛擬地址空間的高128MB用來專門映射高端內存。不過這種映射是動態的,也就是說該區域沒有辦法永久映射到內核的虛擬地址空間。

操作系統的高端內存

二 建立高端內存的映射

操作系統的高端內存

1 永久內核映射

1.1 數據結構

1 page_address_htable

在函數page_address()中,為了加速從頁框指針到線性地址的轉換,內核使用哈希表保存頁框指針和線性地址的關係。桶中的每一項都是一個page_address_map結構。

操作系統的高端內存

static struct page_address_slot {

struct list_head lh; /* List of page_address_maps */

spinlock_t lock; /* Protect this bucket's list */

} page_address_htable[1<

struct page_address_map {

struct page *page;

void *virtual;

struct list_head list;

};

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

否則不調用mapp_new_virtual,直接將count加一,此時count應該大於2

void fastcall *kmap_high(struct page *page)

{

unsigned long vaddr;

// 申請kmap_lock自旋鎖

spin_lock(&kmap_lock);

// 檢查頁框是否已經被映射

vaddr = (unsigned long)page_address(page);

// 如果還沒有被映射,調用 map_new_virtual創建映射 返回一個新的線性地址

if (!vaddr)

vaddr = map_new_virtual(page);

pkmap_count[PKMAP_NR(vaddr)]++;

if (pkmap_count[PKMAP_NR(vaddr)] < 2)

BUG();

spin_unlock(&kmap_lock);

return (void*) vaddr;

}

4 map_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)

static inline unsigned long map_new_virtual(struct page *page)

{

unsigned long vaddr;

int count;

for (;;) {

int count;

DECLARE_WAITQUEUE(wait, current);

//掃描頁表中的每一個頁表項

for (count = LAST_PKMAP; count > 0; --count) {

//last_pkmap_nr 是函數上次結束時,頁表項的索引。

//這裡使用了一些小詭計 等同於 last_pkmap_nr = (last_pkmap_nr + 1) % (LAST_PKMAP) 但是使用位運算更快

last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP - 1);

if (!last_pkmap_nr) {

flush_all_zero_pkmaps( );

count = LAST_PKMAP;

}

如果last_pkmap_nr指向的頁表項可用

if (!pkmap_count[last_pkmap_nr]) {

unsigned long vaddr = PKMAP_BASE + (last_pkmap_nr << PAGE_SHIFT);

//創建並插入頁表項,對應頁的屬性為0110_0011 即 G D A PCD PWT US RW P = 0110_0011

set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, __pgprot(0x63)));

pkmap_count[last_pkmap_nr] = 1;

//插入一個新元素到哈希表page_address_htable並返回線性地址

set_page_address(page, (void *) vaddr);

return vaddr;

}

}

//如果沒有找到可用的頁表項則

current->state = TASK_UNINTERRUPTIBLE;

add_wait_queue(&pkmap_map_wait, &wait);

spin_unlock(&kmap_lock);

schedule( );

remove_wait_queue(&pkmap_map_wait, &wait);

spin_lock(&kmap_lock);

if (page_address(page))

return (unsigned long) page_address(page);

}

}

2 臨時內核映射

2.1

臨時內核映射又稱為原子映射,這裡先拋個問題:為什麼臨時映射要稱作原子映射。

臨時內核映射區域位於固定映射區內,固定映射區內的線性地址可以隨意映射到任意一個物理地址,而不是使用 物理地址 = 線性地址 - 0xC000_0000 得到。

臨時內核映射區的起始和終止的線性地址的索引(關於什麼是線性地址的索引,後面會說明)由 enum fixed_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。於是,臨時映射區變成了這樣:

操作系統的高端內存

臨時映射由kmap_atomic 創建相比於 kmap,kmap_atomic 不阻塞當前進程,不刷新TLB,從而帶來了速度上的提升。但是由於kmap_atomic並不阻塞當前進程,如果同一個CPU 上先後有兩個進程都要在同一個window上建立映射,並且前一個進程還沒有釋放映射,那麼後一個進程創建的映射就會覆蓋前一個進程所創建的映射(其實質是頁表項的覆蓋)。所以必須原子性的創建和釋放映射,這就是kmap_atomic名字的由來。

2.2 kmap_atomic

kmap_atomic接收兩個參數,page是被映射的頁面指針,type表明此次映射位於臨時區間的那個window。

函數返回一個線性地址。

void *kmap_atomic(struct page *page, enum km_type type)

{

//idx是用來完成線性地址轉換的索引

enum fixed_addresses idx;

//vaddr用來保存映射建立後的線性地址

unsigned long vaddr;

/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */

//將task_struct的preempt_count字段加一

inc_preempt_count();

//如果被映射的頁框不在高端內存 則直接利用__va 計算該頁框的線性地址

//__va(page_to_pfn(page) << PAGE_SHIFT);

//先獲取該頁框的頁框號根據公式 pn * 4K(頁大小) 得到頁框的物理地址(PA) 再用 PA + 0xC000 0000 得到線性地址

if (!PageHighMem(page))

return page_address(page);

/**如果該頁框位於高端內存

/* 以下公式需要說明一下, smp_processor_id指明映射發生即當前進程所在的CPU ID type指明瞭本次映射發生在那個window

/*根據該公式我們就能得到映射實際發生的索引,注意該索引只是一個相對索引

idx = type + KM_TYPE_NR*smp_processor_id();

vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);

//設置頁表項

set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));

//刷新TLB

__flush_tlb_one(vaddr);

return (void*) vaddr;

}

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

操作系統的高端內存

enum km_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

#ifdef CONFIG_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)

#define FIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE)

#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))

#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

#define __FIXADDR_TOP 0xfffff000


分享到:


相關文章: