进程的建立
从操作系统的角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程,很多时候一个程序程序被执行同时伴随着一个新进程的创建,在有虚拟存储的情况下,创建一个进程,然后加载可执行文件并且执行,需要做三件事:
- 创建一个独立的虚拟地址空间
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
首先是创建虚拟地址空间
我们知道虚拟地址空间是有一组页映射函数将虚拟空间的各个页映射到相应的物理空间,那么创建虚拟地址空间并不是创建空间而是创建这个映射结构,在i386下创建虚拟地址空间只要分配一个页目录就可以了,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候设置。页表上有各种标志位,其中可以知道这一块地址是否分配物理地址。
虚拟空间与文件映射
上面的那一步映射是虚拟空间到物理内存的映射,当前是虚拟空间到可执行文件的映射。当程序发生页错误的时候,操作系统从物理内存分配一个物理页,然后将缺页从磁盘读到内存,再设置缺页的虚拟页和物理页的映射关系,这样程序继续运行。但是明显的一点是,缺页的时候需要知道缺的页是文件的哪个位置。
执行可执行文件
操作系统设置CPU的指令寄存器将控制权交给进程,切换内核对战和CPU运行权限后,跳转到可执行文件的入口地址,即ELF文件头中的入口地址。
ELF文件映射
linux中将进程虚拟空间的一个段叫做虚拟内存区域(VMA,virtual memory area),比如操作系统创建进程后,会在进程的相应数据结构设置一个.text段的VMA,它在虚拟地址空间的地址为0x08048000~0x08049000,它对应的ELF文件的偏移为0的.text,它的属性是只读。
在操作系统里面,VMA除了被用来映射可执行文件里面的段,他还有其他的作用,操作系统使用VMA对进程的地址空间进行管理,我们知道进程在执行的时候需要用到栈(stack)、堆(heap),事实上他们在内存空间也是以VMA的形式存在,比如我们查看nginx的各个VMA:
进程栈初始化
进程刚启动的时候,需要知道一些进程的运行环境,最基本的就是系统环境变量和进程的运行参数。很常见的做法是操作系统在进程启动的时候将这些信息提前保存到进程的虚拟地址空间的栈上(stack VMA)。
linux内核装载ELF过程
我们在shell下执行一个可执行文件,首先在用户层面,bash进程会调用fork系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,进入exec()系统调用之后,Linux内核就开始真正的装载工作。在内核中,do_exec首先查找被执行文件,如果找到文件,则读取文件的前128字节。为什么这么做,Linux支持的可执行文件不止ELF,前128字节是判断文件格式的,开头的四个字节,称为魔数,通过对魔数的判断可以确定文件的类型和格式。比如ELF的可执行文件前四个字节为0x7F,E,L,F,java的可执行文件头为c,a,f,e.
这里我们只关心ELF的装载,具体步骤为:
- 检查ELF的文件格式有效性,比如魔数,段检查
- 寻找动态链接的.interp段,设置动态链接库路径
- 根据ELF可执行文件的程序头,对ELF文件进行映射
- 初始化ELF进程环境,寄存器地址等
- 将系统调用的返回修改成ELF可执行文件的入口点,这个入口点取决于链接的方式,对于静态链接,入口是e_entry地址,对于动态链接,程序入口点是动态连接器。