对于32位的机器来说,高于896的物理内存在内核中属于高端内存,并没有对内存做一一的映射,系统保留了128M的线性地址空间来临时映射这些高于896M的高端物理内存,该线性地址为3G+768m~4G。返回页框线性地址的页分配函数对于高端内存是无效的,因为高端内存不会自动的映射到某个线性地址。例如__get_free_pages(GFP_HIGH_MEM,0)函数分配高端内存页框时,返回的是NULL;内核可以采用三种方式来使用高端物理内存:***内核映射,临时内核映射和非连续内存分配。建立***内核映射可能会阻塞当前进程的执行,这发生在没有高端内存没有空闲的页表项来做映射的情况下,因此在中断等不能阻塞的代码中不要使用***内核映射。临时内核映射不会发生阻塞的情况,但必须保证没有其他的内核路径在使用同样的临时内核映射。
一、***内存映射
***内核映射使用的是内核主页表中的一个专门的页表,其地址存放在pkmap_page_table中,页表的页表项由宏LAST_PKMAP产生,页表中包含512或者1024项。
该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含了LAST_PKMAP个计数器,pkmap_page_table页表中的每项都有对应一个计数值:
计数器为0:对应的页表项是空闲可用的。
计数器为1:对应的页表项没有映射任何高端内存,但是它不能够使用,因为自从***一次使用以来,其相应的TLB尚未被刷新。
计数器为n:有多个内核成分使用该页表项所对应的页框。
源码分析:
- void fastcall *kmap_high(struct page *page)
- {
- unsigned long vaddr;
- spin_lock(&kmap_lock);
- //page->virtual记录了页框对应的线性地址
- vaddr = (unsigned long)page_address(page);
- //若页框未被映射过,分配新的空闲页表项
- if (!vaddr)
- vaddr = map_new_virtual(page);
- //若是刚分配到了空闲页表项的话,在map_new_virtual()中其count
- //值被设置为了1,在这里再次++
- pkmap_count[PKMAP_NR(vaddr)]++;
- BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
- spin_unlock(&kmap_lock);
- return (void*) vaddr;
- }
- 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; /* Found a usable entry */
- //count变为0的话,意味着当前没有空闲的页表项
- if (--count)
- continue;
- //没有找到空闲的页表项,将当前进程加入到等待队列,进行调度,直到
- //有空闲的页表项或者该页面被别人映射
- {
- DECLARE_WAITQUEUE(wait, current);
- __set_current_state(TASK_UNINTERRUPTIBLE);
- add_wait_queue(&pkmap_map_wait, &wait);
- spin_unlock(&kmap_lock);
- schedule();
- remove_wait_queue(&pkmap_map_wait, &wait);
- spin_lock(&kmap_lock);
- //有可能在该进程睡眠期间,有其它进程对该页面做了内存映射
- 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;
- //将page->virtual的值设为vaddr,ok
- set_page_address(page, (void *)vaddr);
- return vaddr;
- }
二、临时内核映射
临时内核映射比较简单,在内核中,为每个cpu都保存了一组页表项,每个页表项由一个特定的内核成分使用,需要注意的是,不同的内核控制路径不应该同时使用一个页表项,这样的话,会使后一个内核控制路径将前一个内核控制路径设置页表项给冲掉。
建立临时内核映射使用kmap_atomic()函数。
- void *__kmap_atomic(struct page *page, enum km_type type)
- {
- enum fixed_addresses idx;
- unsigned long vaddr;
- //禁止内核抢占,以预防不同内核控制路径使用同一页表项
- inc_preempt_count();
- //非高端内存,不用进行高端内存映射
- if (!PageHighMem(page))
- return page_address(page);
- //得到使用的页表项的下表索引
- idx = type + KM_TYPE_NR*smp_processor_id();
- //得到相关页表项的线性地址
- vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
- //设置对应的页表项
- set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
- local_flush_tlb_one((unsigned long)vaddr);
- return (void*) vaddr;
- }
三、非连续内存分配
下图显示了如何使用高于0xc0000000线性地址的线性地址空间:
- 内存区的开始部分包含的是对前896MB的RAM进行映射的线性地址,直接映射的物理内存的末尾的线性地址保存在high_memory变量中。
- 内存区的结尾位置包含的是固定映射的线性地址。
- 从PKMAP_BASE开始,是用于高端内存***映射的线性地址。
- 其余的线性地址用于非连续内存区,在物理内存映射和***个内存区间有一个8M的安全区,用于捕捉对内存的越界访问,同样道理,插入其它4KB大小的内存区来隔离非连续内存区。
非连续内存区描述符数据结构:
- struct vm_struct {
- void *addr;//内存区***个内存单元的线性地址
- unsigned long size;//内存区的大小加上4K,4K是用来检查越界的内存
- unsigned long flags;//非连续内存的类型,VM_ALLOC表示使用vmalloc分配的内存,VM_MAP表示使用vmap分配的内存,
- //VM_IOREMAP表示用ioremap()分配的内存
- struct page **pages;//非连续内存的的物理页数组
- unsigned int nr_pages;//非连续内存的物理页的个数
- unsigned long phys_addr;
- struct vm_struct *next;//用来将各个非连续内存描述符串联起来
- };
1、分配非连续的内存区
分配函数主要是vmalloc(),vmap(),vmalloc()会去调用__vmalloc_node()函数:
- void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot,
- int node)
- {
- struct vm_struct *area;
- //size要对其为4K的整数倍,因为非连续内存区域是将各个物理页进行映射
- size = PAGE_ALIGN(size);
- if (!size || (size >> PAGE_SHIFT) > num_physpages)
- return NULL;
- //找到一块空闲的线性地址区域,用来映射该非连续内存
- area = get_vm_area_node(size, VM_ALLOC, node);
- if (!area)
- return NULL;
- return __vmalloc_area_node(area, gfp_mask, prot, node);
- }
- void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
- pgprot_t prot, int node)
- {
- struct page **pages;
- unsigned int nr_pages, array_size, i;
- //计算要映射的物理页数
- nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
- //计算vm_struct中pages数组的数组元素个数
- array_size = (nr_pages * sizeof(struct page *));
- //记录下物理页面的数目
- area->nr_pages = nr_pages;
- //为vm_struct中的pages数组分配内存
- if (array_size > PAGE_SIZE) {
- pages = __vmalloc_node(array_size, gfp_mask, PAGE_KERNEL, node);
- area->flags |= VM_VPAGES;
- } else
- pages = kmalloc_node(array_size, (gfp_mask & ~__GFP_HIGHMEM), node);
- area->pages = pages;
- if (!area->pages) {
- remove_vm_area(area->addr);
- kfree(area);
- return NULL;
- }
- memset(area->pages, 0, array_size);
- //为非连续内存进行页面的分配,每次分配一个页面,将其页框指针记录在pages数组中
- for (i = 0; i < area->nr_pages; i++) {
- if (node < 0)
- area->pages[i] = alloc_page(gfp_mask);
- else
- area->pages[i] = alloc_pages_node(node, gfp_mask, 0);
- if (unlikely(!area->pages[i])) {
- /* Successfully allocated i pages, free them in __vunmap() */
- area->nr_pages = i;
- goto fail;
- }
- }
- //将各个物理页框映射到分配好的空闲线性区里面去
- if (map_vm_area(area, prot, &pages))
- goto fail;
- return area->addr;
- fail:
- vfree(area->addr);
- return NULL;
- }
__vmalloc_node()并不触及当前进程的页表,因此当内核态进程访问非连续内存区时,会发生缺页异常,因为对应的进程的相应地址对应的页表项为空。当缺页异常发生时,异常处理程序会到内核主页表(init_mm.pgd页全局目录)中去查看是否有对应的页表项,有的话,就会修改当前进程的页表项,并继续进程的执行。
2、释放非连续的内存区
- void vfree(void *addr)
- {
- BUG_ON(in_interrupt());
- __vunmap(addr, 1);
- }
- void __vunmap(void *addr, int deallocate_pages)
- {
- struct vm_struct *area;
- if (!addr)
- return;
- //释放的地址应该是4k的整数倍
- if ((PAGE_SIZE-1) & (unsigned long)addr) {
- printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
- WARN_ON(1);
- return;
- }
- //移除对应的vm_area数据描述符,解除对各个物理页面的页面映射项
- area = remove_vm_area(addr);
- if (unlikely(!area)) {
- printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
- addr);
- WARN_ON(1);
- return;
- }
- debug_check_no_locks_freed(addr, area->size);
- //需要向伙伴系统归还非连续的物理页
- if (deallocate_pages) {
- int i;
- //将各个物理页面归还给伙伴系统
- for (i = 0; i < area->nr_pages; i++) {
- BUG_ON(!area->pages[i]);
- __free_page(area->pages[i]);
- }
- if (area->flags & VM_VPAGES)
- vfree(area->pages);
- else
- kfree(area->pages);
- }
- kfree(area);
- return;
- }
与vmalloc()一样,该函数修改的是主内核页全局目录和它的页表表项,内核永远不会回收页全局,页上级,页中间目录,也不会回收页表,而进程的页表会指向这些表项。这样的话,假设一个内核进程访问已经释放的非连续内存,最终就会访问到已经被清空的页表表项,从而引发缺页异常,这就是一个错误。