高階記憶體——永久對映區(permanet kernel mappings)
注:本文提及的實體地址空間可以理解為就是物理記憶體,但是在某些情況下,把他們理解為實體記憶體是不對的。
本文討論的環境是NON-PAE的i386平臺,核心版本2.6.31-14
一. 什麼是高階記憶體
linux中核心使用3G-4G的線性地址空間,也就是說總共只有1G的地址空間可以用來對映實體地址空間。但是,如果記憶體大於1G的情況下呢?是不是超過1G的記憶體就無法使用了呢?為此核心引入了一個高階記憶體的概念,把1G的線性地址空間劃分為兩部分:小於896M實體地址空間的稱之為低端記憶體,這部分記憶體的實體地址和3G開始的線性地址是一一對應對映的,也就是說核心使用的線性地址空間3G--(3G+896M)和實體地址空間0-896M一一對應;剩下的128M的線性空間用來對映剩下的大於896M的實體地址空間,這也就是我們通常說的高階記憶體區。
所謂的建立高階記憶體的對映就是能用一個線性地址來訪問高階記憶體的頁。如何理解這句話呢?在開啟分頁後,我們要訪問一個實體記憶體地址,需要經過MMU的轉換,也就是一個32位地址vaddr的高10位用來查詢該vaddr所在頁目錄項,用12-21位來查詢頁表項,再用0-11位偏移和頁的起始實體地址相加得到paddr,再把該paddr放到前端總線上,那麼我們就可以訪問該vaddr對應的實體記憶體了。在低端記憶體中,每一個實體記憶體頁在系統初始化的時候都已經存在這樣一個映射了。而高階記憶體還不存在這樣一個對映(頁目錄項,頁表都是空的),所以我們必須要在系統初始化完後,提供一系列的函式來實現這個功能,這就是所謂的高階記憶體的對映。那麼我們為什麼不再系統初始化的時候把所有的記憶體對映都建立好呢?主要原因是,核心線性地址空間不足以容納所有的實體地址空間(1G的核心線性地址空間和最多可達4G的實體地址空間),所以才需要預留一部分(128M)的線性地址空間來動態的對映所有的實體地址空間,於是就產生了所謂的高階記憶體對映。
二.核心如何管理高階記憶體
上面的圖展示了核心如何使用3G-4G的線性地址空間,首先解釋下什麼是high_memory
在arch/x86/mm/init_32.c裡面由如下程式碼:
#ifdef CONFIG_HIGHMEM highstart_pfn = highend_pfn = max_pfn; if (max_pfn > max_low_pfn) highstart_pfn = max_low_pfn; e820_register_active_regions(0, 0, highend_pfn); sparse_memory_present_with_active_regions( printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn)); num_physpages = highend_pfn; high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1; #else e820_register_active_regions(0, 0, max_low_pfn); sparse_memory_present_with_active_regions(0); num_physpages = max_low_pfn; high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1)+1; #endif |
high_memory是“具體實體記憶體的上限對應的虛擬地址”,可以這麼理解:當記憶體記憶體小於896M時,那麼high_memory = (void *) __va(max_low_pfn * PAGE_SIZE),max_low_pfn就是在記憶體中最後的一個頁幀號,所以high_memory=0xc0000000+實體記憶體大小;當記憶體大於896M時,那麼highstart_pfn= max_low_pfn,此時max_low_pfn就不是實體記憶體的最後一個頁幀號了,而是記憶體為896M時的最後一個頁幀號,那麼high_memory=0xc0000000+896M.總之high_memory是不能超過0xc0000000+896M.
由於我們討論的是實體記憶體大於896M的情況,所以high_memory實際上就是0xc0000000+896M,從high_memory開始的128M(4G-high_memory)就是用作用來對映剩下的大於896M的記憶體的,當然這128M還可以用來對映裝置的記憶體(MMIO)。
從上圖我們看到有VMALLOC_START,VMALLOC_END,PKMAP_BASE,FIX_ADDRESS_START等巨集術語,其實這些術語劃分了這128M的線性空間,一共分為三個區域:VMALLOC區域(本文不涉及這部分內容,關注本部落格的其他文章),永久對映區(permanetkernel mappings), 臨時對映區(temporary kernelmappings).這三個區域都可以用來對映高階記憶體,本文重點闡述下後兩個區域是如何對映高階記憶體的。
三. 永久對映區(permanet kernel mappings)
1. 介紹幾個定義:
PKMAP_BASE:永久對映區的起始線性地址。
pkmap_page_table:永久對映區對應的頁表。
LAST_PKMAP:pkmap_page_table裡面包含的entry的數量=1024
pkmap_count[LAST_PKMAP]陣列:每一個元素的值對應一個entry的引用計數。關於引用計數的值,有以下幾種情況:
0:說明這個entry可用。
1:entry不可用,雖然這個entry沒有被用來對映任何記憶體,但是他仍然存在TLB entry沒有被flush,
所以還是不可用。
N:有N-1個物件正在使用這個頁面
首先,要知道這個區域的大小是4M,也就是說128M的線性地址空間裡面,只有4M的線性地址空間是用來作永久對映區的。至於到底是哪4M,是由PKMAP_BASE決定的,這個變量表示用來作永久記憶體對映的4M區間的起始線性地址。
在NON-PAE的i386上,頁目錄裡面的每一項都指向一個4M的空間,所以永久對映區只需要一個頁目錄項就可以了。而一個頁目錄項指向一張頁表,那麼永久對映區正好就可以用一張頁表來表示了,於是我們就用pkmap_page_table來指向這張頁表。
pgd = swapper_pg_dir+ pgd_index(vaddr); pud = pud_offset(pgd, vaddr);//pud==pgd pmd = pmd_offset(pud, vaddr);//pmd==pud==pgd pte = pte_offset_kernel(pmd, vaddr); pkmap_page_table = pte; |
2. 具體程式碼分析(2.6.31)
void *kmap(struct page*page) { might_sleep(); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); } |
/** * kmap_high - map a highmem page into memory * @page: &struct page to map * * Returns the page's virtual memory address. * * We cannot call this from interrupts, as it may block. */ 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 = (unsignedlong)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++; BUG_ON(pkmap_count[PKMAP_NR(vaddr)]< 2); unlock_kmap(); return (void*) vaddr; } |
如果發現vaddr不為空,那麼就是剛才說的,已經被其他cpu上執行的任務給建立了,這裡只需要把表示該頁引用計數的pkmap_count[]再加一就可以了。同時呼叫BUG_ON來確保該引用計數確實是不小於2的,否則就是有問題的了。然後返回vaddr,整個建立就完成了。
如果發現vaddr為空呢?呼叫map_new_virtual()函式,到此我們看到,其實真正進行建立對映的程式碼在這個函式裡面
static inline unsigned long map_new_virtual(struct page*page) { unsigned long vaddr; int count; start: count = LAST_PKMAP;//LAST_PKMAP=1024 /* Find an empty entry */ for (;;){ last_pkmap_nr = (last_pkmap_nr + 1)& LAST_PKMAP_MASK; if (!last_pkmap_nr){ flush_all_zero_pkmaps(); count = LAST_PKMAP; } if (!pkmap_count[last_pkmap_nr]) break; /* Found a usable entry */ if (--count) continue; /* * Sleep for somebody else to unmap their entries */ { 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 = PKMAP_ADDR(last_pkmap_nr); set_pte_at(&init_mm, vaddr, &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); pkmap_count[last_pkmap_nr]= 1; set_page_address(page,(void*)vaddr); return vaddr; } |
last_pkmap_nr:記錄上次被分配的頁表項在pkmap_page_table裡的位置,初始值為0,所以第一次分配的時候last_pkmap_nr等於1。
接下來判斷什麼時候last_pkmap_nr等於0,等於0就表示1023(LAST_PKMAP(1024)-1)個頁表項已經被分配了,這時候就需要呼叫flush_all_zero_pkmaps()函式,把所有pkmap_count[]計數為1的頁表項在TLB裡面的entry給flush掉,並重置為0,這就表示該頁表項又可以用了,可能會有疑惑為什麼不在把pkmap_count置為1的時候也就是解除對映的同時把TLB也flush呢?個人感覺有可能是為了效率的問題吧,畢竟等到不夠的時候再重新整理,效率要好點吧。
再判斷pkmap_count[last_pkmap_nr]是否為0,0的話就表示這個頁表項是可用的,那麼就跳出迴圈了到下面了。
PKMAP_ADDR(last_pkmap_nr)返回這個頁表項對應的線性地址vaddr.
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
set_pte_at(mm, addr, ptep, pte)函式在NON-PAE i386上的實現其實很簡單,其實就等同於下面的程式碼:
static inline void native_set_pte(pte_t *ptep , pte_t pte)
{
*ptep = pte;
}
我們已經知道頁表的線性起始地址存放在pkmap_page_table裡面,那麼相應的可用的頁表項的地址就是&pkmap_page_table[last_pkmap_nr],得到了頁表項的地址,只要把相應的pte填寫進去,那麼整個對映不就完成了嗎?
pte由兩部分組成:高20位表示實體地址,低12位表示頁的描述資訊。
怎麼通過page查詢對應的實體地址呢(參考page_address()一文)?其實很簡單,用(page - mem_map) 再移PAGE_SHIFT位就可以了。
低12位的頁描述資訊是固定的:kmap_prot=(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL).
下面的程式碼就是做了這些事情:
mk_pte(page, kmap_prot));
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#define page_to_pfn __page_to_pfn
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
massage_pgprot(pgprot));
}
接下來把pkmap_count[last_pkmap_nr]置為1,1不是表示不可用嗎,既然對映已經建立好了,應該賦值為2呀,其實這個操作是在他的上層函式kmap_high裡面完成的(pkmap_count[PKMAP_NR(vaddr)]++).
到此為止,整個對映就完成了,再把page和對應的線性地址加入到page_address_htable雜湊連結串列裡面就可以了(參考page_address一文)。
我們繼續看所有的頁表項都已經用了的情況下,也就是1024個頁表項全已經映射了記憶體了,如何處理。此時count==0,於是就進入了下面的程式碼:
/*
* Sleep for somebody else to unmap their entries
*/
{
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;
}
這段程式碼其實很簡單,就是把當前任務加入到等待佇列pkmap_map_wait,當有其他任務喚醒這個佇列時,再繼續goto start,重新整個過程。這裡就是上面說的呼叫kmap函式有可能阻塞的原因。
那麼什麼時候會喚醒pkmap_map_wait佇列呢?當呼叫kunmap_high函式,來釋放掉一個對映的時候。
kunmap_high函式其實頁很簡單,就是把要釋放的頁表項的計數減1,如果等於1的時候,表示有可用的頁表項了,再喚醒pkmap_map_wait佇列
/**
* kunmap_high - map a highmem page into memory
* @page: &struct page to unmap
*
* If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
* only from user context.
*/
void kunmap_high(struct page *page)
{
unsigned long vaddr;
unsigned long nr;
unsigned long flags;
int need_wakeup;
lock_kmap_any(flags);
vaddr = (unsigned long)page_address(page);
BUG_ON(!vaddr);
nr = PKMAP_NR(vaddr);
/*
* A count must never go down to zero
* without a TLB flush!
*/
need_wakeup = 0;
switch (--pkmap_count[nr]) {//減一
case 0:
BUG();
case 1:
/*
* Avoid an unnecessary wake_up() function call.
* The common case is pkmap_count[] == 1, but
* no waiters.
* The tasks queued in the wait-queue are guarded
* by both the lock in the wait-queue-head and by
* the kmap_lock. As the kmap_lock is held here,
* no need for the wait-queue-head's lock. Simply
* test if the queue is empty.
*/
need_wakeup = waitqueue_active(&pkmap_map_wait);
}
unlock_kmap_any(flags);
/* do wake-up, if needed, race-free outside of the spin lock */
if (need_wakeup)
wake_up(&pkmap_map_wait);
}
轉自: http://bbs.chinaunix.net/thread-1938084-1-1.html