天天看點

linux arm的高端記憶體映射(2) 永久映射和臨時映射一、永久映射和臨時映射:

一、永久映射和臨時映射:

2.1、永久映射:

依然把前面描述vmalloc的文章的圖搬上來:

high_memory
VMALLOC_START
Vmalloc 1
4KB隔離帶
Vmalloc 2
4KB隔離帶
Vmalloc 3
4KB隔離帶
……..
VMALLOC_END
PKMAP_BASE
8MB隔離帶
Vmalloc N
8KB隔離帶
低端(實體)記憶體映射
FIXADDR_START
永久映射區
臨時映射區
FIXADDR_TOP

事實上這個圖有些誤導,永久映射區始于PKMAP_BASE,但并非結束于FIXADDR_START,在檔案arch/arm/include/asm/highmem.h中描述了永久映射區到底是在哪裡:

#define PKMAP_BASE                        (PAGE_OFFSET - PMD_SIZE)

PKMAP_BASE是永久映射區的起始,在這裡的arm裝置中,PKMAP_BASE是在核心使用者分界點之下2MB處,下面這個宏LAST_PKMAP辨別永久映射區有多少個條目,注意每個條目是可以映射1頁:

#define LAST_PKMAP                PTRS_PER_PTE

在這裡的arm裝置中,LAST_PKMAP值為512,即512個條目,每個條目映射一頁,即可最多映射4KB * 512 = 2MB大小,是以永久映射區的範圍是[PAGE_OFFSET – 2MB,PAGE_OFFSET],對這裡的arm裝置就是[0xBFE00000,0xC0000000];這也證明了前面文章說的,高端記憶體空間不一定就是在high_memory之上的位址空間;

要清楚一個問題就是,一般來說,永久映射包括臨時映射是幹什麼用的,可以說在實體記憶體不大時(如這裡的arm裝置是256MB)并且實體位址起始偏移(PHYS_OFFSET)不很大基本上沒有意義,因為所有實體記憶體都可以映射在核心低端記憶體位址空間,使用簡單的偏移即可實作實體位址和虛拟位址的映射,根本用不着什麼永久映射,永久映射包括臨時映射事實上是在核心空間無法完全容納實體記憶體時(比如超過1G的實體記憶體)才會顯出作用;

不論是永久映射還是臨時映射,本質上都是建立一個二級映射,把實體位址和虛拟位址對應起來,需要注意的是兩者的差別,永久映射是有可能睡眠的,原因在後面描述時會很明顯發現,而臨時映射不會睡眠,另外永久映射并非一經映射無法解除,臨時映射的特點顧名思義,可以不斷的覆寫之前的映射關系;

下面看看永久映射的情況:

void *kmap(struct page *page)

{

         might_sleep();

         if (!PageHighMem(page))

                   return page_address(page);

         return kmap_high(page);

}

kmap函數是體系結構自己實作,這裡的arm裝置就是在arch/arm/mm/highmem.c檔案中,kmap函數的作用是:建立這個屬于高端記憶體的實體頁與永久核心映射區的映;

kmap的參數page是要映射的高端實體頁位址,如果它是低端實體頁,則直接傳回它對應的虛拟位址;如果它确實是高端實體頁位址,則調用函數kmap_high建立一個高端記憶體實體頁的虛拟映射即永久映射:

void *kmap_high(struct page *page)

{

         unsigned long vaddr;

         lock_kmap();

         vaddr = (unsigned long)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;

}

首先檢視這個高端實體頁是否已經做過高端映射了,通過函數page_address檢視,

void *page_address(struct page *page)

{

         unsigned long flags;

         void *ret;

         struct page_address_slot *pas;

         if (!PageHighMem(page))

                   return lowmem_page_address(page);

         pas = page_slot(page);

         ret = NULL;

         spin_lock_irqsave(&pas->lock, flags);

         if (!list_empty(&pas->lh)) {

                   struct page_address_map *pam;

                   list_for_each_entry(pam, &pas->lh, list) {

                            if (pam->page == page) {

                                     ret = pam->virtual;

                                     goto done;

                            }

                   }

         }

done:

         spin_unlock_irqrestore(&pas->lock, flags);

         return ret;

}

注意高端位址是通過哈希連結清單管理的,這裡找出該實體頁所在的哈希連結清單的表頭,然後通過周遊找到page描述符所映射的虛拟位址來判斷是否映射過,若映射過則傳回其映射的虛拟位址,否則傳回NULL;

回到函數kmap_high,如果從page_address傳回NULL,說明這個高端頁不存在高端映射,應該建立一個映射,即為這個高端頁配置設定一個虛拟位址,調用函數map_new_virtual:

static inline unsigned long map_new_virtual(struct page *page)

{

         unsigned long vaddr;

         int count;

start:

         count = LAST_PKMAP;

         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;      

                   if (--count)

                            continue;

                   {

                            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();

                            if (page_address(page))

                                     return (unsigned long)page_address(page);

                            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;

}

全局變量pkmap_count和last_pkmap_nr用于管理全部512個永久映射條目,這裡先在for循環中周遊查找一個空白條目,如果找不到說明全部512個條目都正在映射着,如再需映射需要等其中某一個釋放才行,這就需要等這個事件的發生,是以會睡眠,這就是為什麼在中斷上下文等不能睡眠的地方不能使用永久映射的具體原因;

在找到空白條目後會退出for循環建立映射,通過函數PKMAP_ADDR在永久映射區找一個位置,其實全局變量last_pkmap_nr儲存目前永久映射的個數,是以下面的源碼就很好解釋了,就是移位:

#define PKMAP_ADDR(nr)                (PKMAP_BASE + ((nr) << PAGE_SHIFT))

接下來建立這個映射關系,建立的依然是二級映射,隻是二級頁表不用動态申請了,全局變量pkmap_page_table是二級頁表條目的數組。

最後把這個實體頁位址和虛拟位址組成的映射節點(結構類型struct page_address_map)插入到管理永久映射的哈希連結清單中,調用函數set_page_address,注意不論是建立映射還是釋放映射都會調用這個函數:

void set_page_address(struct page *page, void *virtual)

{

         unsigned long flags;

         struct page_address_slot *pas;

         struct page_address_map *pam;

         BUG_ON(!PageHighMem(page));

         pas = page_slot(page);

         if (virtual) {                

                   BUG_ON(list_empty(&page_address_pool));

                   spin_lock_irqsave(&pool_lock, flags);

                   pam = list_entry(page_address_pool.next,

                                     struct page_address_map, list);

                   list_del(&pam->list);

                   spin_unlock_irqrestore(&pool_lock, flags);

                   pam->page = page;

                   pam->virtual = virtual;

                   spin_lock_irqsave(&pas->lock, flags);

                   list_add_tail(&pam->list, &pas->lh);

                   spin_unlock_irqrestore(&pas->lock, flags);

         }

    else {                 

                   spin_lock_irqsave(&pas->lock, flags);

                   list_for_each_entry(pam, &pas->lh, list) {

                            if (pam->page == page) {

                                     list_del(&pam->list);

                                     spin_unlock_irqrestore(&pas->lock, flags);

                                     spin_lock_irqsave(&pool_lock, flags);

                                     list_add_tail(&pam->list, &page_address_pool);

                                     spin_unlock_irqrestore(&pool_lock, flags);

                                     goto done;

                            }

                   }

                   spin_unlock_irqrestore(&pas->lock, flags);

         }

done:

         return;

}

2.2、臨時映射:

臨時映射和永久映射差别不是很大,隻是說臨時映射顧名思義,比較臨時,和永久相反,永久映射若需要修改映射關系需要先釋放映射再建立新的映射關系,而臨時映射就是直接再映射一次把前面的映射關系覆寫掉就行了,确實是比較臨時;是以臨時映射不會睡眠,它不會像永久映射,可能産生條目滿需要等待其他條目被釋放的情況,如果條目滿了它隻需覆寫掉一個就行了;

對于arm體系結構,在檔案arch/arm/include/asm/fixmap.h檔案中定義了臨時映射的位址範圍:

#define FIXADDR_START          0xfff00000UL

#define FIXADDR_TOP              0xfffe0000UL

#define FIXADDR_SIZE              (FIXADDR_TOP - FIXADDR_START)

#define FIX_KMAP_BEGIN       0

#define FIX_KMAP_END           (FIXADDR_SIZE >> PAGE_SHIFT)

臨時映射位址範圍為[0xfff00000,0xfffe0000],長度為896KB即224頁的範圍,顯然FIX_KMAP_BEGIN和FIX_KMAP_END是臨時映射條目個數的下标。

通過函數kmap_atomic建立臨時映射:

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

{

         unsigned int idx;

         unsigned long vaddr;

         void *kmap;

         pagefault_disable();

         if (!PageHighMem(page))

                   return page_address(page);

         debug_kmap_atomic(type);

         kmap = kmap_high_get(page);

         if (kmap)

                   return kmap;

         idx = type + KM_TYPE_NR * smp_processor_id();

         vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);

#ifdef CONFIG_DEBUG_HIGHMEM

         BUG_ON(!pte_none(*(TOP_PTE(vaddr))));

#endif

         set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);

         local_flush_tlb_kernel_page(vaddr);

         return (void *)vaddr;

}

這部分源碼的道理和永久映射的kmap非常相似,就不解釋了。

高端映射其實還有很多應該了解的東西,後續随着應用不斷補充吧。