故事的开始是这样的,某天在脉脉上看到有人发了下面的帖子:

mmapmmap

其实,源码分析是比较难写的,主要有两个原因:

  • 一方面是源码实现一般会涉及多个知识点,所以在分析源码时需要穿插多个知识点,从而增加分析的难度。
  • 另一方面是源码实现会处理很多细节问题,这些细节问题虽然不是设计的主要框架,但忽略了有时会让人摸不着头脑。

所以,为了降低分析的难度和让读者能够更容易看懂,在分析源码时更注重知识点的实现,而在不影响理解的情况下,我会忽略一些细节问题。而对于穿插其他知识点的时候,会先跳过其实现,并且在后续的文章对其进行分析。

mmap 原理

mmapmmapmmapmmap

好消息是,这几个子系统我们都有对应的文章介绍过:

在阅读本文前,最好复习一下上面的文章。

mmapmmap
mmapmemory map内存映射
mmap

从上图可以看出,mmap 的原理就是将虚拟内存空间映射到文件的页缓存,在《什么是页缓存》一文中可知,对文件进行读写时需要经过页缓存进行中转的。所以当虚拟内存地址映射到文件的页缓存后,就可以直接通过读写映射区内存来对文件进行读写操作。

mmap 实现

mmapmmap

1. 文件映射

mmap()do_mmap_pgoff()do_mmap_pgoff()
unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr, 
              unsigned long len, unsigned long prot, 
              unsigned long flags, unsigned long pgoff)
{
    ...
    // 1. 获取一个未被使用的虚拟内存区
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (addr & ~PAGE_MASK)
        return addr;

    ...
    // 2. 调用 mmap_region() 函数继续进行映射操作
    return mmap_region(file, addr, len, flags, vm_flags, pgoff, accountable);
}
do_mmap_pgoff()
get_unmapped_area()mmap_region()

在 32 位的操作系统中,每个进程都有 4GB 的虚拟内存空间,应用程序在使用内存前,需要先向操作系统发起申请内存的操作。操作系统会从进程的虚拟内存空间中查找未被使用的内存地址,并且返回给应用程序。

操作系统会记录进程正在使用中的虚拟内存地址,如果内存地址没被登记,说明此内存地址是空闲的(未被使用)。

mmap_region()
unsigned long
mmap_region(struct file *file, unsigned long addr,
            unsigned long len, unsigned long flags,
            unsigned int vm_flags, unsigned long pgoff,
            int accountable)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    int correct_wcount = 0;
    int error;
    ...

    // 1. 申请一个虚拟内存区管理结构(vma)
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    ...

    // 2. 设置vma结构各个字段的值
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)];
    vma->vm_pgoff = pgoff;

    if (file) {
        ...
        vma->vm_file = file;

        /* 3. 此处是内存映射的关键点,调用文件对象的 mmap() 回调函数来设置vma结构的 fault() 回调函数。
         *    vma对象的 fault() 回调函数的作用是:
         *        - 当访问的虚拟内存没有映射到物理内存时,
         *        - 将会调用 fault() 回调函数对虚拟内存地址映射到物理内存地址。
         */
        error = file->f_op->mmap(file, vma);
        ...
    }
    ...

    // 4. 把 vma 结构连接到进程虚拟内存区的链表和红黑树中。
    vma_link(mm, vma, prev, rb_link, rb_parent);
    ...

    return addr;
}
mmap_region()
vm_area_structmmap()fault()mmap()generic_file_mmap()
vm_area_structvm_area_struct
vm_area_struct
struct vm_area_struct {
    struct mm_struct *vm_mm;
    unsigned long vm_start;              // 内存区的开始地址
    unsigned long vm_end;                // 内存区的结束地址
    struct vm_area_struct *vm_next;      // 把进程所有已分配的内存区链接起来
    pgprot_t vm_page_prot;               // 内存区的权限
    ...
    struct rb_node vm_rb;                // 为了加快查找内存区而建立的红黑树
    ...
    struct vm_operations_struct *vm_ops; // 内存区的操作回调函数集

    unsigned long vm_pgoff;
    struct file *vm_file;                // 如果映射到文件,将指向映射的文件对象
    ...
};

struct vm_operations_struct {
    // 当虚拟内存区没有映射到物理内存地址时,将会触发缺页异常,
    // 而在缺页异常处理函数中,将会调用此回调函数来对虚拟内存映射到物理内存。
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    ...
};
vmavm_filemmap()vmafault()
vmafault()
generic_file_mmap()vmafault()
struct vm_operations_struct generic_file_vm_ops = {
    .fault = filemap_fault, // 将 fault() 回调函数设置为:filemap_fault()
};

int generic_file_mmap(struct file *file, struct vm_area_struct *vma)
{
    ...
    vma->vm_ops = &generic_file_vm_ops;
    ...
    return 0;
}

至此,文件映射的过程已经分析完毕。我们来看看其调用链:

sys_mmap()
└→ do_mmap_pgoff()
└→ mmap_region()
└→ generic_file_mmap()

2. 缺页异常

mmap()mmap()vmavm_filevmafault()filemap_fault()mmap()

我们在《漫画解说 “内存映射”》一文中介绍过,虚拟内存必须映射到物理内存才能使用。如果访问没有映射到物理内存的虚拟内存地址,CPU 将会触发缺页异常。也就是说,虚拟内存并不能直接映射到磁盘中的文件。

页缓存mmap()
mmap()

答案就是:缺页异常

mmap()缺页异常do_page_fault()
do_page_fault()
do_page_fault()
└→ handle_mm_fault()
└→ handle_pte_fault()
└→ do_linear_fault()
└→ __do_fault()
__do_fault()
static int
__do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
           unsigned long address, pmd_t *pmd, pgoff_t pgoff,
           unsigned int flags, pte_t orig_pte)
{
    ...
    vmf.virtual_address = address & PAGE_MASK; // 要映射的虚拟内存地址
    vmf.pgoff = pgoff;                         // 映射到文件的偏移量
    vmf.flags = flags;                         // 标志位
    vmf.page = NULL;                           // 映射到虚拟内存中的物理内存页

    // 1. 如果虚拟内存管理区提供了 falut() 回调函数,那么将调用此函数来获取要映射的物理内存页,
    //    我们在 mmap() 系统调用的实现中看到,已经将其设置为 filemap_fault() 函数了。
    if (likely(vma->vm_ops->fault)) {
        ret = vma->vm_ops->fault(vma, &vmf);
        ...
    }
    ...

    if (likely(pte_same(*page_table, orig_pte))) {
        ...
        // 2. 通过物理内存页生成一个页表项值(可以参考内存映射一文)
        entry = mk_pte(page, vma->vm_page_prot);
        if (flags & FAULT_FLAG_WRITE)
            entry = maybe_mkwrite(pte_mkdirty(entry), vma);

        // 3. 将虚拟内存地址映射到物理内存(也就是将进程的页表项设置为刚生成的页表项的值)
        set_pte_at(mm, address, page_table, entry);
        ...
    }
    ...

    return ret;
}
__do_fault()
fault()filemap_fault()
filemap_fault()

最后,我们以一幅图来描述一下虚拟内存是如何与文件进行映射的:

mmap()