execve原理 #
execve 的作用是使当前进程执行一个新的可执行程序,它的原型如下所示:
#include <unistd.h>
int execve(const char* filename, const char* argv[],
const char* envp[])
其中 execve 的第一个参数是可执行程序的文件名,第二个参数用来传递命令行参数,第三个参数用来传递环境变量。
execve 的执行步骤如下所示:
- 清空页表这样整个进程中的页都变成不存在了,一旦访问这些页,就会发生页中断;
- 打开待加载执行的文件在内核中创建代表这个文件的 struct file 结构;
- 加载和解析文件头文件头里描述了这个可执行文件一共有多少 section;
- 创建相应的vma vma 用来描述代码段,数据段,并且将文件的各个 section 与这些内存区域建立映射关系;
- 加载其他共享库文件如果当前加载的文件还依赖其他共享库文件,则找到这个共享库文件,并跳转到第 2 步继续处理这个共享库文件;
- 跳转到可执行程序的入口处执行。
execve 的实现并不负责将文件内容加载到物理页中,它只建立了文件 section,与内存区域的映射关系就结束了。真正负责加载文件内容的是缺页中断,接下来,我们就看看缺页中断是如何加载物理页的。
在 execve 的执行步骤中,内核为可执行程序创建一个 vma 结构体实例,然后将它的 vm_file 属性设成第 2 步所打开的文件,这就建立起了内存区域和文件的映射关系。这个内核区域的区间首地址、区间尾地址和控制权限,都是由第 3 步解析的信息决定的。例如.text 段被加载到的内存首地址,也就是链接时所决定的起始地址,它就决定了内存代码段的起始地址。
由于第 1 步把页表都清空了,这就导致 CPU 在加载指令时会发现代码段是缺失的,此时就会产生缺页中断。
Linux 内核用于处理缺页中断的函数是 do_no_page,如果内核检查,当前出现缺页中断的虚拟地址所在的内存区域 vma(虚拟地址落在该内存区域的 vm_start 和 vm_end 之间)存在文件映射 (vm_file 不为空),那就可以通过虚拟内存地址计算文件中的偏移,这就定位到了内存所缺的页对应到文件的哪一段。然后内核就启动磁盘 IO,将对应的页从磁盘加载进内存。一次缺页中断就这样被解决了。
可执行程序的加载不是一次性完成的,而是由缺页中断根据需要,将文件的内容以页为单位加载进内存的,一次只会加载一页。