Linux高端内存映射(上)
高端内存概述
在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;
- /*
- * 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;
- }