程序怎么变进程
文章目录
- 分页管理
- 虚拟地址空间
- 页映射
- 页表和页表项
- 逻辑地址结构
- 缺页中断处理
- 页映射优化
- 转换检测缓冲区
- 多级页表
- 倒排页表
- 操作系统视角看装载
- 创建进程虚拟地址空间
- 共享页框
- 建立与可执行文件的映射
- 可执行文件中段地址分配策略
- 调用入口函数
- 堆和栈
分页管理
在静态链接得到一个可执行文件后,下一步就是要运行这个程序,程序要运行,就需要把其加载到内存中,这个过程叫做可执行文件的装载,此时操作系统需要构建进程,并为该进程构建地址空间,并维护虚拟地址和可执行文件和物理地址的映射关系。因此需要先来了解一下什么是虚拟地址空间
虚拟地址空间
虚拟地址空间是一种虚拟存储技术,用来解决物理内存不足和难以维护的问题,它为每一个进程提供一个远大于物理内存大小的地址范围,现代计算机中每一个进程都有自己的地址空间,它们无法直接访问物理地址,CPU眼中的内存读写地址都是逻辑值,这个逻辑值经过内存管理单元(MMU)的转换变成物理地址,这个映射关系由页表维护。这么做的直接好处——进程视角来看它享有整个内存空间,程序员再也不用考虑目标地址是否被其他进程占用的问题了(因为地址空间是进程私有的,地址变化工作也都有硬件自动完成)
Linux中将地址空间划分为2部分,内核空间和用户空间,CPU在用户态下只可以访问用户空间,在内核态下可以访问整个地址空间,内核态位于地址空间的高地址处(默认大小为1GB,32位下)
页映射
页表和页表项
为了维护逻辑地址和物理地址之间的映射关系,需要借助页表这个数据结构,页表中的一个数据项称为页表项,不论是虚拟地址空间还是物理空间,它们都以页为单位进行划分,在地址空间中一页称为页面,在物理空间中一页称为页框,每一个页表项所记录的就是页面和页框的对应关系
(这里的页表不完整)
进程想要对内存进行读写,就需要查询页表,想要查询页表就需要获得页号,怎么获得页号,解析逻辑地址
逻辑地址结构
逻辑地址不单纯是一个数字,一个逻辑地址被按位划分位虚拟页号和偏移量2部分,虚拟页号在地址高位,偏移量在地址低位(偏移量就是在距离页框起始位置的距离,实现精确定位)
因此通过逻辑地址得到物理地址的流程如下
缺页中断处理
上述流程图中有一个叫做缺页中断处理的步骤,这个步骤是保证进程能供正常运行的关键步骤之一。尽管引入每一个进程都有私有的虚拟地址空间,这让它们看起来都是独占内存的,但地址空间只不过是操作系统为它们画的大饼,物理内存有限的事实无法改变。多进程环境下将一个可执行程序全部载入内存时不够现实的,物理内存没有那么大。事实上,一个时段内一个进程的地址空间只有部分页面是真的有其对应的页框,当CPU执行时发现目标页框不存在时会产生页错误,之后发出缺页中断信号,操作系统收到缺页中断后需要进行缺页中断处理,将所需的数据加载至物理内存,并更新页表项。这里会涉及到页面置换,所谓页面置换是发生在物理空间没有足够可用的页框时采取的策略,根据一定的算法操作系统会将部分页框中的数据交换到磁盘中(这些页框中的数据可能长久未使用),而后把所需数据写入这个空闲的页框中。
页映射优化
引入虚拟地址空间一定会有一定的时间代价,因为多了一层转化,因此任何分页式系统中都需要考虑一下2个问题
- 虚拟地址到物理地址的映射必须非常快
- 虚拟地址空间很大的情况下页表也会很大,甚至达到内存放不下
这两个问题一个针对时间,一个针对空间,时间问题通过转换检测缓冲区(TLB)解决,又称为快表查询;空间问题通过多级页表或倒排页表解决
转换检测缓冲区
每一次访问内存都需要进行虚拟地址到物理地址的映射,在没有任何优化的情况下,每一次访存都需要查询页面,而页表本身也位于内存中,查询页表也需要访存,访存的次数很大程度上影响速度。因此减少访存次数是解决时间问题的管件。
TLB一般内置于MMU中,可以将TLB视为一种缓存技术,内部数据其实就是部分页表项,当MMU经过一次查询之后,会将页面和页框的映射存入TLB中,下一次访问同一位置时可以直接得到物理地址,省去了访问页表的访存步骤。
这个TLB会随着进程的运行时刻变化,也会存在置换的操作。当MMU获得一个逻辑地址时,首先查询TLB,如果存在则直接获得物理地址(根据局部性原理,大概率是存在的);如果不存在,则进行页表查询,接着从TLB中淘汰一个表项,然后从新找到的页表项来替代它;如果连页表中都不存在,则进行缺页中断处理。
页面访存时TLB中无页表中有时称为软失效;TLB和页表中均无时称为硬失效;处理硬失效的时间代价远高于软失效
多级页表
32位情况下地址空间一共有100万页(一页位4KB),64位则更多,在没有多级页表的情况下一张页表会有100万条页表项,一张页表大的竟然连物理内存都无法容纳,这种情况我们一定是不想遇到的,根据局部性原理,大多数时间对于页表的查询都只是在页表的某一段区间上,由此我们可以把庞大的页表进行拆分,当然了,这些拆分后的页表也需要管理和维护,用什么管理维护——页表的页表,称为一级页表,一级页表所管理的对象是二级页表,二级页表所管理的对象是页面和页框的映射关系。多级页表的实现也较为简单,只需要对逻辑地址进行三段划分即可,取最高n位为一级页表的页号,次高m位为二级页表的页号,低位作为偏移量即可。这样一来,进程只需要保持内存中一级页表的存在即可,至于二级页表则是用时载入内存,通过多级页表的方式指数级的降低了页表的大小。
倒排页表
多级页表在32位平台下多级页表可以有效得压缩页表大小,但是64位平台下多级页表就显得有点力不从心了(二级页表的情况下64位一级页表页表仍然高达2^52条,通过三级页表、四级页表的方式并不可取,这会增加访存次数降低效率),因此对于64位平台来说倒排页表是一个更好的选择。
倒排页表的基本思想是物理内存中每一个页框有一个表项,而不是每一个虚拟页面有一个表项,表项记录哪一个<进程,虚拟页面>对应页框本身,这样一来页表的大小取决于物理内存而不是虚拟地址空间
倒排页表带来的问题时逻辑地址到物理地址的转换变得困难,不过好在这种困难是比较容易解决的,一是通过TLB缓存机制快速查询,二是借助哈希表在软失效发生时能够快速定位页框从而避免遍历查询。
操作系统视角看装载
装载可以当作操作系统是如何建立一个新进程。装载分三步走
- 创建进程虚拟地址空间
- 建立虚拟空间与可执行文件对应关系
- 调用入口函数启动进程
创建进程虚拟地址空间
这一步的目的时构建虚拟空间和物理空间的映射,它比较简单,因为逻辑地址在静态链接完毕后就已经生成了,操作系统只需要按照可执行文件给出的逻辑值将程序指令和数据对地址空间进行写操作即可,加之地址空间本身就是一个虚拟产物,操作系统中并不需要真得实例化出一张地址空间表来维护,而是只需要为进程构建一张专属页表即可,这个页表的页表项也可以不做处理(延迟到页错误发生时写页表)
共享页框
显然,地址空间上连续不代表物理空间上连续,物理空间是按照页框为单位的,也就是说一次至少是4KB,一段一段的进行映射似乎有些浪费空间
直接按段进行映射,所需要5个页面,空间利用率低下
Unix系统对于这种情况采用段合并再映射的机制来提高空间利用率,即让段的相接部分共享一个页框,MMU进行2次映射定位到不同的虚拟页面
建立与可执行文件的映射
前面提到过由于物理空间大小有限的这个事实,一个进程的全部指令和数据不可全部存在于内存中,需要依靠缺页中断和页置换手段来阶段,要页置换,那么肯定需要先知道所需要的指令和数据在磁盘上的可执行文件的哪个位置。因此操作系统需要通过读取可执行文件的文件头构建虚拟地址和可执行文件的映射(或者可以说构建虚拟地址与磁盘地址的映射),当发生页错误时,通过此映射关系可以快速地将磁盘数据拷贝至内存
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件又被称为映像文件
可执行文件中段地址分配策略
探求可执行文件中段的分布是一件有意义的事,因为它涉及到内存空间的利用率。
一种直观的策略是严格按照段进行分配地址,以.text段和.init段(.init段保存程序初始化指令)为例,.text段的长度是4097B,.init段的长度是512B,如果严格按照段分配地址,.text将得到8192B的空间,而.init将得到4096B的空间,空间利用率为37.5%,很低。其中有大部分空间是浪费的。
因此这种策略不可取,现在主流的方式是将权限相同段作为一个整体分配地址,所谓权限相同,指的是段之间具有相同的读写执行权限,.text和.init段都是可读可执行不可写的段,不妨将它俩绑在一块分配地址,这样就只需要8192B的空间了(减少了整整1页)
调用入口函数
最后一般操作系统会将CPU的指令寄存器设置为入口函数的地址,随后CPU执行入口函数,进行全局变量的初始化,全局对象的实例化一级堆栈初始化等等,最后进入main函数运行(main函数不是程序入口)
堆和栈
堆和栈是一个进程必不可少的区域,堆负责动态内存分配,栈负责维护局部变量和函数栈帧,堆和栈也是一个虚拟内存区域(VMA),只不过它们是匿名虚拟内存区域,因为在可执行文件中并没有堆和栈,所谓堆和栈是进程才有的
通过/proc查看地址空间分布
- 堆VMA,可读可写可执行;匿名,向高地址扩展
- 栈VMA,可读可写不可执行;匿名,向低地址扩展
————————————————————————————————————