高端内存概述
在32位的系统上,内核占有从第3GB~第4GB的线性地址空间,共1GB大小,内核将其中的前896MB与物理内存的0~896MB进行直接映射,即线性映射,将剩余的128M线性地址空间作为访问高于896M的内存的一个窗口。引入高端内存映射这样一个概念的主要原因就是我们所安装的内存大于1G时,内核的1G线性地址空间无法建立一个完全的直接映射来触及整个物理内存空间,而对于80x86开启PAE的情况下,允许的最大物理内存可达到64G,因此内核将自己的最后128M的线性地址空间腾出来,用以完成对高端内存的暂时性映射。而在64位的系统上就不存在这样的问题了,因为可用的线性地址空间远大于可安装的内存。下图描述了内核1GB线性地址空间是如何划分的
其中可以用来完成上述映射目的的区域为vmalloc area,Persistent kernel mappings区域和固定映射线性地址空间中的FIX_KMAP区域,这三个区域对应的映射机制分别为非连续内存分配,永久内核映射和临时内核映射。
永久内核映射
在内核初始化页表管理机制时,专门用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;
-
-
- 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;
- }
如果该页框之前没被映射到永久内核映射区,则要通过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();
-
-
-
- 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数组找不到计数值为0的页表项时,就要调用flush_all_zero_pkmaps(),将计数值为1的页表项置为0,撤销已经不用了的映射,并且刷新TLB
- <span style="font-size:12px;">static void flush_all_zero_pkmaps(void)
- {
- int i;
- int need_flush = 0;
-
- flush_cache_kmaps();
-
- for (i = 0; i < LAST_PKMAP; i++) {
- struct page *page;
-
-
-
- if (pkmap_count[i] != 1)
- continue;
- pkmap_count[i] = 0;
-
-
- BUG_ON(pte_none(pkmap_page_table[i]));
-
-
-
- page = pte_page(pkmap_page_table[i]);
- pte_clear(&init_mm, (unsigned long)page_address(page),
- &pkmap_page_table[i]);
-
- set_page_address(page, NULL);
- need_flush = 1;
- }
- if (need_flush)
- flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP));
- }</span>
在map_new_virtual()中,当找到一个空闲页表后,还要调用set_page_address()将page与该页表项对应的线性地址进行关联,这里并不是简单的page结构中的virtual给赋上相应的值,而是将该映射的page添加到page_address_htable散列表中,该散列表维护着所有被映射到永久内核映射区的页框,散列表中的每一项记录了页框的page结构地址和映射页框的线性地址。
- <span style="font-size:12px;">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;
- }
- </span>
弄清楚了kmap()为页框建立永久内核映射,那么释放映射的话就容易理解了,当我们需要释放页框的映射时,调用kunmap()函数
- <span style="font-size:12px;">void kunmap(struct page *page)
- {
- if (in_interrupt())
- BUG();
- if (!PageHighMem(page))
- return;
- kunmap_high(page);
- }</span>
- <span style="font-size:12px;">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);
-
-
- need_wakeup = 0;
- switch (--pkmap_count[nr]) {
- case 0:
- BUG();
- case 1:
-
-
- need_wakeup = waitqueue_active(&pkmap_map_wait);
- }
- unlock_kmap_any(flags);
-
-
- if (need_wakeup)
- wake_up(&pkmap_map_wait);
- }
- </span>