前言
ELF文件中含有PT_LOAD属性的segment最后都会被加载到进程的VM中。加载之后elf中的segment通过VMA便和进程中的某块VM关联上了。感觉这是废话,elf链接的时候给的就是虚拟地址,不关联执行会出错(详细参考链接地址,执行地址,加载地址之间的关系)。而物理内存此时还没有分配出去,页表中的相关条目还是空的。如果此时CPU来访问这部分内容,那便会产生缺页异常(缺页异常的流程可以参考这篇文章:https://blog.csdn.net/rockrockwu/article/details/80993594)。
这篇文章来说说VMA是如何与物理内存关联上的。
分配VMA
先看VMA的分配过程。
shell中执行elf可执行文件,内核会首先fork()出一个进程。然后调用execve()系统调用来替换这个进程,使其变成一个全新的进程。execve()的详细流程网上有很多文章。这里从和elf文件类型相关load_elf_binary()函数开始分析。
这个函数在for循环中依次轮询整个elf文件,然后将elf文件中所有PT_LOAD属性的segment通过elf_map函数加载到进程VM空间中。
需要注意的是,像text/data等section在链接时已经给了链接地址(虚拟地址)。加载时,进程VM空间中对应的这些虚拟地址区域会分配给这些段(废话,不然load到哪里去 — —!)。但是,除了这些链接分配的虚拟地址,在进程运行过程中还会去打开文件,或者分配栈空间或扩展堆区等等操作。这些操作不会涉及文件或者说文件描述符fd。那这些VMA如何分配呢?
先列出这些VMA的分配或更确切的说是寻找过程,如下:laod_elf_binary()->elf_map()->vm_mmap()->vm_mmap_pgoff()->do_mmap_pgoff()->… …->unmapped_area()。
在unmapped_area()中会设计具体的寻找过程。这个函数很长,这里只贴说到的部分。
1784 vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);
1785 if (vma->rb_subtree_gap < length)
1786 goto check_highest;
这里第一句很明显,是先找到rb tree的root node(VMA通过rb tree来管理,具体实现自行网上搜索)。
第二句没法直接理解。但length的含义很明白,通过上下文能知道它表示所需vma占用的长度,所以这句的重点是理解rb_subtree_gap的含义。
rb_subtree_gap保存是本node中左右子树中最大的gap,也就是最大间隙size。换句话说,从这个node开始,左右子树中能插入的vma最大length是多少。例如gap是10,表示该节点下最大只能插入leng为10的vma。如果vma的length是11,那是没办法insert到这个节点的子树下了,只能另寻parent。我在这里卡了很久,为了360度无死角说清楚,再贴一幅图来更直观的说明。
假设图中1/2/3/8/9/10/20 数字表示每个node起始地址,gap表示当前节点和子树中的最大间隙size。那如此设计的好处就是可以快速判断出一个新vma应该插入到哪个节点下或者说哪个节点下还有足够的空间来容纳这个新的vma。
例如有新的节点起始地址为12,由于其其实地址大于root node,所以从node的右子树开始判断,发现gap为9,那就是表示这颗字数下可以存放起始地址大于10,length小于9的所有节点。
这是rb tree中有gap能插入新的vma。那如果当前的gap都比length小该怎么办呢?如果gap小于length,那说明当前的rb tree中没有足够大的间隙供这个新的vma插入。所以需要一片新的,足够大的区域来存放这个新的vma。且这个新的vma需要被插入到rb tree最右端。也就是分配一个新的vma挂到vma的rb tree中。
#匿名映射和线性映射对VMA的不同处理
分配vma的过程中,匿名映射和文件映射并没有区别对待,那就接着看分配完的后续处理。
关键函数是do_mmap_pgoff。可以看到这个函数在找到vma之后,会通过if(file)来判断当前的操作是否和文件有关。也就是说匿名映射和匿名映射在这里开始分叉了。
ELF可执行文件中PT_LOAD属性的加载涉及的是文件操作。那什么情况下file是空的呢?这就需要追本朔源,看看哪些地方会调用do_mmap_pgoff(),并且传入的file内容。经过搜索,答案是除了execve()系统调用外,还有mmap()系统调用也会走到这里。malloc的底层实现会调用到mmap,文件映射也会调用到mmap。
为了直观分析,用strace来跟踪一个hello_world程序运行时的系统调用过程。
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int a[1024] = {1};
5
6 int main()
7 {
8 printf("hello world!\n");
9
10 return 0;
11 }
strace得到log如下:
binwu@binwu3-DESK2:~/work/hello_world$ strace ./hello_world
execve("./hello_world", ["./hello_world"], [/* 81 vars */]) = 0
brk(NULL) = 0x199c000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=255491, ...}) = 0
mmap(NULL, 255491, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1a0c16d000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1a0c16c000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1a0bbbd000
mprotect(0x7f1a0bd7d000, 2097152, PROT_NONE) = 0
mmap(0x7f1a0bf7d000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f1a0bf7d000
mmap(0x7f1a0bf83000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f1a0bf83000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1a0c16b000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1a0c16a000
arch_prctl(ARCH_SET_FS, 0x7f1a0c16b700) = 0
mprotect(0x7f1a0bf7d000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f1a0c1ac000, 4096, PROT_READ) = 0
munmap(0x7f1a0c16d000, 255491) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
brk(NULL) = 0x199c000
brk(0x19bd000) = 0x19bd000
write(1, "hello world!\n", 13hello world!
) = 13
exit_group(0) = ?
+++ exited with 0 +++
execve()的file参数
第一行就可知,execve()系统调用会通过第一个参数传入elf文件的path。跟踪execve()到do_mmap_pgoff()的过程可知,此时的file非空。我们会进入到文件映射的分支。
mmap()的file参数
strace的log中包含多次mmap()的系统调用,且传入参数各不相同。分析可知,mmap()的倒数第二个参数是fd,表示传入的文件描述符。如果是匿名映射fd应该为-1,且对应的flag要设置MAP_ANONYMOUS。从log清楚看到,这个hello_world的进程在运行过程中,会通过mmap()多次进行匿名映射和文件映射。
在知道file和文件/匿名映射的关系后,再看看他们的不同处理。
VMA的不同处理
PT_LOAD属性segment的load过程属于PRIVATE,所以先看PRIVATE的case。
1336 case MAP_PRIVATE:
1337 if (!(file->f_mode & FMODE_READ))
1338 return -EACCES;
1339 if (path_noexec(&file->f_path)) {
1340 if (vm_flags & VM_EXEC)
1341 return -EPERM;
1342 vm_flags &= ~VM_MAYEXEC;
1343 }
1344
1345 if (!file->f_op->mmap)
1346 return -ENODEV;
1347 if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
1348 return -EINVAL;
1349 break;
可以看到如果是PRIVATE的文件,f_op中必须要有mmap方法,否这直接return错误码。震不震惊?惊不惊喜?
如果是匿名映射,因为file是空,所以会走另外的分支。
1365 case MAP_PRIVATE:
1366 /*
1367 * Set pgoff according to addr for anon_vma.
1368 */
1369 pgoff = addr >> PAGE_SHIFT;
1370 break;
所以可知,匿名/文件映射的关键是有没有f_op->mmap这个操作。这好像也是废话。因为匿名映射压根就没有file,哪来的f_op。这个mmap方法的具体实现在哪里呢?这个实现是和具体文件系统类型有关,不同文件系统类型的mmap实现是不一样的。以ext2为例,这个文件系统的f_op如下:
63 const struct file_operations ext2_file_operations = {
64 .llseek = generic_file_llseek,
65 .read = new_sync_read,
66 .write = new_sync_write,
67 .read_iter = generic_file_read_iter,
68 .write_iter = generic_file_write_iter,
69 .unlocked_ioctl = ext2_ioctl,
70 #ifdef CONFIG_COMPAT
71 .compat_ioctl = ext2_compat_ioctl,
72 #endif
73 .mmap = generic_file_mmap,
74 .open = dquot_file_open,
75 .release = ext2_release_file,
76 .fsync = ext2_fsync,
77 .splice_read = generic_file_splice_read,
78 .splice_write = iter_file_splice_write,
79 };
可知道ext2的mmap实现是generic_file_mmap。偷偷说一句,大多数文件系统用的都是这个实现。然后看下mmap的具体功能。
2086 int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
2087 {
2088 struct address_space *mapping = file->f_mapping;
2089 if (!mapping->a_ops->readpage)
2090 return -ENOEXEC;
2091 file_accessed(file);
2092 vma->vm_ops = &generic_file_vm_ops;
2093 return 0;
2094 }
函数实现非常简单,只是将vma的vm_ops方法初始化成generic_file_vm_ops。
现在mmap的实现知道了,最终的就是要知道这个mmap什么时候被call到,需要接着往下分析do_mmap_pgoff()中的map_region()。
map_region()的内容比较丰富,主要就是初始化vma数据结构成员。这里现忽略其它,重点关注我们想要的内容——call mmap()!
1657 if (file) {
1658 if (vm_flags & VM_DENYWRITE) {
... ...
1674 vma->vm_file = get_file(file);
1675 error = file->f_op->mmap(file, vma);
函数在call mmap要通过if(file)来判断vma是否是文件映射。如果是,则简单粗暴的调用了f_op的mmap()方法,将该vma对应的vm_ops初始化成相关操作。这里ext2文件系统对应的vm_ops是generic_file_vm_ops方法。如果此vma是匿名映射,则vm_ops为空。
所以,匿名/文件映射的VMA重要的区别点就是是否有自己的ops,文件映射的vma有ops,匿名映射则没有!
在handle_pte_fault中就是通过这一点来判断缺页的映射类型,且文件映射缺页时会回调这个vma的ops!
VMA缺页处理
前面分析了VMA的分配和初始化的过程。但是,此时虚拟地址和物理地址还没有建立起映射关系。CPU第一次访问这些空间时会触发缺页异常。通过缺页异常来建立虚拟地址和物理地址的映射关系。缺页处理参考之前博文:(https://blog.csdn.net/rockrockwu/article/details/80993594)。
之前的博文中提到,缺页处理handle_pte_fault()函数中,内核会根据缺页的VMA属性来选择相对应的操作。假如缺页的VMA属于匿名映射,则调用do_anonymous_page来处理,否则调用do_fault来处理。
这里的do_fault对应的就是文件映射。不信,用git log -p mm/memory.c看下这个文件的修改历史。可以发现,do_fault就是do_linear_fault的马甲。所以,这里依然采用匿名/文件映射这两种分法。
4167 -static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma,
4168 +static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
4169 unsigned long address, pte_t *page_table, pmd_t *pmd,
4170 unsigned int flags, pte_t orig_pte)
4171 {
4172 @@ -3009,46 +3005,6 @@ static int do_linear_fault(struct mm_struct *mm, struct vm_area_struct *vma,
4173 return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
4174 }
在这个handle_pte_fault()函数中也得到了我们想要的结果。就是判断缺页部分是属于匿名映射还是文件映射,而判断标准就是前面讲过根据当前缺页vma是否有vm_ops方法!
vma->vm_ops
有就调用do_fault进行文件映射缺页的处理,没有则进入匿名映射。匿名映射的分配过程在之前缺页的博文中有描述,这里就不再分析了。下面重点看文件映射的缺页处理hanler do_fault()。
3176 if (!(flags & FAULT_FLAG_WRITE))
3177 return do_read_fault(mm, vma, address, pmd, pgoff, flags,
3178 orig_pte);
3179 if (!(vma->vm_flags & VM_SHARED))
3180 return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
3181 orig_pte);
3182 return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
do_fault()中最终要的就是上面三行,判断出缺页的操作类型。如果是read导致缺页择调用do_read_fault。如果是写出错则掉调用do_cow_fault。如果既不是读也不是写,就掉用do_shared_fault。
虽然有这三种错误类型,但是这三个api最终又都会去调用vm_ops中的fault方法。我们这里的vm_ops是generic_file_vm_ops()。
2173 const struct vm_operations_struct generic_file_vm_ops = {
2174 .fault = filemap_fault,
2175 .map_pages = filemap_map_pages,
2176 .page_mkwrite = filemap_page_mkwrite,
2177 };
所以其中的fault就是filemap_fault。也就是说文件映射的缺页异常在handle_pte_fault之后都会调用filemap_fault()来处理。
filemap_fault的实现设计到文件系统,其中有cache等等很多文件系统的概念。这里不展开了。