系统调用mmap的内核实现分析

时间:2022-06-25
本文章向大家介绍系统调用mmap的内核实现分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

首先来看个例子:

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  void *p;
  sleep(5);

  p = mmap(NULL, 1, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (p == MAP_FAILED) {
    perror("mmap");
    return -1;
  }

  printf("%pn", p);
  sleep(5);
  return 0;
}

执行该程序,输出mmap方法返回的内存地址,同时使用pmap命令输出该程序执行mmap之前以及之后的内存使用情况。

mmap方法返回的内存地址:

$ ./a.out
0x7f521d667000

pmap命令的两次输出结果:

$ pmap -x $(pgrep a.out)
32408:   ./a.out
Address           Kbytes     RSS   Dirty Mode  Mapping
0000555bb0511000       4       4       0 r---- a.out
0000555bb0512000       4       4       0 r-x-- a.out
0000555bb0513000       4       0       0 r---- a.out
0000555bb0514000       4       4       4 r---- a.out
0000555bb0515000       4       4       4 rw--- a.out
00007f521d45e000     148     140       0 r---- libc-2.29.so
00007f521d483000    1320     628       0 r-x-- libc-2.29.so
00007f521d5cd000     292      64       0 r---- libc-2.29.so
00007f521d616000       4       0       0 ----- libc-2.29.so
00007f521d617000      12      12      12 r---- libc-2.29.so
00007f521d61a000      12      12      12 rw--- libc-2.29.so
00007f521d61d000      24      16      16 rw---   [ anon ]
00007f521d63e000       8       8       0 r---- ld-2.29.so
00007f521d640000     124     124       0 r-x-- ld-2.29.so
00007f521d65f000      32      32       0 r---- ld-2.29.so
00007f521d668000       4       4       4 r---- ld-2.29.so
00007f521d669000       4       4       4 rw--- ld-2.29.so
00007f521d66a000       4       4       4 rw---   [ anon ]
00007fffd1e55000     132      12      12 rw---   [ stack ]
00007fffd1f04000      12       0       0 r----   [ anon ]
00007fffd1f07000       4       4       0 r-x--   [ anon ]
---------------- ------- ------- -------
total kB            2156    1080      72

$ pmap -x $(pgrep a.out)
32408:   ./a.out
Address           Kbytes     RSS   Dirty Mode  Mapping
0000555bb0511000       4       4       0 r---- a.out
0000555bb0512000       4       4       0 r-x-- a.out
0000555bb0513000       4       4       0 r---- a.out
0000555bb0514000       4       4       4 r---- a.out
0000555bb0515000       4       4       4 rw--- a.out
0000555bb1b7a000     132       4       4 rw---   [ anon ]
00007f521d45e000     148     140       0 r---- libc-2.29.so
00007f521d483000    1320     948       0 r-x-- libc-2.29.so
00007f521d5cd000     292     128       0 r---- libc-2.29.so
00007f521d616000       4       0       0 ----- libc-2.29.so
00007f521d617000      12      12      12 r---- libc-2.29.so
00007f521d61a000      12      12      12 rw--- libc-2.29.so
00007f521d61d000      24      16      16 rw---   [ anon ]
00007f521d63e000       8       8       0 r---- ld-2.29.so
00007f521d640000     124     124       0 r-x-- ld-2.29.so
00007f521d65f000      32      32       0 r---- ld-2.29.so
00007f521d667000       4       0       0 rw---   [ anon ]
00007f521d668000       4       4       4 r---- ld-2.29.so
00007f521d669000       4       4       4 rw--- ld-2.29.so
00007f521d66a000       4       4       4 rw---   [ anon ]
00007fffd1e55000     132      12      12 rw---   [ stack ]
00007fffd1f04000      12       0       0 r----   [ anon ]
00007fffd1f07000       4       4       0 r-x--   [ anon ]
---------------- ------- ------- -------
total kB            2292    1472      76

在pmap命令的前后两次输出中,我们可以看到,第二次pmap输出多了一个 [anon] 内存段(第47行),而该内存段的起始地址正好是上面程序输出的地址。

也就是说,该内存段就是操作系统为mmap系统调用新分配出来的区域。

由pmap的输出可以看到,该内存段的大小是4kb,实际物理内存占用(rss)是0。

实际物理内存占用为什么是0呢?

在我们向操作系统申请内存时,比如用malloc或mmap等方式,操作系统只是标记了我们拥有一段新的内存区域,如上pmap输出,而并没有实际分配给我们物理内存。

当我们要使用该段内存时,比如读或写,会先触发page fault,操作系统内部的page fault handler会检查触发page fault的地址是否是我们拥有的合法地址,如果是,则在此时真正为我们分配物理内存。

有关page fault相关内容,我们会另起一篇文章单独讲解。

再看下上面的源码,我们指定的内存长度明明是1字节,为什么pmap的显示是4kb呢?

这个在下面的源码分析中会看到原因。

看下mmap系统调用对应的内核源码:

// arch/x86/kernel/sys_x86_64.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
                unsigned long, prot, unsigned long, flags,
                unsigned long, fd, unsigned long, off)
{
        long error;
        error = -EINVAL;
        if (off & ~PAGE_MASK)
                goto out;

        error = ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
        return error;
}

该方法调用了ksys_mmap_pgoff方法:

// mm/mmap.c
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
                              unsigned long prot, unsigned long flags,
                              unsigned long fd, unsigned long pgoff)
{
        struct file *file = NULL;
        unsigned long retval;

        if (!(flags & MAP_ANONYMOUS)) {
                ...
                file = fget(fd);
                ...
        }
        ...
        retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
        ...
        return retval;
}

该方法又调用了vm_mmap_pgoff:

// mm/util.c
unsigned  longvm_mmap_pgoff(struct file *file, unsigned long addr,
        unsigned long len, unsigned long prot,
        unsigned long flag, unsigned long pgoff)
{
        unsigned long ret;
        struct mm_struct *mm = current->mm;
        ...
        if (!ret) {
                ...
                ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
                                    &populate, &uf);
                ...
        }
        return ret;
}

该方法又调用了do_mmap_pgoff:

// include/linux/mm.h
static inline unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,
        unsigned long len, unsigned long prot, unsigned long flags,
        unsigned long pgoff, unsigned long *populate,
        struct list_head *uf)
{
        return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate, uf);
}

该方法又调用了do_mmap:

// mm/mmap.c
unsigned long do_mmap(struct file *file, unsigned long addr,
                        unsigned long len, unsigned long prot,
                        unsigned long flags, vm_flags_t vm_flags,
                        unsigned long pgoff, unsigned long *populate,
                        struct list_head *uf)
{
        struct mm_struct *mm = current->mm;
        ...
        len = PAGE_ALIGN(len);
        ...
        addr = get_unmapped_area(file, addr, len, pgoff, flags);
        ...
        addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
        ...
        return addr;
}

该方法先用宏PAGE_ALIGN,使len大小page对齐,在最开始的源码中,我们指定的len大小为1,page对其后为4096,即4kb,这也是为什么pmap输出的内存段大小为4kb。

其实,操作系统为进程分配的内存段都是以page为单位的。

之后,该方法又调用了get_unmapped_area来获取mmap的内存段的起始地址,这个方法就不详细看了。

最后,该方法又调用了mmap_region,继续执行mmap操作。

// mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
                unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
                struct list_head *uf)
{
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma, *prev;
        ...
        vma = vm_area_alloc(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;

        if (file) {
                ...
                vma->vm_file = get_file(file);
                error = call_mmap(file, vma);
                ...
        } else if (vm_flags & VM_SHARED) {
                ...
        } else {
                vma_set_anonymous(vma);
        }

        vma_link(mm, vma, prev, rb_link, rb_parent);
        ...
        return addr;
        ...
}

该方法先调用vm_area_alloc,分配一个类型为struct vm_area_struct的实例,并赋值给vma,然后设置vma的起始地址、结束地址等信息。

这个vma里包含的内容,就是上面pmap命令输出的内存段。

之后,如果我们是想mmap一个file,则调用call_mmap:

// include/linux/fs.h
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
        return file->f_op->mmap(file, vma);
}

该方法又调用了file->f_op->mmap指针指向的方法,以ext4文件系统为例,该方法为ext4_file_mmap:

// fs/ext4/file.c
static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
        ...
        if (IS_DAX(file_inode(file))) {
                ...
        } else {
                vma->vm_ops = &ext4_file_vm_ops;
        }
        return 0;
}

该方法的作用是初始化vma的vm_ops字段,使其值为ext4_file_vm_ops。

vma->vm_ops字段会在page fault handler中被使用到,以后其他文章会讲。

再回到上面的mmap_region方法,如果我们mmap的是一块anonymous的内存区域,则会调用vma_set_anonymous方法:

// include/linux/mm.h
static inline void vma_set_anonymous(struct vm_area_struct *vma)
{
        vma->vm_ops = NULL;
}

该方法将vma->vm_ops字段设置为null,用此来表示,该vma代表的内存段为anonymous模式。

再之后,mmap_region方法会调用vma_link方法将新创建的vma链接到struct mm_struct的mmap字段和mm_rb字段,标识该进程拥有vma表示的这段内存区域。

最后,mmap_region方法返回该内存段的起始地址给用户。

至此,mmap方法就已经结束了。

由上可以看到,mmap系统调用只是为当前进程分配并初始化了一个vma实例,用来标识该进程拥有这段以vma表示的内存空间,并没有实际分配物理内存。

对此感兴趣的朋友也可以去读读相关的内核源码。

完。