Linux内存之页表

Linux的相关页表的框架在 vmalloc分析中,__vmalloc_area_node调用流程与分配页表项两节中已经有所阐述。本文再做一些总体上的把握。文中会借用其中的一些图。

所谓的页表结构如下。针对现在的64位操作系统,Linux已经可以支持4级页表操作,即pgd,pud,pmd和pte。但目前一般嵌入式的应用并不会使能该功能,所以在代码中涉及到pud和pmd的指针,实际都是指向pgd表项的。
简单介绍下这幅图
     1. 所有Linux都是4G的地址空间,每个表项代表2MB空间,共2K表项,占2个page
     2. 所有的内存在表中都是有序的,即物理地址在这个空间中也是有低到高的
     2. 每个pgd表项指向一个page,4KB,分成4个子表如上图。
     3. 下面的两个子表2KB,包含512个表项,正好2MB空间,视为一个pte,其中每个表项指向一个page

     pgd表保存在进程的mm_struct中
     struct mm_struct {
          ……
          pgd_t * pgd;
          ……
     }
     pte_alloc_map / pte_alloc_map_lock / pte_alloc_kernel这几个函数为pgd的每个表项分配他所指向的那一页数据,其实就是pte。
     因为内存会不断的分配,不可能每次分配一个page都会新建这样的一张表格,所以Linux为每个pgd表项(代码中为了兼容,还是将之称为pmd)加入了一些标志位,并在每次分配之前,用宏pmd_present(*(pmd))来检验此pte是否已经分配了,如果已经分配就不会再分配了。这个标志位是怎么分配的呢?vmalloc分析也有所分析。即函数pmd_populate_kernel,其实还有其他函数,例如pmd_populate。他们调用这的最终函数都同是__pmd_populate。即
     pmdp[0] = __pmd(pmdval) ;  // = pmdval
     pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t)); // =  pmdval + 256 * sizeof(pte_t)

     
     
pmd_populate是在为用户空间的地址建立页表,而pmd_populate_kernel是在为内核地址建立页表。所以他们之间的差别便水到渠成了:
     1. 两者为pmd填充的内容不一样
          pmd_populate_kernel:__pa(pte_ptr) | _PAGE_KERNEL_TABLE
          pmd_populate:page_to_pfn(ptep) << PAGE_SHIFT | _PAGE_USER_TABLE
          可见,一个是物理地址+kernel标志,另一个是内核逻辑地址+user标志
     2. 两者传入的参数也不同
          pmd_populate_kernel传入的是内核逻辑地址
          pmd_populate传入的则是page结构指针

     不管怎么样,其实他们的实质是一样的,pmd的内容都是这一个page表格所在的具体位置(物理地址,或者物理地址偏移PAGE_SHIFT的内核逻辑地址)+一个标志内核进程还是用户进程 的标志位。

     至此,pte(或者pmd)的内容就讲清楚了,那有了这页表格后,就顺利成章的该往里面填pte表项了。首先用pte_offset_map / pte_offset_kernel找到表格中对应的表项,然后用函数set_pte_at真正的做填充。set_pte_at最终会调用到一个汇编函数set_pte_ext。如果不考虑高端内存的话,pte_offset_map和pte_offset_kernel是一样的,都
     = pmd_page_vaddr(*(dir)) + __pte_index(addr)
前半部分是pmd的逻辑地址,后半部分是本page在pmd中的偏移量。

=========================分割线===================================

     说了这么多,页表的建立就是这么些事情了。下面再举个小小的例子,是mmap实现的一种方式remap_pfn_range。mmap要做的事情是把一个文件(包括设备文件,如果是设备文件,则要驱动来支持)映射到一款内存地址上,那remap_pfn_range就是要为一段物理page建立页表项。先看他们的原型:
          mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)

     int remap_pfn_range(struct vm_area_struct *vma,
                     unsigned long virt_addr, unsigned long pfn,
                     unsigned long size, pgprot_t prot);
     
     
     上面的mmap是Linux的API,可以针对普通文件和设备文件,所以比较有普遍性,下面是驱动中为了实现mmap经常会用到的remap_pfn_range函数。理一下他们的参数之间的对应关系:
          mmap                               remap_pfn_range
          addr          <=>               vma,   virt_addr
fd,offset,len  <=>               pfn, size
     调用过程就是:
     1. 应用程序调用mmap来映射一个文件fd中offset~offset+len这一部分。
     2. 内核按照传入的参数来,分配一个vma_area_struct,这个结构包含了所有要分配的地址映射的信息,包括物理地址(fd, offset,len)和虚拟地址空间(addr),所以remap_pfn_range的参数virt_addr,pfn,size,都在vma中有所描述了。至于为什么要这样做,我猜是为了提供灵活性,毕竟vma可以超过我们需要映射的地址空间,比如内核在分配vma时发现有两个vma可以合并,则传入的vma就大于我们需要的空间,这样就可以用virt_addr,pfn,size来说明我们需要的映射。
     remap_pfn_range最核心的部分就是为virt_addr~virt_addr+PAGE_ALIGN(size)的区间建立页表,代码如下

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
            unsigned long pfn, unsigned long size, pgprot_t prot)
{
     ……
     do {
        next = pgd_addr_end(addr, end);
        err = remap_pud_range(mm, pgd, addr, next,
                pfn + (addr >> PAGE_SHIFT), prot);
        if (err)
            break;
    } while (pgd++, addr = next, addr != end);
     ……
}

     remap_pud_range -> remap_pmd_range -> remap_pte_range
     前面两个是虚的,最后一个就是建立页表了
     static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
            unsigned long addr, unsigned long end,
            unsigned long pfn, pgprot_t prot)
{
    ……
    pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
    ……
    do {
        set_pte_at(mm, addr, pte, pte_mkspecial(pfn_pte(pfn, prot)));
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr != end);
    ……
}

传入的参数
1. pmd就是我们之前提到的pgd表项指向的那1个page
2. addr~end是要建立页表的虚拟地址范围
3. pfn是这块虚拟地址范围首地址的物理地址帧号
4. prot是一些标志位