標籤:記憶體管理 linux kernel
前面簡單地介紹了三種不同的地址空間,接下來重點講述線性地址空間到物理地址空間的映射。
我們先從32位系統開始。
在32位系統中,線性地址空間的大小為 2^32,即4GB。Kernel一般會按照 3:1 的比例,把線性地址空間分為兩部分:
-
0~3GB 使用者地址空間
-
3GB~4GB 核心地址空間。
650) this.width=650;" src="http://s3.51cto.com/wyfs02/M02/70/D9/wKiom1W_NS6isOSgAABJiSmORuo084.jpg" title="32-mapping.png" alt="wKiom1W_NS6isOSgAABJiSmORuo084.jpg" width="230" height="253" border="0" hspace="0" vspace="0" style="width:230px;height:253px;" />
使用者地址空間的管理和映射是個大的topic。我們後面再詳細講述。
核心地址空間只有1GB大小,最多隻能映射1GB的實體記憶體。那問題來了:1GB之外實體記憶體怎麼辦?
Kernel 給出的解決辦法是,把1GB的核心地址空間分成兩部分:
這樣,分時複用最後這128MB的線性地址空間,Kernel 就可以訪問到全部的實體記憶體了。正所謂,退一步海闊天空啊。
也正因為如此,大於896MB實體記憶體,無法直接映射到核心地址空間,通常被稱為高端記憶體。
那核心地址空間中後面的這128MB是如何動態映射不同位置的實體記憶體的呢?Kernel為此提供了三種機制,如所示。
650) this.width=650;" src="http://s3.51cto.com/wyfs02/M00/70/DA/wKiom1W_PXKQDfdtAAHt0J3gQhU569.jpg" title="kernel-mapping.png" width="720" height="194" border="0" hspace="0" vspace="0" style="width:720px;height:194px;" alt="wKiom1W_PXKQDfdtAAHt0J3gQhU569.jpg" />
雖然機制不同,但是它們都需要通過修改頁表來完成映射。我們先介紹一類特殊的頁表:kernel page tables。
我們知道,每個進程都有自己的一套頁表,用於完成線性地址到物理地址的轉換。而核心也維護了一套頁表,稱為 kernel page tables.
不過,這套頁表有點與眾不同。系統初始化完成後,kernel page tables不會被任何使用者態或核心態進程直接使用。咦?不被任何進程使用,那它到底有什麼用呢?
這套頁表的頁目錄表,即所謂的 master kernel Page Global Directory,其最高的一些頁目錄項 (128/4 = 32),為進程的頁表提供了一個參考模型。如果修改了master kernel Page Global Directory中的這些頁目錄項,這些改動最終會傳播到進程所使用的頁表中。那這個“傳播”是怎麼實現的呢?我們在後面會更詳細地講述。
持久核心映射 (Persistent Kernel Mappings)
持久核心映射可以用來比較持久地把高端記憶體頁面映射到核心地址空間中。既然是映射,那就要涉及到三個元素:集合L,集合P,以及這兩個集合之間的映射M。
集合L:核心在那128MB的線性地址空間中預留了一塊地區。如所示,該地區的範圍是 PKMAP_BASE ~ FIXADDR_START。
集合P:高端記憶體頁面。
映射M:這個就是下面要講的內容。
資料結構
持久核心映射使用了 master kernel page tables 中一個供自己專用的頁表: pkmap_page_table.
宏 LAST_PKMAP 定義了該頁表中有多少頁表項。
41 #ifdef CONFIG_X86_PAE 42 #define LAST_PKMAP 512 43 #else 44 #define LAST_PKMAP 1024 45 #endif
沒開啟PAE的情況下,該頁表中有 1024 頁表項,所以持久核心映射一次能夠映射 4MB 的高端記憶體。
整型數組 pkmap_count 用來描述頁表 pkmap_page_table 中每個頁表項的使用方式。
61 static int pkmap_count[LAST_PKMAP];
-
counter 等於 0:對應的頁表項沒有映射高端記憶體頁面,可以使用。
-
counter 等於 1:對應的頁表項沒有映射高端記憶體頁面,但是卻不能被使用,因為它對應的TLB項還沒有被重新整理。
-
counter 大於 1:對應的頁表項映射了高端記憶體頁面,且被使用了 (n - 1)次。
為了能夠方便的找到一個高端記憶體頁面通過持久核心映射機制所映射到的線性地址,核心使用了一個雜湊表:page_address_htable.
該雜湊表中每一項為 struct page_address_map:
234 /*235 * Describes one page->virtual association236 */237 struct page_address_map {238 struct page *page;239 void *virtual;240 struct list_head list;241 };
在介紹核心如何利用這些資料結構建立持久核心映射之前,我們先看看核心如何尋找一個物理頁面所映射到的線性地址。
尋找
262 void *page_address(struct page *page)263 {264 unsigned long flags;265 void *ret;266 struct page_address_slot *pas;267268 if (!PageHighMem(page))269 return lowmem_page_address(page);270271 pas = page_slot(page);272 ret = NULL;273 spin_lock_irqsave(&pas->lock, flags);274 if (!list_empty(&pas->lh)) {275 struct page_address_map *pam;276277 list_for_each_entry(pam, &pas->lh, list) {278 if (pam->page == page) {279 ret = pam->virtual;280 goto done;281 }282 }283 }284 done:285 spin_unlock_irqrestore(&pas->lock, flags);286 return ret;287 }
函數 page_address() 區分兩種情況:
1.該頁面不屬於高端記憶體。那麼該頁面位於實體記憶體中的 0 ~ 896MB,該範圍的實體記憶體頁面都是直接映射到核心地址空間中的,所以頁面的線性地址總是存在,且可通過頁面索引計算出來。
536 static __always_inline void *lowmem_page_address(struct page *page) 537 { 538 return __va(page_to_pfn(page) << PAGE_SHIFT); 539 }
2.該頁面屬於高端記憶體。這時就用到前面講的那個雜湊表了。如果在雜湊表中找到了該頁面,說明該頁面已經建立了持久核心映射,則返回其線性地址;否則返回NULL。
映射
建立一個持久核心映射是由函數 kmap() 完成的。
4 void *kmap(struct page *page) 5 { 6 might_sleep(); 7 if (!PageHighMem(page)) 8 return page_address(page); 9 return kmap_high(page); 10 }
如果頁面不屬於高端記憶體,則直接返回其對應的線性地址。否則利用函數 kmap_high() 來完成映射操作。
166 void fastcall *kmap_high(struct page *page)167 {168 unsigned long vaddr;169170 /*171 * For highmem pages, we can‘t trust "virtual" until172 * after we have the lock.173 *174 * We cannot call this from interrupts, as it may block175 */176 spin_lock(&kmap_lock);177 vaddr = (unsigned long)page_address(page);178 if (!vaddr)179 vaddr = map_new_virtual(page);180 pkmap_count[PKMAP_NR(vaddr)]++;181 BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);182 spin_unlock(&kmap_lock);183 return (void*) vaddr;184 }
該函數首先判斷頁面是否已經映射。如果沒有,則通過函數 map_new_virtual()來建立一個新的映射。
最後遞增數組 pkmap_count中的計數值。
116 static inline unsigned long map_new_virtual(struct page *page)117 {118 unsigned long vaddr;119 int count;120121 start:122 count = LAST_PKMAP;123 /* Find an empty entry */124 for (;;) {125 last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;126 if (!last_pkmap_nr) {127 flush_all_zero_pkmaps();128 count = LAST_PKMAP;129 }130 if (!pkmap_count[last_pkmap_nr])131 break; /* Found a usable entry */132 if (--count)133 continue;134135 /*136 * Sleep for somebody else to unmap their entries137 */138 {139 DECLARE_WAITQUEUE(wait, current);140141 __set_current_state(TASK_UNINTERRUPTIBLE);142 add_wait_queue(&pkmap_map_wait, &wait);143 spin_unlock(&kmap_lock);144 schedule();145 remove_wait_queue(&pkmap_map_wait, &wait);146 spin_lock(&kmap_lock);147148 /* Somebody else might have mapped it while we slept */149 if (page_address(page))150 return (unsigned long)page_address(page);151152 /* Re-start */153 goto start;154 }155 }156 vaddr = PKMAP_ADDR(last_pkmap_nr);157 set_pte_at(&init_mm, vaddr,158 &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));159160 pkmap_count[last_pkmap_nr] = 1;161 set_page_address(page, (void *)vaddr);162163 return vaddr;164 }
該函數做了兩件事情:
在找可用的頁表項時,是從上次的位置 (last_pkmap_nr) 開始的。如果已遍曆到了數組pkmap_count的尾部,則從數組頭部開始接著遍曆。不過接著遍曆之前,會先調用函數 flush_all_zero_pkmaps()。該函數專門尋找counter為1的頁表項,然後對它們做了四件事情:
如果沒能找到可用的頁表項,當前進程就會進入睡眠,直到其他進程釋放了 pkmap_page_table中的一個頁表項。也正因為如此,函數kmap()會阻塞當前進程,不能用在中斷上下文中。
如果找到了可用的頁表項,則計算出其對應的線性地址,在頁表pkmap_page_table對應的頁表項中建立映射,把pkmap_count中的counter置為1,最後把該映射關係插入到雜湊表中。
解除一個持久核心映射是由函數 kunmap() 完成的。
12 void kunmap(struct page *page) 13 { 14 if (in_interrupt()) 15 BUG(); 16 if (!PageHighMem(page)) 17 return; 18 kunmap_high(page); 19 }
如果頁面不屬於高端記憶體,就沒啥可做的了。否則,利用函數 kunmap_high() 來解除映射。
188 void fastcall kunmap_high(struct page *page)189 {190 unsigned long vaddr;191 unsigned long nr;192 int need_wakeup;193194 spin_lock(&kmap_lock);195 vaddr = (unsigned long)page_address(page);196 BUG_ON(!vaddr);197 nr = PKMAP_NR(vaddr);203 need_wakeup = 0;204 switch (--pkmap_count[nr]) {205 case 0:206 BUG();207 case 1:218 need_wakeup = waitqueue_active(&pkmap_map_wait);219 }220 spin_unlock(&kmap_lock);221222 /* do wake-up, if needed, race-free outside of the spin lock */223 if (need_wakeup)224 wake_up(&pkmap_map_wait);225 }
該函數的實現非常簡單。遞減數組 pkmap_count 中對應的counter。如果發現counter為1,則說明其對應的頁表項已沒人使用,於是喚醒在函數 map_new_virtual() 中由於等待可用頁表項而進入睡眠的進程。
本文出自 “核心部落格” 部落格,請務必保留此出處http://richardguo.blog.51cto.com/9343720/1682364
Kernel那些事兒之記憶體管理(11) --- 核心映射(上)