浅析linux内核内存管理之高端内存
作者:李万鹏
进程可以寻址4G,其中0~3G为用户态,3G~4G为内核态。如果内存不超过1G那么最后这1G线性空间足够映射物理内存了,如果物理内存大于1G,为了使内核空间的1G线性地址可以访问到大于1G的物理内存,把物理内存分为两部分,0~896MB的进行直接内存映射,也就是说存在一个线性关系:virtual address = physical address + PAGE_OFFSET,这里的PAGE_OFFSET为3G。还剩下一个128MB的空间,这个空间作为一个窗口动态进行映射,这样就可以访问大于1G的内存,但是同一时刻内核空间还是只有1G的线性地址,只是不同时刻可以映射到不同的地方。综上,大于896MB的物理内存就是高端内存,内核引入高端内存这个概念是为了通过128MB这个窗口访问大于1G的物理内存。
上图是内核空间1G线性地址的布局,直接映射区为PAGE_OFFSET~PAGE_OFFSET+ 896MB,直接映射的物理地址末尾对应的线性地址保存在high_memory变量中。直接映射区后边有一个8MB的保护区,目的是用来"捕获"对内存的越界访问。然后是非连续内存区,范围从VMALLOC_START~VMALLOC_END,出于同样的原因,每个非连续内存区之间隔着4KB。永久内核映射区从PKMAP_BASE开始,大小为2MB(启动PAE)或4MB。后边是固定映射区,范围是FIXADDR_START~FIXADDR_TOP,至于临时内核映射区是永久内核映射区里的一部分,在后边会做详细解析。
下边来详细介绍高端内存的三种访问方式:非连续内存区访问,永久内核映射,临时内核映射。
非连续内存区访问:
非连续内存区访问会使用一个vm_struct结构来描述每个非连续内存区:
- struct vm_struct {
- void *addr;
- unsigned long size;
- unsigned long flags;
- struct page **pages;
- unsigned int nr_pages;
- unsigned long phys_addr;
- struct vm_struct *next;
- };
vm_struct与vmalloc()分配的非连续线性区有如下关系:
下边来看非连续内存区的分配,分配调用了vmalloc()函数:
- void *vmalloc(unsigned long size)
- {
- return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
- }
flags标志中设置了从high memory分配。
- void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
- {
- struct vm_struct *area;
- struct page **pages;
- unsigned int nr_pages, array_size, i;
-
- size = PAGE_ALIGN(size);
-
- if (!size || (size >> PAGE_SHIFT) > num_physpages)
- return NULL;
-
- area = get_vm_area(size, VM_ALLOC);
- if (!area)
- return NULL;
-
- nr_pages = size >> PAGE_SHIFT;
-
- array_size = (nr_pages * sizeof(struct page *));
-
- area->nr_pages = nr_pages;
-
-
- if (array_size > PAGE_SIZE)
- pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL);
- else
- pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
- area->pages = pages;
-
- if (!area->pages) {
- remove_vm_area(area->addr);
- kfree(area);
- return NULL;
- }
-
- memset(area->pages, 0, array_size);
-
- for (i = 0; i < area->nr_pages; i++) {
- area->pages[i] = alloc_page(gfp_mask);
- if (unlikely(!area->pages[i])) {
-
- area->nr_pages = i;
- goto fail;
- }
- }
-
- if (map_vm_area(area, prot, &pages))
- goto fail;
- return area->addr;
-
- fail:
- vfree(area->addr);
- return NULL;
- }
调用get_vm_area()分配描述符和获得线性地址空间,get_vm_area()函数在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域。步骤如下:
1. 调用kmalloc()为vm_struct类型的新描述符获得一个内存区。
2. 为写得到vmlist_lock()锁,并扫描类型为vm_struct的描述符链表来查找线性地址一个空闲区域,至少覆盖size + 4096个地址。
3. 如果存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock锁,并以返回这个非连续内存区的起始地址而结束。
4. 否则,get_vm_area()释放先前得到的描述符,释放vmlist_lock,然后返回NULL。
调用map_vm_area()建立页表与物理页之间的映射:
- int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
- {
- unsigned long address = (unsigned long) area->addr;
- unsigned long end = address + (area->size-PAGE_SIZE);
- unsigned long next;
- pgd_t *pgd;
- int err = 0;
- int i;
-
- pgd = pgd_offset_k(address);
-
- spin_lock(&init_mm.page_table_lock);
- for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
-
- pud_t *pud = pud_alloc(&init_mm, pgd, address);
- if (!pud) {
- err = -ENOMEM;
- break;
- }
-
- next = (address + PGDIR_SIZE) & PGDIR_MASK;
- if (next < address || next > end)
- next = end;
-
- if (map_area_pud(pud, address, next, prot, pages)) {
- err = -ENOMEM;
- break;
- }
-
- address = next;
- pgd++;
- }
-
- spin_unlock(&init_mm.page_table_lock);
- flush_cache_vmap((unsigned long) area->addr, end);
- return err;
- }
map_area_pud也就是反复调用map_area_pmd来填充各级页目录,页表。map_area_pte()的主循环如下:
- do {
- struct page *page = **pages;
- WARN_ON(!pte_none(*pte));
- if (!page)
- return -ENOMEM;
-
- set_pte(pte, mk_pte(page, prot));
- address += PAGE_SIZE;
- pte++;
- (*pages)++;
- } while (address < end);
调用set_pte设置将相应页的页描述符地址设置到相应的页表项。非连续内存区的释放:
- #define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
- #define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
- #define set_pte(pteptr, pteval) (*(pteptr) = pteval)
需要注意的是,map_vm_area()并不触及当前进程的页表。
非连续内存区的释放:
调用vfree()函数:
- void vfree(void *addr)
- {
- BUG_ON(in_interrupt());
- __vunmap(addr, 1);
- }
__vunmap()调用remove_vm_area(),执行与map_vm_area()相反的操作,最终调用到了unmap_area_pte():
- do {
- pte_t page;
- page = ptep_get_and_clear(pte);
- address += PAGE_SIZE;
- pte++;
- if (pte_none(page))
- continue;
- if (pte_present(page))
- continue;
- printk(KERN_CRIT "Whee.. Swapped out page in kernel page table\n");
- } while (address < end);
这里调用ptep_get_and_clear()宏将pte指向的页表项设为0。
注意,在调用vmalloc()时建立的映射是在物理内存和主内核页表之间的,并没有涉及到进程的页表。当内核态的进程访问非连续内存区时,缺页发生,因为该内存区所对应的进程页表中的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中。一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。在调用vfree时,与vmalloc()一样,内核修改主内核页全局目录和它的子页表中相应的项,但是映射第4个GB的进程页表的项保持不变。unmap_area_pte()函数只是清除页表中的项(不回收页表本身)。进程对已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。
永久内核映射:
永久内核映射使用主内核页表中一个专门的页表,其地址存放在pkmap_page_table变量中,页表中的表项数由LAST_PKMAP宏产生,因此内核一次访问2MB(启动PAE)或4MB的高端内存。该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含LAST_PKMAPGE个计数器,pkmap_page_table页表中每一项都有一个。
计数器为0
对应的页表项没有映射任何高端内存页框,并且是可用的。
计数器为1
对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的TLB表项还未被刷新。
计数器为2
相应的页表项映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。
- struct page_address_map {
- struct page *page;
- void *virtual;
- struct list_head list;
- };
-
- static struct page_address_slot {
- struct list_head lh;
- spinlock_t lock;
- } ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];<span style="font-family: Arial, Verdana, sans-serif; font-size: 18px; white-space: normal; "> </span>
- static struct page_address_slot *page_slot(struct page *page)
- {
- return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
- }
为了记录高端内存页框与永久内核映射的线性地址之间的联系,内核使用了page_address_htable散列表,该表包含一个page_address_htable结构,该表包含一个page_address_map数据结构,用于为高端内存每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。page是一个指向全局mem_map数组中的page实例的指针,virtual指定了该页在内核虚拟地址空间中分配的位置。为了便于组织,映射保存在散列表中,结构中的链表元素用于建立溢出的链表,以处理散列碰撞。散列表为page_address_htable,散列函数是page_slot,根据page实例确定页的虚拟地址。如果page是在普通内存中的,则根据page在mem_map数组中的位置计算。
进行永久内核映射需要调用kmap()函数:
- void *kmap(struct page *page)
- {
- might_sleep();
- if (!PageHighMem(page))
- return page_address(page);
- return kmap_high(page);
- }
kmap函数只是一个前端,用于确定指定的页是否确实在高端内存域中。首先判断是不是高端内存,如果不是高端内存就调用page_address直接返回page对应的线性地址,0~896MB的是直接映射,也就是在kernel初始化的时候映射已经建立好了,之后直接访问就行了。而高端内存需要自己建立映射,然后才能访问。如果是高端内存则将工作委托给kmap_high:
- void fastcall *kmap_high(struct page *page)
- {
- unsigned long vaddr;
-
-
-
-
-
-
-
- spin_lock(&kmap_lock);
- vaddr = (unsigned long)page_address(page);
- if (!vaddr)
- vaddr = map_new_virtual(page);
- pkmap_count[PKMAP_NR(vaddr)]++;
- if (pkmap_count[PKMAP_NR(vaddr)] < 2)
- BUG();
- spin_unlock(&kmap_lock);
- return (void*) vaddr;
- }
首先获得需要映射的page对应的线性地址,从page_address_htable中进行查找,如果已经映射过了肯定不为空,如果没有映射过则执行map_new_virtual进行映射。注意这里进行的pkmap_count加一操作,后边会提到。
- 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);
- 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);
-
-
- goto start;
- }
- }
- vaddr = PKMAP_ADDR(last_pkmap_nr);
- set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
-
- kmap_count[last_pkmap_nr] = 1;
- set_page_address(page, (void *)vaddr);
-
- return vaddr;
- }
内核将上次使用过的页表项的索引保存在last_pkmap_nr变量中,避免了重复查找。如果找到计数器为0的,则获得这个页表项对应的页表的线性地址,填充相应的页表项,计数器为1。这里产生一个问题,刚才不是说为1代表“对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的TLB表项还未被刷新。”,确实是这样,看看调用map_new_virtual函数的kmap_high函数,在这里对pkmap_count进行了再次加一。
- void kunmap(struct page *page)
- {
- if (in_interrupt())
- BUG();
- if (!PageHighMem(page))
- return;
- kunmap_high(page);
- }
kmap会导致进程阻塞,所以永久内核映射不能运行在中断处理程序中和可延迟函数的内部。所以这里kunmap首先判断是不是在中断上下文中,如果不在中断上下文并且不再高端内存,则将工作委托kunmap_high。如果有计数器为1的表项,而且有等待的进程,则唤醒进程。如果有进程被阻塞那么肯定没有计数器为0的表项了,所以从map_new_virtual中start标号的地方开始执行,应该会把表项遍历一圈,此时肯定会遇到last_pkmap_nr为0的情况,此时就可以调用flush_all_zero_pkmaps()函数来寻找计数器为1的表项,将其清零,解除映射,并刷新TLB。可以看到kunmap并没有解除映射,并刷新tlb,这里仅仅是将表项的计数器减一。为什么要这样做呢?看一下kmap_high函数中,判断了一次是否已经映射了,如果映射了就把表项的引用计数器加一,也就是说如果调用过kunmap使计数为1了,此时又变成2,又可以进行访问了,不用重新填充页表,刷新tlb等。
- void fastcall kunmap_high(struct page *page)
- {
- unsigned long vaddr;
- unsigned long nr;
- int need_wakeup;
-
- spin_lock(&kmap_lock);
- vaddr = (unsigned long)page_address(page);
- if (!vaddr)
- BUG();
- nr = PKMAP_NR(vaddr);
-
-
-
-
-
- need_wakeup = 0;
- switch (--pkmap_count[nr]) {
- case 0:
- BUG();
- case 1:
- need_wakeup = waitqueue_active(&pkmap_map_wait);
- }
- spin_unlock(&kmap_lock);
-
-
- if (need_wakeup)
- wake_up(&pkmap_map_wait);