1. 程式人生 > >linux 高階記憶體頁框管理:永久核心對映、臨時核心對映以及非連續記憶體分配

linux 高階記憶體頁框管理:永久核心對映、臨時核心對映以及非連續記憶體分配

摘要:高階記憶體頁框的核心對映分為三種情況:永久核心對映、臨時核心對映和非連續記憶體對映。那麼這三者有什麼區別和聯絡呢?臨時核心對映如何保證不會被阻塞呢?本文主要為你解答這些疑問,並詳細探討高階記憶體對映的前兩種方式。

1.高階記憶體的區域劃分

核心將高階記憶體劃分為3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。

對 於高階記憶體,可以通過 alloc_page() 或者其它函式獲得對應的 page,但是要想訪問實際實體記憶體,還得把 page 轉為線性地址才行(為什麼?想想 MMU 是如何訪問實體記憶體的),也就是說,我們需要為高階記憶體對應的 page 找一個線性空間,這個過程稱為高階記憶體對映。

對應高階記憶體的3部分,高階記憶體對映有三種方式
對映到”核心動態對映空間”(noncontiguous memory allocation)
這種方式很簡單,因為通過 vmalloc() ,在”核心動態對映空間”申請記憶體的時候,就可能從高階記憶體獲得頁面(參看 vmalloc 的實現),因此說高階記憶體有可能對映到”核心動態對映空間”中。

持久核心對映(permanent kernel mapping)
如果是通過 alloc_page() 獲得了高階記憶體對應的 page,如何給它找個線性空間?
核心專門為此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用於對映高階記憶體。在 2.6核心上,

這個地址範圍是 4G-8M 到 4G-4M 之間。這個空間起叫”核心永久對映空間”或者”永久核心對映空間”。這個空間和其它空間使用同樣的頁目錄表,對於核心來說,就是 swapper_pg_dir,對普通程序來說,通過 CR3 暫存器指向。通常情況下,這個空間是 4M 大小,因此僅僅需要一個頁表即可(注意理解這句話:一個頁表(不是頁表項),大小為4K,可以對映4M的空間),核心通過來 pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 對映到這個空間來。由於這個空間是 4M 大小,最多能同時對映 1024 個 page。因此,對於不使用的的 page,及應該時從這個空間釋放掉(也就是解除對映關係),通過 kunmap() ,可以把一個 page 對應的線性地址從這個空間釋放出來。

臨時對映(temporary kernel mapping)
核心在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用於特殊需求。這個空間稱為”固定對映空間”在這個空間中,有一部分用於高階記憶體的臨時對映。

這塊空間具有如下特點:
(1)每個 CPU 佔用一塊空間
(2)在每個 CPU 佔用的那塊空間中,又分為多個小空間,每個小空間大小是 1 個 page,每個小空間用於一個目的,這些目的定義在 kmap_types.h 中的 km_type 中。

當要進行一次臨時對映的時候,需要指定對映的目的,根據對映目的,可以找到對應的小空間,然後把這個空間的地址作為對映地址。這意味著一次臨時對映會導致以前的對映被覆蓋。通過 kmap_atomic() 可實現臨時對映。

896M邊界以上的頁框並不對映在核心線性地址空間的第4個GB,因此核心不能直接訪問它們。所以,返回所分配頁框線性地址的頁分配器函式並不對高階記憶體可用。

在64位平臺上不存在這個問題,因為可以使用的線性地址空間大於能安裝的RAM,也就是說這些體系結構的ZONE_HIGHMEM是空的。linux使用如下方法來使用高階記憶體:

1)高階記憶體頁框的分配只能通過alloc_pages( )函式和它的快捷函式alloc_page( )。這些函式不返回線性地址,而是返回第一分配頁框的頁描述符的線性地址。

2)沒有線性地址的高階記憶體中的頁框不能被核心訪問。

核心採用三種不同的機制將頁框對映到高階記憶體:永久核心對映、臨時核心對映、非連續記憶體分配。本節討論前兩種。

建立永久核心對映可能阻塞當前程序;也就是高階記憶體上沒有頁表項可以用作頁框的視窗的時候。因此,這種方法不能用在中斷處理函式和可延遲函式。臨時核心對映不會阻塞當前程序,但是隻有很少的臨時核心對映可以建立起來。

需要注意的是,無論哪種方法,128M的線性地址用於高階記憶體對映,無法保證定址範圍同時到達的實體記憶體。

2.永久核心對映:注意,下列多有函式應用的範圍是核心空間

巨集定義與關鍵變數定義:

pkmap_page_table:高階記憶體主核心頁表中,一個用於永久核心對映的專用頁表鎖在的地址

LAST_PKMAP: 上述頁表所含有的表項(512或者1024)

PKMAP_BASE:該頁表所對映線性地址的start地址

pkmap_count:對頁表項提供計數器的陣列

page_address_htable:散列表,用於記錄高階頁框與永久核心對映的線性地址之間的關係

page_address_map:一個數據結構,包含指向頁描述符的指標和分配給頁框的線性地址;用於為高階記憶體的每個頁框提供當前對映,它被包含在page_address_htable這個hansh表中

關鍵函式:

page_address( page):返回頁框對應的線性地址

Void * kmap(struct page * page):返回對應page的線性地址

Void * kmap_high(struct page * page): 同上,不過接受的引數是高階記憶體的頁框描述符

map_new_virtual( ):插入頁框的實體地址到pkmap_page_table,在page_address_htable散列表中加入一個元素

它是高階頁框到核心地址空間的長期對映。使用主核心頁表中的一個專門頁表,地址存放在pkmap_page_table變數中。頁表中的表項數由LAST_PKMAP巨集產生。頁表照樣包含512或者1024項,這取決於PAE是否啟用,因此,核心一次訪問最多2M或者4M的高階記憶體。

該頁表對映的線性地址從PKMAP_BASE開始,pkmap_count陣列包涵LAST_PKMAP個計數器,pkmap_page_table頁表中的每一個項都有一個。我們區分下列三種情況

計數器為0:對應頁表項沒有對映任何的高階記憶體頁框,並且是可用的。

計數器為1:對應的頁表項沒有對映任何記憶體頁框,但是它不可用,因為從它最後一次使用以來,對應的TLB表項還未被重新整理。

計數器為n:相應的頁表項對映一個高階記憶體頁框,這意味著正好有n-1個核心成分在使用這個頁框。

當分配項的值等於0時為自由項,等於1時為緩衝項,大於1時為對映項。對映頁面的分配基於分配表的掃描,當所有的自由項都用完時,系統將清除所有的緩衝項,如果連緩衝項都用完時,系統將進入等待狀態。

為了記錄高階記憶體頁框與永久核心對映的線性地址之間的聯絡,核心使用了page_address_htable散列表。該表包含一個page_address_map資料結構,用於為高階記憶體的每個頁框進行當前對映。而該資料結構還包涵一個指向頁描述符號的指標和分配給該頁框的線性地址。

對應資料結構關係圖如下:


page_address()函式返回頁框對應的線性地址,如果頁框在高階記憶體中並沒有被對映,則返回NULL。這個函式接受一個頁描述符指標page作為引數,並區分以下兩種情況:

1)頁框不在高階記憶體中:

__va( ( unsigned long) (page - meme_map) << 12)

2) 頁框在高階記憶體中,該函式就得到page_address_htable中尋找。如果在散列表中找到頁框,page_address()就返回它的線性地址,否則就返回NULL。

程式碼實現如下:

void *kmap(struct page *page)
{
        might_sleep();
        if (!PageHighMem(page))
                return page_address(page);
        return kmap_high(page);
}
如果頁框確實屬於高階記憶體,那麼呼叫kmap_high()函式如下:
 /* 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 = (unsigned long)page_address(page);//檢查頁框是否已經被對映
        if (!vaddr)//沒有被對映
                vaddr = map_new_virtual(page);//將頁框的實體地址插入到pkmap_page_table並在pkmapa_address_table散列表中加入一個元素
        pkmap_count[PKMAP_NR(vaddr)]++;//頁框的線性地址對應的計數器+1
        BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
        unlock_kmap();
        return (void*) vaddr;
}<span style="font-size:14px">
</span>

其中的一些巨集定義內容如下:
#define PKMAP_BASE              (PAGE_OFFSET - PMD_SIZE)
#define LAST_PKMAP              PTRS_PER_PTE
#define LAST_PKMAP_MASK         (LAST_PKMAP - 1)
#define PKMAP_NR(virt)          (((virt) - PKMAP_BASE) >> PAGE_SHIFT)
#define PKMAP_ADDR(nr)          (PKMAP_BASE + ((nr) << PAGE_SHIFT))

map_new_virtual( )函式本質上是兩個巢狀迴圈,完成的工作是:插入實體地址到hashtable和在對應hashtable中增加一個元素,程式碼如下:
static inline unsigned long map_new_virtual(struct page *page)
{
        unsigned long vaddr;
        int count;

start:
        count = LAST_PKMAP;//固定對映的頁表項個數
        /* Find an empty entry */
        for (;;) {
                last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;//與掩碼進行按位與運算,避免資料過長造成的溢位
                if (!last_pkmap_nr) {//last_pkmap_nr==0,說明它原來已經到達最大值(注意與運算)
                        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;
}


然後,kunmap()函式撤銷原來有kmap()建立的永久核心對映。如果頁處在高階記憶體,呼叫kunmap_high()函式。程式碼如下:
250 void kunmap_high(struct page *page)
251 {
252         unsigned long vaddr;            
253         unsigned long nr;               
254         unsigned long flags;
255         int need_wakeup;
256         
257         lock_kmap_any(flags);
258         vaddr = (unsigned long)page_address(page);
259         BUG_ON(!vaddr);//嵌入式彙編有關的bug處理
260         nr = PKMAP_NR(vaddr);//(((virt) - PKMAP_BASE) >> PAGE_SHIFT)頁號
261 
262         /*
263          * A count must never go down to zero
264          * without a TLB flush!
265          */
266         need_wakeup = 0;
267         switch (--pkmap_count[nr]) {
268         case 0:
269                 BUG();
270         case 1://沒有程序在使用頁
271                 /*
272                  * Avoid an unnecessary wake_up() function call.
273                  * The common case is pkmap_count[] == 1, but
274                  * no waiters.
275                  * The tasks queued in the wait-queue are guarded
276                  * by both the lock in the wait-queue-head and by
277                  * the kmap_lock.  As the kmap_lock is held here,
278                  * no need for the wait-queue-head's lock.  Simply
279                  * test if the queue is empty.
280                  */
281                 need_wakeup = waitqueue_active(&pkmap_map_wait);//喚醒
282         }
283         unlock_kmap_any(flags);
284 
285         /* do wake-up, if needed, race-free outside of the spin lock */
286         if (need_wakeup)
287                 wake_up(&pkmap_map_wait);//喚醒由map_new_virtual()新增在等待佇列中的程序
288 }
289 

3.臨時核心對映:和程序控制有關

臨時核心對映實現簡單,可以用在中斷處理程式和可延遲函式的內部(這些函式不能被阻塞),因為臨時核心對映從來不阻塞當前程序,因為它被設計成是原子的。對比永久核心對映,發現如果頁框暫時沒有空閒的虛擬地址可以對映,那麼永久核心對映將要被阻塞。

建立臨時核心對映禁用核心搶佔,這是必須的,因為對映對於每個處理器都是獨特的,如果沒有禁用搶佔,那麼哪個任務在哪個CPU上執行是不確定的。(這一段需要結合程序管理加以理解)

撤銷臨時核心對映的函式實際上可以不進行任何實質性的操作,它僅僅允許核心搶佔即可(這樣新的程序被排程,可以直接使用臨時核心對映區域,覆蓋原來的對映關係)。

每個CPU都有它自己的包含13個視窗的集合,它們用enum km_type資料結構表示。該資料結構定義的每個符號,標識了一個視窗的線性地址。

<span style="font-size:14px"> </span> 7 enum km_type {
  8         KM_BOUNCE_READ,
  9         KM_SKB_SUNRPC_DATA,
 10         KM_SKB_DATA_SOFTIRQ,
 11         KM_USER0,
 12         KM_USER1,
 13         KM_BIO_SRC_IRQ,
 14         KM_BIO_DST_IRQ,
 15         KM_PTE0,
 16         KM_PTE1,
 17         KM_IRQ0,
 18         KM_IRQ1,
 19         KM_SOFTIRQ0,
 20         KM_SOFTIRQ1,
 21         KM_L1_CACHE,
 22         KM_L2_CACHE,
 23         KM_TYPE_NR
 24 };
其中,核心要確保同一個視窗永遠不會被兩個不同的控制路徑同時使用。最後一個符號非線性地址,但由每個CPU用來產生不同的可用視窗數。

km_type的每一個符號都是固定對映的線性地址的一個下標。enum fixed_addresses資料結構包含符號FIX——KMAP——BEGIN和FIX_KMP_END;把後者的值賦成下標FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。在這種方式下,系統中的每個CPU有KM-TYPE-NR個固定對映的線性地址。此外,核心用fix_to

_virt(FIX_KMAP_BEGIN )線性地址對應的頁表項的地址初始化kmap_pte變數。

 39 void *kmap_atomic(struct page *page, enum km_type type)
 40 {
 41         unsigned int idx;
 42         unsigned long vaddr;
 43         void *kmap;
 44 
 45         pagefault_disable();//有關鎖和核心搶佔機制
 46         if (!PageHighMem(page))
 47                 return page_address(page);
 48 
 49         debug_kmap_atomic(type);//debug點
 50 
 51         kmap = kmap_high_get(page);//類似kmap_high的功能,只有這個函式返回非空指標,才可以呼叫kmap_high()
 52         if (kmap)
 53                 return kmap;
 54 
 55         idx = type + KM_TYPE_NR * smp_processor_id();//指明需要使用的線性地址
 56         vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);//固定對映的線性地址轉化成虛擬地址
 57 #ifdef CONFIG_DEBUG_HIGHMEM
 58         /*
 59          * With debugging enabled, kunmap_atomic forces that entry to 0.
 60          * Make sure it was indeed properly unmapped.
 61          */
 62         BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
 63 #endif
 64         set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);//設定頁表項:線性地址,page 頁框資訊
 65         /*
 66          * When debugging is off, kunmap_atomic leaves the previous mapping
 67          * in place, so this TLB flush ensures the TLB is updated with the
 68          * new mapping.
 69          */
 70         local_flush_tlb_kernel_page(vaddr);//重新整理TLB無效
 71 
 72         return (void *)vaddr;
 73 }

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
68 #define cpu_set_pte_ext(ptep,pte,ext)   processor.set_pte_ext(ptep,pte,ext)

6 #define TOP_PTE(x)      pte_offset_kernel(top_pmd, x)
311 /* Find an entry in the third-level page table.. */
312 extern inline pte_t * pte_offset_kernel(pmd_t * dir, unsigned long address)
313 {
314         pte_t *ret = (pte_t *) pmd_page_vaddr(*dir)
315                 + ((address >> PAGE_SHIFT) & (PTRS_PER_PAGE - 1));
316         smp_read_barrier_depends(); /* see above */
317         return ret;
318 }

371 #define mk_pte(page, pgprot)   pfn_pte(page_to_pfn(page), (pgprot))