讲述用户空间大块内存映射mmap原理
都知道在用户空间申请使用小块内存是使用sys_brk系统调用,而使用申请大块内存映射则是sys_mmap系统调用,下面就来讲一讲mmap的调用逻辑。
1.建立逻辑上的物理内存与虚拟内存之间的映射关系
大致说一下流程,在使用mmap建立关联关系时有两种情况需要考虑到:(1)匿名映射,即虚拟内存与物理内存的映射; (2)文件与虚拟内存的映射。故进一步考虑我们想要做到映射关系不仅要做到虚拟内存到物理内存的映射关系还需要做到文件到物理内存的映射才行,那么究竟是怎么做到的哩?
其实它首先建立这种映射关系的逻辑上的联系,把相关的数据结构的指向发生改变,比若说把多虚拟内存的操作,即第一篇中struct vm_area_struct的ops成员直接指向要映射文件的对应文件系统的ops,同时也把文件的struct file结构中的相关数据成员指向虚拟内存的vm_area_struct结构,这样就建立起来了内存映射的基本逻辑。当然,若仅仅是匿名映射的话就不用这么麻烦了。下面就以相关代码来验证之:
首先sys_mmap调用sys_mmap_pgoff:
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, pgoff)
{//linux-4.13.16\mm\mmap.c
struct file *file = NULL;
unsigned long retval;
...
file = fget(fd);
...
retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
...
return retval;
}
此处函数逻辑其实不算复杂,函数首先判断若是匿名文件直接调用vm_mmap_pgoff完成映射,否则先获得映射文件的struct file结构然后调用vm_mmap_pgoff完成映射。
而vm_mmap_pgoff函数经过一系列数据处理与函数调用最终会调用do_mmap函数:
//linux-4.13.16\mm\mmap.c
unsigned long do_mmap(struct file *file, unsigned long addr,...){
struct mm_struct *mm = current->mm;
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags);
...
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
return addr;
}
该函数中get_unmapped_area用于找到虚拟内存中目前还未映射的内存空间,而无论是对于匿名映射还是文件映射其实他们的本质就是在mm_struct的管理各虚拟区域数据结构的红黑树中找到在内存映射区中的最靠近vm_area_struct然后返回地址。而mmap_region函数就用以构建我们需要的vm_area_struct结构,然它何须前一个vm_area_struct哩?
unsigned long mmap_region(struct file *file, unsigned long addr,...)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
...
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);
if (file) {
vma->vm_file = get_file(file);
error = call_mmap(file, vma);
...
addr = vma->vm_start;
vm_flags = vma->vm_flags;
}
...
vma_link(mm, vma, prev, rb_link, rb_parent);
由vma_merge可知,他会先验证前一个vm_area_struct以及和其相连的区域能否满足我们的要求,若可以的话直接就进行收尾工作,否则就需要调用kmem_cache_zalloc函数利用我们上一章讲的slub技术在vm_area_cachep缓冲区中分配vm_area_struct结构,返回之后对其进行初始化,当然还是要分为匿名映射和文件映射两种。而且若是文件映射还会调用call_mmap函数将修改vm_area_struct的fops指向为文件的fops;
在这之后,函数会调用vma_link函数完成此阶段的最后一步:
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
struct address_space *mapping = NULL;
if (vma->vm_file) {
mapping = vma->vm_file->f_mapping;
i_mmap_lock_write(mapping);
}
//将新创建的vm_area_struct挂在mm_struct中管理的红黑树上
__vma_link(mm, vma, prev, rb_link, rb_parent);
__vma_link_file(vma);//将文件的struct file 的address_space的一个成员指针指向va_area_struct
if (mapping)
i_mmap_unlock_write(mapping);
mm->map_count++;
validate_mm(mm);
}
address_space直到今天才为我解开了一番神秘面纱,简单来说他好像就是用于管理文件在内存中映射关系的一个结构体,我们先获取到struct file的address_space指针mapping,然后将当前的vm_area_struct插在mapping->i_mmap上:
void vma_interval_tree_insert(struct vm_area_struct *node,
struct rb_root *root);
static void __vma_link_file(struct vm_area_struct *vma)
{
struct file *file;
file = vma->vm_file;
if (file) {
struct address_space *mapping = file->f_mapping;
vma_interval_tree_insert(vma, &mapping->i_mmap);
...
}
}
巴拉巴拉好久之后发现搞了大半天和物理内存还没扯上关系,那么什么时候和真正的物理内存发起真正的联系哩?看下面的分析。
2.在缺页中断的催促下虚拟地址映射的内存有了~~
先大致说一说具体流程,然后由代码验证之。首先需要注意的当我们mmap完成上面描述的那些逻辑完成了逻辑上的映射之后我们就可以进行访问对应虚拟内存映射的物理内存了。然而不幸的是当访问之后会发现对应的物理页面并不存在,那么这时候就会触发缺页中断,调用回调函数do_page_fault。而do_page_fault函数其实做的事情也比较简单,它首先会根据触发缺页中断的虚拟地址创建此虚拟地址对应的页表项,就拿64bit的操作系统来说的话它就会创建上层页目录表项PUD与中层页目录表项PMD以及最后一层的页表项。而对页表项绑定具体的物理地址的时候要分三种情况:
(1)匿名映射,直接利用伙伴系统分配新的页即可
(2)文件内存映射,若有缓存页就找到安排之,否则将文件读入内存
(3)页面被换出到swap区,即磁盘上了
好嘞,上面大致讲了一下工作流程,下面就用具体的代码执行逻辑来验证之:
//linux-4.13.16\arch\x86\mm\fault.c
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
unsigned long address = read_cr2(); /* Get the faulting address */
...
__do_page_fault(regs, error_code, address);
}
do_page_fault函数调用__do_page_fault(regs, error_code, address)函数:
//linux-4.13.16\arch\x86\mm\fault.c
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk = current;
struct mm_struct *mm = tsk->mm;
...
if (unlikely(fault_in_kernel_space(address)))
if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
if (vmalloc_fault(address) >= 0)
return;
...
}
...
vma = find_vma(mm, address);
...
fault = handle_mm_fault(vma, address, flags);
__do_page_fault首先判断缺页异常发现的空间是内核还是用户空间,如果说是内核的话就调用vmalloc_fault(address)函数处理之,此部分今天这篇文章不做描述,主要看发生在用户空间的情况。
当发生在用户空间的时候先调用find_vma函数找到发生异常地址对应的虚拟地址存储区对应的vm_area_struct结构,然后调用handle_mm_fault——>__handle_mm_fault(vma, address, flags)
处理这个缺页异常:
//linux-4.13.16\mm\memory.c
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
struct vm_fault vmf = {//记录缺页消息的结构体特有用
.vma = vma,//仍有部分成员此处为进行列表初始化,包括下面的pte,pmd,pud等
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd = pgd_offset(mm, address);
p4d_t *p4d = p4d_alloc(mm, pgd, address);
。。。
vmf.pud = pud_alloc(mm, p4d, address);//pud来了
。。。
vmf.pmd = pmd_alloc(mm, vmf.pud, address);//pmd也来了
。。。
return handle_pte_fault(&vmf);//瞧,去处理pte了
}
函数先构造了对应的PUD与PMD两级页目录项,然后调用handle_pte_fault函数去构造页表项,以及使用具体的物理页面的物理地址初始化这个新的页表项。来瞅一瞅吧!
static int handle_pte_fault(struct vm_fault *vmf){
pte_t entry; //嘿嘿,页表项
。。。
if (!vmf->pte) {//当pte不存在时
if (vma_is_anonymous(vmf->vma))//匿名
return do_anonymous_page(vmf);
else
return do_fault(vmf);//文件映射
}
if (!pte_present(vmf->orig_pte))//交换到交换分区了吧
return do_swap_page(vmf);
。。。
很清楚当对应的页表项不存在时它会分三种处理:
- 当为匿名映射时调用vma_is_anonymous
vma_is_anonymous函数分配页表项,同时在伙伴系统上分配物理页面初始页表项,最后将页表项插到页表上。 - do_fault处理文件映射
do_fault最终会调用__do_fault函数:
static int __do_fault(struct vm_fault *vmf){
struct vm_area_struct *vma = vmf->vma;
int ret;
...
ret = vma->vm_ops->fault(vmf);
...
}
其实通过前面的讲解就知道vma->vm_ops->fault其实调用的是对应文件系统的vm_ops->fault函数,比如在ext4文件系统中就是下面结构体中的fault函数:
//linux-4.13.16\fs\ext4\file.c
static const struct vm_operations_struct ext4_file_vm_ops = {
.fault = ext4_filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = ext4_page_mkwrite,
};
显然会调用ext4_filemap_fault函数,该函数将分两种情况处理,若内存中有文件缓存就将缓存找到,如果不在就会申请一个事先准备好的缓存页然后调用相关函数将文件从磁盘上读到内存里面就可以了。对第二种情况要特别注意,内核并不能直接将文件内容直接放到用户空间映射的物理页面上,因为内核不能使用物理地址,所以就需要在内核里面临时映射一把,这就是在前面剖析内核空间布局时关于最上面临时映射区的具体使用途径。
- swap缓冲区
do_swap_page(vmf)
前面了解过有一个内核线程Kswapd会将长时间不在使用的物理页面换出内存到swap区,此处针对的就是这种情况,他会先在swap缓冲区中查找是否有我们要查找的页面,有就好,没有的话就会重新分配页面将swap区数据给读取进来。
3.总结
- 当调用mmap函数进行用户态内存映射时仅仅只是先分配虚拟内存,当真正使用时才会分配
- 当发生缺页中断时调用回调函数do_page_fault,该函数会根据触发缺页中断的虚拟地址创建此虚拟地址对应的页表项PUD与PMD以及最后一层的页表项。最后当为匿名映射,直接利用伙伴系统分配新的物理内存;为文件映射时将文件读入内存;如果是swap就将swap读入。
- 对于每一个进程来说当在用户态时他们进行物理地址的转换时并不需要进入内核态,因为有一个CPU寄存器Cr3里面保存了当前进程的顶级页表gpd,它可以自动将虚拟地址转换为物理地址,只有当初发缺页中断时才会调用do_page_fault进入内核态
- 最后最后,在进行虚实地址转换时为了更进一步提高映射速度又提出了快表机制以便于更快的查询。其实本质上就是利用局部性原理将经常要访问的那些页表部分给缓存到TLB这个硬件中罢了,只是这种硬件设备除了速度比内存更快之外,容量也很小。