Linux内存之页表
Linux的相关页表的框架在 vmalloc分析中,__vmalloc_area_node调用流程与分配页表项两节中已经有所阐述。本文再做一些总体上的把握。文中会借用其中的一些图。
unsigned long size, pgprot_t prot);
err = remap_pud_range(mm, pgd, addr, next,
pfn + (addr >> PAGE_SHIFT), prot);
if (err)
break;
} while (pgd++, addr = next, addr != end);
unsigned long pfn, pgprot_t prot)
所谓的页表结构如下。针对现在的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)
{
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是一些标志位