首先来看个例子:
#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的地址是否是我们拥有的合法地址,如果是,则在此时真正为我们分配物理内存。
有关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表示的内存空间,并没有实际分配物理内存。
对此感兴趣的朋友也可以去读读相关的内核源码。
完。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!