OS学习--从操作系统启动进程到缺页异常以及内核/用户的处理
我们知道一个程序的执行,需要可执行文件的生成和加载,从操作系统的角度来看可执行文件的装载,重点依赖虚拟存储方式,特别是虚拟地址空间的开辟以及硬件地址转换和映射机制。
首先,代码原始文本经过预编译,编译,汇编,链接装载(指包含符号解析与重定向过程的静态链接)之后,生成一类可执行文件,或称为ELF文件;之后操作系统为该进程创建独立的虚拟空间,使其有别于其他进程。总体来看从创建进程到装载可执行文件并启动主要分为:
1.创建独立虚拟地址空间;
2.读取可执行文件头,创建虚拟空间与可执行文件的映射关系;
3.将可执行文件入口地址存入CPU指令寄存器,返回执行启动;
详细的,对于Linux内核来说,在用户层面bash进程会fork()一个新进程,新进程会调用execve()系统调用执行指定的ELF文件,原先bash等待新进程执行结束;内核装载ELF文件的系统调用入口是sys_execve(),参数检查后调用do_execve(),来读取文件头,然后调用search_binary_handle,所有可执行文件格式都有匹配的装载处理过程,该函数会通过魔数来确定文件格式并调用处理方法,对ELF来说将调用load_elf_binary(),其主要步骤是:
1.检查文件格式有效性;
2.寻找动态链接的“.interp”段,设置动态链接器路径(注意动态链接器本身是通过自举静态链接的);
3.根据ELF可执行文件的Header描述对ELF文件映射到fork进程中;
4.初始化ELF进程环境,如寄存器地址等;
5. 将系统调用的返回地址修改为ELF入口节点,对于静态链接ELF文件,入口是文件头中Header所指向的地址,对于动态链接,入口是动态链接器;当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器跳转到ELF入口地址,ELF装载完成开始运行程序。
对于地址转换与映射机制,我们回顾多级Cache,Page页表,TLB等存储结构,以及完整的寻址流程,首先值得注意的是Cache,page,TLB以及内存的访问/命中顺序:
如下图所示,Cache按照与CPU的距离可以划分为不同层级,从功能上来看,如果TLB介于 CPU 和 Cache之间,其实就是MMU介于CPU和Cache之间,则是先访问TLB后访问Cache且此时访问Cache使用的是物理地址,这种情况称为物理Cache。如果TLB介于 Cache和Memory之间,即MMU介于Cache与Memory之间,则是先访问Cache后访问TLB且此时访问Cache使用的是虚拟地址,这种情况称为逻辑Cache:
因此按物理cache的设定来看,基本的寻址顺序应该是从CPU获得虚拟地址起,在MMU的TLB中查找常用页表项(称为快表查找),如果未命中,查找Page页表寄存器进行地址转换:
虚页号+页偏移-->页表寄存器-->物理页号+页偏移
根据物理地址查找物理cache(内存副本),如果未找到,则查找内存;
回到可执行文件被映射的虚拟空间,进程虚拟空间的分布如下:
操作系统将ELF到虚拟地址空间的映射关系保存为一个内部数据结构;同时,系统设置了一种新的段概念作为虚拟地址空间的管理单位,Linux叫做虚拟内存区域(VMA),Windows中叫做虚拟段(VS);注意,堆和栈在任何进程中都分别有一个对应的VMA,其余ELF段的段合并原则,是按照有相同属性的,有相同映像文件的映射成一个VMA;一个进程按照权限可以分为如下几类VMA:
1.代码VMA;//rx
2.数据VMA;//rwx
3.堆VMA;//rwx
4.栈VMA;//rw
5.匿名VMA;//未使用
映射关系如下图:
详细请见:Linux可执行文件与进程的虚拟地址空间_@HDS的博客-CSDN博客_可执行文件 虚拟地址
缺页中断原理
对于进程刚刚创建运行的阶段也会伴随大量缺页异常,操作系统具备一系列流程用来处理缺页异常;首先了解Linux系统的线性地址与页式管理方式:
CPU通过页式管理单元,负责将一个线性地址转换为物理地址;线性地址分为:页面目录索引(10bits),页表索引(10bits),页内偏移(12bits);
而线性地址通常由逻辑地址转换得来:
此处常出现在CPU的地址转换操作中;
物理页,分页单元把所有的物理内存划分成固定长度管理单位,长度与内存页一一对应;每个进程都有自己的页目录,当进程处于运行态的时候,其页目录地址存放在CR3寄存器中;
进程对应的线性虚拟内存里的页面不必常驻物理内存,在执行一条指令时,如果发现他要访问的页没有在内存中,那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。常见发生场景如下:
1.MMU中没有创建虚拟物理内存页表映射关系,同时虚拟地址之后当前进程线性区VMA丢失;
2.MMU中没有创建虚拟物理内存页表映射关系,当前进程VMA存在,可能是栈溢出缺页中断;
3.使用malloc/mmap访问物理空间库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
缺页中断的处理方式与查看进程(启动以来)相关参数:
ps -o majflt,minflt -C program
对于Linux来说,缺页异常类型的判断流程如下:
内核态缺页中断:
1.VMALLOC区异常:页表同步(因为伴随着进程的切换可能用户进程的页表不是最新的,需要将内核的页表更新到用户进程的页表)
2.内核用户地址空间异常
3.内核bug
内核处理缺页异常的主函数就是do_page_fault();如果当前执行流程在内核态,不论是在临界区还是内核进程本身(内核的mm为NULL),说明在内核态出了问题,跳到标号no_context进入内核态异常处理,由函数_do_kernel_fault()完成;
用户态缺页中断:
调用函数_do_page_fault(),首先从CPU的控制寄存器中读出出错的地址address,然后调用find_vma()在进程的虚拟地址空间中找出结束地址大于address的第一个VMA,如果找不到的话,则说明中断是由地址越界引起的,转到bad_area()执行相关错误处理;
do_page_fault处理流程如下:
特别是对于进程启动初期的大量页错误,缺页异常,还记得创建ELF与虚拟地址空间之间映射关系的数据结构吗,此时操作系统将查询该数据结构,并找到空页面所在的VMA,计算出相应页面在可执行文件中的偏移,并在物理内存中重新分配一个物理页面,并重建虚拟页与物理页的映射关系,并归还控制权给进程,进程从中断处恢复执行。详细可参考阅读: linux内核缺页中断处理_lcjmsr的博客-CSDN博客_linux缺页中断。
当发生缺页中断并需要置换页面以便为即将调入的页面腾出空间时,如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本,如果该页面没有被修改过(如一个包含程序正文的页面),那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面就可以了。详细的页面置换算法处了先前介绍过的FIFO/LRU/LFU之外,还可以参考内存管理——页面置换算法 - 知乎 (zhihu.com)。