在vmalloc()的实现过程中,首先遍历vmlist链表找到一个所需大小的vmalloc区,接着为这个子内存区依次分配物理页框。从这两部分的实现过程可以看到,虚拟的子内存区是在整个vmalloc区中查找一块合适大小的子区域,而物理内存则是分散的依次分配。因此,vmalloc()的实现就落在了如何将这些分散的物理页框映射到连续的vmalloc区上。整个映射过程可以简单的看作是不断修改内核页表的过程。
map_vm_area()的实现
vmalloc()通过map_vm_area()完成物理页框与虚拟子内存区的映射。map_vm_area()首先计算虚拟内存子区域的起始地址addr和终止地址end。由于vm_struct中的size是实际子区间长度加上一个页大小,因此终止地址必须减去PAGE_SIZE。接着它调用了vmap_page_range()。下面的代码表明了map_vm_area()的实际调用过程(本文所有内核源码均取自v2.6.34)。
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages) { unsigned long addr = (unsigned long)area->addr; unsigned long end = addr + area->size - PAGE_SIZE; int err; err = vmap_page_range(addr, end, prot, *pages); if (err > 0) { *pages += err; err = 0; } return err; }
vmap_page_range()又再次封装了真正用于映射的函数vmap_page_range_noflush()。当映射完毕后,它调用flush_cache_vmap(),即将刚刚修改的内核页表项刷新到CPU高速缓存。
static int vmap_page_range(unsigned long start, unsigned long end, pgprot_t prot, struct page **pages) { int ret; ret = vmap_page_range_noflush(start, end, prot, pages); flush_cache_vmap(start, end); return ret; }
修改内核页表
map_vm_area()经过重重调用,终于来到了进行实际映射工作的vmap_page_range_noflush()中。这个函数中首先通过pgd_offset_k()计算出addr在主内核页全局目录中对应的页表项地址。接着通过一个循环,为start到end之间的子内存区修改内核页表。 在每次循环的过程中,next为当前页表项所映射的内存区的终止地址。通过vmap_pud_range()继续修改start到next之间所对应的页上级目录。
static int vmap_page_range_noflush(unsigned long start, unsigned long end, pgprot_t prot, struct page **pages) { pgd_t *pgd; unsigned long next; unsigned long addr = start; int err = 0; int nr = 0; BUG_ON(addr >= end); pgd = pgd_offset_k(addr); do { next = pgd_addr_end(addr, end); err = vmap_pud_range(pgd, addr, next, prot, pages, &nr); if (err) return err; } while (pgd++, addr = next, addr != end); return nr; }
vmap_pud_range()所做的工作和vmap_page_range_noflush()类似,只不过它是针对页上级目录的页表项做出对应的修改。首先pud_alloc()为addr分配对应的页上级目录,同时也将该页上级目录对应的物理地址写入页全局目录对应的表项中。接着再次进入一个循环来依次修改addr对应的页中间目录,每次循环时计算出当前页上级目录所映射的内存区间范围addr和next。
static int vmap_pud_range(pgd_t *pgd, unsigned long addr, unsigned long end, pgprot_t prot, struct page **pages, int *nr) { pud_t *pud; unsigned long next; pud = pud_alloc(&init_mm, pgd, addr); if (!pud) return -ENOMEM; do { next = pud_addr_end(addr, end); if (vmap_pmd_range(pud, addr, next, prot, pages, nr)) return -ENOMEM; } while (pud++, addr = next, addr != end); return 0; }
vmap_pmd_range()为页中件目录所指向的所有页表执行和上述类似的循环。
static int vmap_pmd_range(pud_t *pud, unsigned long addr, unsigned long end, pgprot_t prot, struct page **pages, int *nr) { pmd_t *pmd; unsigned long next; pmd = pmd_alloc(&init_mm, pud, addr); if (!pmd) return -ENOMEM; do { next = pmd_addr_end(addr, end); if (vmap_pte_range(pmd, addr, next, prot, pages, nr)) return -ENOMEM; } while (pmd++, addr = next, addr != end); return 0; }
现在为addr更新最后一级页表,首先由pte_alloc_kernel()生成页表pte,再通过循环依次更新每个页表项。在每次循环过程中,先通过pages数组得到第nr个页框的页描述符page,再将其传入mk_pte生成对应页表项。最后由set_pte_at()将此页表项更新到pte所指的页表的对应项中。页表项的增加过程由当前线性地址addr加上PAGE_SIZE完成。
static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end, pgprot_t prot, struct page **pages, int *nr) { pte_t *pte; pte = pte_alloc_kernel(pmd, addr); if (!pte) return -ENOMEM; do { struct page *page = pages[*nr]; if (WARN_ON(!pte_none(*pte))) return -EBUSY; if (WARN_ON(!page)) return -ENOMEM; set_pte_at(&init_mm, addr, pte, mk_pte(page, prot)); (*nr)++; } while (pte++, addr += PAGE_SIZE, addr != end); return 0; }
经过对每级页表的层层修改,最终start到end之间的连续vmalloc区都与相应的物理页框相映射。整个vmalloc()完成。