在讲到进程与线程的区别时,一定会谈到的一点是:进程具有独立的地址空间,是内存分配的最小单元。那么这个地址空间指的是什么呢?
地址空间是操作系统对物理内存的抽象,书上对地址空间的定义是:“地址空间是一个进程可用于寻址的一套地址集合”。称为空间,所以是一个地址集合理解起来不难,最让人困惑的地方在于,为什么叫对内存的抽象,地址空间又是一种怎样的存在?
如果看过设计模式的人可能会知道,设计模式中提到最多的概念之一就是抽象,纯虚的基类作为接口就是对各种派生类对象的抽象。调用接口的用户,并不知道内部如何实现,因此内部实现的方法可能也有多种。地址空间也可以这样理解,32位机上,创建进程时操作系统为进程分配4GB的独立地址空间,用户可以使用这4GB的独立地址空间。但是,反过来一想,给每个进程都分配4GB地址空间,对于8GB内存的计算机而言岂不也就能同时运行两个进程。对于现代计算机而言,这显然是不可能的。所以实际上,用户能使用的4GB地址空间并不是对应物理内存的4GB,具体怎么实现被封装了,所以叫内存抽象。
要理解操作系统对内存的管理,首先需要理解两个概念,交换技术和虚拟内存。
计算机是多道程序设计,可以同时运行多个程序,然而内存大小有限,操作系统如何实现同时运行多个程序呢?
如下图,蓝色区域表示空闲的内存,绿色区域表示被某个进程占用的内存。刚开始装入A进程,然后装入B进程,再装入C进程。
对于这种模型而言,也是一种内存抽象,对于进程B而言假设其地址空间为0x0000-0xFFFF,对于其地址0x0000而言,对应的物理地址肯定不是0x0000,那应该是进程A的首地址。所以如果进程B要访问其首地址0x0000,就必须加上一个偏移量,而这样偏移量保存在一个基址寄存器中,除了基址寄存器还有一个界限寄存器防止访问越界。
再接着上面的说,如果这时候来了一个进程D,剩余的内存放不下进程D了,这时候可以选择将进程B交换出去,将进程B的数据保存到硬盘上,将进程D装入内存运行,如果CPU重新调度到进程B然后再采用同样的方式将某个进程交换出去,保存到硬盘上,把进程B装入内存中,这样的过程就叫交换技术。
交换技术似乎解决了多道程序运行的问题,但是实际上如果每次交换一整个进程的数据,CPU需要花费数秒的时间来处理,这显然是不能被容忍的。因此,需要提出虚拟内存的概念。
虚拟内存:每个进程拥有自己的地址空间,这个空间被分割成多个连续地址范围的页面,这些页面被映射到内存实际物理地址,但是操作系统给每个进程分配的实际物理地址远没有其地址空间那样大,因此任何时候都只有一部分页面映射到实际物理地址的页框(内存也被分成了一个个小块,称为页框)。没有映射到物理地址的页面上的数据被保存在硬盘上。当进程范围地址空间的某个地址时,操作系统将这个地址传给MMU,MMU中保存了该进程的所有页表,通过对应的地址查询页表,页表上记录了该地址是否映射到内存,如果是,则将映射的实际物理地址传给内存总线。如果没有映射到内存上,则发生缺页中断,将所缺页从硬盘交换到内存上,将内存中一个页框交换出去到硬盘上。因为,交换的数据少因此速度很快。而且,对于一个进程而言,常用的数据可能就集中在一小部分数据上,所以中断发生的次数也没有上面交换技术那样频繁。
虚拟内存
在提内存管理之前,需要先解释为什么需要内存管理?这是因为,现代计算机都是多道程序设计,计算机需要同时运行多个程序,那么需要将所有运行进程使用的数据放在内存上,但是计算机物理内存有限,因此需要将部分数据放在你硬盘上,当需要访问到这些硬盘上的数据时,操作系统将这些数据从硬盘上已到内存上。那么某个进程如何访问到该进程的数据,如何在硬盘和内存之间交换数据就需要进行内存管理。
数据访问
在创建进程时,操作系统为每个进程分配独立的地址空间,这个地址空间就是虚拟地址,对于32位系统而言是4GB。这个地址空间,被分为四段:文本段,数据段,堆段和栈段,栈一般在地址空间的顶部,在栈的顶部一小部分在程序启动的时候被装入进程的环境变量等。堆由低到高生长,栈由高向低生长。当程序访问该地址空间上的某个数据时,操作系统将这个虚拟地址传给MMU,MMU里面为每个进程维护了一张页表,这张表说明了每个进程的虚拟地址到实际物理地址的映射。实际上虚拟和地址和实际物理地址被分成了一个个小片,每个虚拟小片(页表)都映射到物理地址的一个小片(页框),通过一个基地址(页框号)加上一个偏移量就能访问到指定的字节。在MMU维护的页表上不仅有这个映射关系还有一些标志位,有个一在不在位表示访问的虚拟地址的数据是否在内存上,如果在那么MMU就将映射的实际物理地址传给CPU,如果不在则发生缺页中断,操作系统将该页数据从硬盘上交换到物理内存上,将物理内存的一页交换出去。但是应该交换哪一页呢?因此,有了许多的页面置换算法:先进先出算法,最近未使用算法,第二次机会页面算法,工作集时钟算法等。都是为了时交换的次数尽量减小,毕竟数据复制会很占用时间。
内存分配
上面已经阐述了一个进程如何访问内存数据的,接着说内存是如何分配的。那么对于每一个进程而言,操作系统又是如何给其分配实际物理内存空间的呢?操作系统内核为内存维护了一张空闲内存表,这张表一般来说是一个链表,为什么是链表,因为会经常发生从中间删除和插入,你又可以去扯链表的特点了。当分配内存时,操作系统去查询这张表,找到一块能容纳下的地方放进去,将剩余的返还给空闲表。如何找到这个容纳的地方有出现了多种算法:首次适配算法,第二次首次适配,最佳适配算法,最差适配算法。这四个算法你可以去细讲差别,首次适配第一次找到第一个大于需要的地址空间的块,每次都从表头开始找,第二次着从上次找到的位置开始找,最佳适配找一个和需要大小最接近比需要的大的,最差每次早最大的。实际上对于进程内部堆的分配,页可以采取同样类似的办法。
如果嫌不够,咋们还可以继续,对unix系统而言,fork会创建一个父进程的副本作为子进程的地址空间,他们会共享文本段。但是,复制数据太慢,因此出现了写时复制,一开始父子进程共享所有数据,只有当父进程或子进程需要修改某个数据时,才会该数据块创建副本,一般是虚拟地址一页。