高端內存概述
在32位的系統上,內核占有從第3GB~第4GB的線性地址空間,共1GB大小,內核將其中的前896MB與物理內存的0~896MB進行直接映射,即線性映射,將剩余的128M線性地址空間作為訪問高於896M的內存的一個窗口。引入高端內存映射這樣一個概念的主要原因就是我們所安裝的內存大於1G時,內核的1G線性地址空間無法建立一個完全的直接映射來觸及整個物理內存空間,而對於80x86開啟PAE的情況下,允許的最大物理內存可達到64G,因此內核將自己的最後128M的線性地址空間騰出來,用以完成對高端內存的暫時性映射。而在64位的系統上就不存在這樣的問題了,因為可用的線性地址空間遠大於可安裝的內存。下圖描述了內核1GB線性地址空間是如何劃分的
其中可以用來完成上述映射目的的區域為vmalloc area,Persistent kernel mappings區域和固定映射線性地址空間中的FIX_KMAP區域,這三個區域對應的映射機制分別為非連續內存分配,永久內核映射和臨時內核映射。
相關閱讀:
Linux高端內存映射(上) http://www.linuxidc.com/Linux/2012-05/60627.htm
Linux高端內存映射(中) http://www.linuxidc.com/Linux/2012-05/60628.htm
Linux高端內存映射(中) http://www.linuxidc.com/Linux/2012-05/60902.htm
永久內核映射
在內核初始化頁表管理機制時,專門用pkmap_page_table這個變量保存了PKMAP_BASE對應的頁表項的地址,由pkmap_page_table來維護永久內核映射區的頁表項的映射,頁表項總數為LAST_PKMAP個,具體可以看前面關於頁表機制初始化的博文。這裡的永久並不是指調用kmap()建立的映射關系會一直持續下去無法解除,而是指在調用kunmap()解除映射之間這種映射會一直存在,這是相對於臨時內核映射機制而言的。
內核用一個pkmap_count數組來記錄pkmap_page_table中每一個頁表項的使用狀態,其實就是為每個頁表項分配一個計數器來記錄相應的頁表是否已經被用來映射。計數值分為以下三種情況:
計數值為0:對應的頁表項沒有映射高端內存,即為空閒可用的
計數值為1: 對應的頁表項沒有映射高端內存,但是不可用,因為上次映射後對應的TLB項還未被淸刷
計數值為n(n>1):對應的頁表項已經映射了一個高端內存頁框,並且有n-1個內核成分正在利用這種映射關系
下面結合代碼進行具體的分析,先通過alloc_page(__GFP_HIGHMEM)分配到了一個屬於高端內存區域的page結構,然後調用kmap(struct page*page)來建立與永久內核映射區的映射,需要注意一點的是,當永久內核映射區沒有空閒的頁表項可供映射時,請求映射的進程會被阻塞,因此永久內核映射請求不能發生在中斷和可延遲函數中。
- void *kmap(struct page *page)
- {
- might_sleep();
- if (!PageHighMem(page))/*頁框屬於低端內存*/
- return page_address(page);/*返回頁框的虛擬地址*/
- return kmap_high(page);
- }
如果頁框是屬於高端內存的話,則調用kmap_high()來建立映射
- void *kmap_high(struct page *page)
- {
- unsigned long vaddr;
-
- /*
- * For highmem pages, we can't trust "virtual" until
- * after we have the lock.
- */
- lock_kmap();/*獲取自旋鎖防止多處理器系統上的並發訪問*/
-
- /*試圖獲取頁面的虛擬地址,因為之前可能已經有進程為該頁框建立了到永久內核映射區的映射*/
- vaddr = (unsigned long)page_address(page);
-
- /*虛擬地址不存在則調用map_new_virtual()為該頁框分配一個虛擬地址,完成映射*/
- if (!vaddr)
- vaddr = map_new_virtual(page);
- pkmap_count[PKMAP_NR(vaddr)]++;/*相應的頁表項的計數值加1*/
- BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
- unlock_kmap();
- return (void*) vaddr;
- }
如果該頁框之前沒被映射到永久內核映射區,則要通過map_new_virtual()函數在永久內核映射區對應的頁表中找到一個空閒的表項來映射這個頁框,簡單的說就是為這個頁框分配一個線性地址。
- static inline unsigned long map_new_virtual(struct page *page)
- {
- unsigned long vaddr;
- int count;
-
- start:
- /*LAST_PKMAP為永久映射區可以映射的頁框數,在禁用PAE的情況下為512,開啟PAE的情況下為1024,
- 也就是說內核通過kmap,一次最多能映射2M/4M的高端內存*/
- count = LAST_PKMAP;
- /* Find an empty entry */
- for (;;) {
- /*last_pkmap_nr記錄了上次遍歷pkmap_count數組找到一個空閒頁表項後的位置,首先從
- last_pkmap_nr出開始遍歷,如果未能在pkmap_count中找到計數值為0的頁表項,則last_pkmap_nr
- 和LAST_PKMAP_MASK相與後又回到0,進行第二輪遍歷*/
- last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
-
- /*last_pkmap_nr變為了0,也就是說第一次遍歷中未能找到計數值為0的頁表項*/
- if (!last_pkmap_nr) {
- flush_all_zero_pkmaps();
- count = LAST_PKMAP;
- }
- if (!pkmap_count[last_pkmap_nr])/*找到一個計數值為0的頁表項,即空閒可用的頁表項*/
- break; /* Found a usable entry */
- if (--count)
- continue;
-
- /*
- * Sleep for somebody else to unmap their entries
- */
- /*在pkmap_count數組中,找不到計數值為0或1的頁表項,即所有頁表項都被內核映射了,
- 則聲明一個等待隊列,並將當前要求映射高端內存的進程添加到等待隊列中然後
- 阻塞該進程,等待其他的進程釋放了KMAP區的某個頁框的映射*/
- {
- DECLARE_WAITQUEUE(wait, current);
-
- __set_current_state(TASK_UNINTERRUPTIBLE);
- add_wait_queue(&pkmap_map_wait, &wait);
- unlock_kmap();
- schedule();
- remove_wait_queue(&pkmap_map_wait, &wait);
- lock_kmap();
-
- /* Somebody else might have mapped it while we slept */
- /*在睡眠的時候,可能有其他的進程映射了該頁面,所以先試圖獲取頁面的虛擬地址,成功的話直接返回*/ if(page_address(page))
- return (unsigned long)page_address(page);
-
- /* Re-start */
- goto start;/*喚醒後重新執行遍歷操作*/
- }
- }
- /*尋找到了一個未被映射的頁表項,獲取該頁表項對應的線性地址並賦給vaddr*/
- vaddr = PKMAP_ADDR(last_pkmap_nr);
- /*將pkmap_page_table中對應的pte設為申請映射的頁框的pte,完成永久內核映射區中的頁表項到物理頁框的映射*/
- set_pte_at(&init_mm, vaddr,
- &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
-
- pkmap_count[last_pkmap_nr] = 1;
- /*設置頁面的虛擬地址,將該頁面添加到page_address_htable散列表中*/
- set_page_address(page, (void *)vaddr);
-
- return vaddr;
- }