mmap 的内核实现
mmap 的内核实现
延时分配
参考如下简单的 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("%p\n", 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 的地址是否是我们拥有的合法地址,如果是,则在此时真正为我们分配物理内存。
按页分配
再看下上面的源码,我们指定的内存长度明明是 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。再回到上面的 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 方法返回该内存段的起始地址给用户。