下面是对俞甲子等著的书籍《程序员的自我修养》第6章主要内容的整理。
作者介绍了程序和进程的区别,程序是静态的概念,是一些预先编译好的指令和数据集合的文件,比如说Linux中可执行的ELF文件,Windows的PE文件。
每个程序运行起来以后都有自己独立的虚拟地址空间(Virtual Address Space),虚拟地址空间由CPU的位数决定,硬件的寻址空间大小,比如32位平台的硬件决定的虚拟地址空间最大范围是0到2^32-1,即0x00000000~0xFFFFFFFF,一共4GB,64位的硬件平台可支持的虚拟地址空间达到17 179 869 184 GB。一般来说,在32位平台,C语言指针是4个字节,64位平台指针是8字节。
32位平台下的4GB虚拟地址空间被分为两部分,一部分给用户进程,一部分给操作系统,Linux 4GB虚拟空间3GB给用户进程,1GB给操作系统。
3GB的虚拟地址空间对于一些大型程序确实不够用,想要扩充内存,最直接的方法就是升级64位处理器,把虚拟地址扩展到17 179 869 184 GB。但是32位CPU只能使用4G的虚拟地址空间,但是可以使用超过4GB的物理地址空间,如果32位CPU的地址线使用36位,修改页映射方式,可以访问高达64GB的物理内存,Intel把这种扩展方式叫做PAE(Physical Address Extension)技术。
与之对应,在操作系统层,提供一个窗口映射的方法,把额外的内存映射到进程地址空间来,比如把0x10000000~0x20000000这一段256MB的虚拟地址空间用来做窗口,可以从高于4GB的物理地址空间申请多个256MB的物理空间,比如有A、B、C这三个256MB的物理地址空间,操作系统根据应用程序的内存空间申请,将0x10000000~0x20000000的虚拟地址空间映射到A、B、C其中一个,并且根据程序的要求,在A、B、C直接重复操作。Windows下这种访问内存的方法叫做AWE(Address Windowing Extensions)。
Intel的PAE和Windows的AWE是位于Wintel架构两个不同层对虚拟地址空间扩展的的支持,一个是硬件支持,一个是操作系统支持。
程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法是将程序运行所需要的指令和数据全部装入内存中,这就是静态装入的办法。但是很多情况下程序所需要的内存数量大于物理内存的数量,要解决程序所需要内存的数量日益增长与CPU内存由于昂贵且稀缺的矛盾,研究发现,程序运行时是有局部性原理,可以将程序最常用的部分驻留在内存中,而将不太常用的数据放在磁盘里,通过动态装入内存。
动态装入的思想是程序用到哪个模块就将哪个模块装入内存,如果不用就暂时不装如,存放到磁盘中。
有两种典型的动态装载方法:覆盖装入(Overlay)和页映射(Paging)
现在几乎被淘汰,这里不介绍了。
页映射是将内存和所有磁盘中的数据和指令按照“页”为单位划分成若干个页,以后所有的装载和操作的单位就是页。硬件规定的页大小有4096字节、8192字节、2M字节、4M字节等。程序运行需要哪个页就将该页调入物理内存。但是将新的页调入内存,有时候需要替换物理内存中已经占用的页。装载器(装载管理器)必须做出抉择,有很多中算法可以用来决定选择哪个页作为替换页,比如FIFO(选择最先装入内存的页,先进先出算法),比如LUR(选择最少被访问到的页,最少使用算法)。
如果程序直接操作物理地址,那么每次页被装入时,都需要重新定位。在虚拟内存机制中,现在的硬件MMU(内存管理单元)都提供了地址转换和页映射机制。从操作系统的角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间,一个程序的执行同时伴随着新进程的创建,在虚拟内存的机器中,进程的创建过程分三步:
创建一个独立的虚拟地址空间虚拟地址空间是一组页映射函数,将虚拟空间的各个页映射到相应的物理空间,创建虚拟地址空间实际上不是创建空间,而是创建映射函数所需要的数据结构。在i386的Linux下,创建虚拟地址空间实际上是分配一个页目录。甚至不设置页映射关系,这些映射关系要等到程序发生页错误时再进行设置。
读取可执行文件头,并建立虚拟地址空间与可执行文件的映射关系上面那一步是虚拟空间到物理内存的映射,接下来就要将虚拟地址空间与可执行文件进行映射。当程序发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该缺页从磁盘中读到物理内存中,再设置缺页的虚拟页与物理页的映射关系。
虽然是在程序出现缺页错误时才干这事,但是操作系统在捕获缺页错误时,它已经知道程序当前所需要的页在可执行文件中的哪个位置。即,需要事先建立虚拟地址空间与可执行文件的映射关系。虚拟地址空间与物理地址空间的映射是在运行时出现缺页时建立。从这个解释,可以明白可执行文件为啥又叫做映像文件(Image),应为可执行文件实际上是被操作系统映射到了虚拟地址空间。
可执行文件与执行该可执行文件的进程虚拟空间的映射,是操作系统通过内部的数据结构完成的。Linux中的进程虚拟空间中一个段叫做虚拟内存区域(VMA, Virtual Memory Area),Windows中进程虚拟空间中的一个段叫做虚拟段(Virtual Section)。
将CPU的指令寄存器设置成可执行文件的入口地址,启动程序操作系统通过设置CPU的指令寄存器,将控制权交给进程(CPU),此进程开始执行。
第二步说到”缺页错误“,什么是页错误?
操作系统通过可执行文件的头部信息建立可知行文件和进程虚拟内存之间的映射关系。
比如说操作系统告诉CPU程序的入口地址是0x08048000,CPU开始打算执行这个地址的指令时发现页面0x08048000~0x08049000是个空页面,于是CPU认为这是个页错误(Page Fault)。CPU将控制权交给操作系统,操作系统有专门的页错误处理例程来处理这种情况。这个时候就需要用到第二步建立的虚拟地址空间与可执行文件的映射关系数据结构了,操作系统查询这个数据结构,然后找到页面所在的VMA,计算出相应页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程虚拟地址空间该虚拟页(VMA)与分配的物理页之间建立映射关系,一旦建立了虚拟地址空间的虚拟页与物理内存的物理页的映射关系,物理内存中的空页面已经有进程即将执行的指令和数据了,此时操作系统把控制权还给进程(CPU),进程(CPU)从刚才出现错误的位置重新开始执行。
随着程序的执行,页错误不断产生,操作系统也会分配相应的物理页来满足进程执行的需求,有可能进程所需要的内存会超过可用的物理内存数量,特别是多个进程同时执行时,这时候操作系统需要精心设计,有时候需要决定将分配给进程的物理页暂时回收,这些”高级操作“就涉及到操作系统的虚拟内存管理了。
Linux上的ELF文件是一个程序,ELF文件在构建时有链接视图,也即是将多个目标文件拼装成一个可执行可定位二进制ELF时,将各个目标文件相同的section放到一起。
ELF文件也包含了执行视图。可执行文件实际上不止代码段,还有数据段、BSS段等。映射到虚拟地址空间的往往不止一个段。ELF文件被映射时是以系统的页长度为单位,也就是说站在操作系统的角度,操作系统实际上不关心可执行文件各个段的实际内容,因为这些可执行二进制指令,最终是由硬件CPU去解释去执行,操作系统只需准备好指令和数据即可。它只关心一些与装载相关的问题,最重要的是段的权限(可读,可写,可执行),对操作系统而言,段的权限只有几种组合,基本上是三种:
以代码段为代表的权限为可读可执行段以数据段和BSS段为代表的权限为可读可写的段以只读数据段为代表的权限为只读的段相同权限的段(这里是指链接视图的section),合并到一起当作一个段(执行视图的segment)进行映射。
操作系统的VMA除了用于映射可执行文件中的segment以外,还用于对进程的虚拟地址空间进行管理,比如程序执行时需要用到栈(stack)和堆(heap)等空间,在进程的虚拟空间中是以VMA段的形式存在。每一个进程的栈和堆都有一个VMA与之对应。
一个进程基本上有以下几种类型的VMA区域:
代码VMA,只读、可执行,有映像文件(ELF中有section与之对应)数据VMA,可读写、可执行,有映像文件堆VMA,可读写、可执行,无印象文件(ELF中没有section与之对应),匿名,可向上扩展。栈VMA,可读写、不可执行,无映像文件,匿名,可向下扩展介绍了在Linux系统的bash shell下输入一个命令执行某个ELF程序时的过程:
1、bash进程调用fork()系统调用创建一个新的进程
2、新的进程调用execve()系统调用执行指定的ELF文件,bash进程继续返回等待刚才启动的新进程结束,继续等待用户输入命令。
3、在execve()系统调用中,sys_execve()进行一些参数检查和复制之后调用do_execve()。do_execve()首先查找被执行文件,读取前128个字节,根据前4个字节可以确定可执行文件格式和类型,比如ELFW文件、Java可执行文件、shell脚本、perl、python等等,并且确定具体的解释程序的路径。接着do_execve()根据这128个字节调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理程序。
如ELF的装载处理程序叫做load_elf_binary(),a.out可执行文件的装载过程叫做load_aout_binary(),可执行脚本的装载处理过程叫做load_script()。
load_xxx_binary()会寻找动态链接的.interp段,设置动态链接器路径,根据ELF可执行文件的程序头表,对ELF文件进行映射,将ELF文件映射到进程的虚拟地址空间,初始化ELF的进程环境,将系统调用的返回地址修改成ELF可执行文件的入口点,对于静态链接的ELF可执行文件,这个入口点是ELF文件的文件头中e_entry所指的地址,对于动态链接的ELF可执行文件,程序入口点是动态链接器。
当load_xxx_binary()执行完,load_xxx_binary返回的地址改成了被装载的ELF程序的入口地址(静态链接),load_xxx_binary返回到do_execve(),再返回到sys_execve(),所有系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是ELF程序开始执行,ELF可执行文件装载完毕。
null
null