操作系统 第四章

第四章 存储管理

层次化存储体系结构

存储器 特点
第一层 存储器
第二层 高速缓存(Cache)
第三层 主存(内存)
第四层 磁盘

操作系统的任务

  1. 记录存储使用情况
  2. 分配、回收存储资源
  3. 内存不够时,数据装入和写回

基本存储管理

分类

  • 由于内存不足,需要在内存和磁盘间换进换出(进程、页面置换)
  • 不需要换进换出的系统
    特点:进程被调入运行后,始终位于内存中,直至运行结束
    • 没有交换和分页的单道程序
    • 固定分区的多道程序

单道程序存储管理

基本思路:内存划分为系统区和用户区,同一时刻只允许一道程序,应用程序和操作系统共享存储器,直至运行结束。

实现方式

(a)早期大型机和小型机:操作系统被放在RAM的最低端;
(b)掌上电脑和嵌入式系统:操作系统被放在了内存地址的最高端,且在ROM里;
(c)早期PC:操作系统被分成两部分,一部分是设备驱动程序,放在最高端的ROM中(基本输入输出系统),另一部分放在内存地址最低端。操作系统 第四章

固定分区的多道程序系统

基本思路:将内存划为n个分区,每个分区大小可以相等,也可不等。

单个输入队列

缺点:小分区输入是满的,大分区输入队列是空的。

多个输入队列

缺点:要么浪费内存空间,要么小进程会被忽视。
解决方案:至少保留一个小分区让小进程直接运行,或规定一个进程被忽略次数不能超过k次。

固定数量任务的多道程序

操作员手工划分若干个区域,并确定每个区域的起始位置和大小,在整个系统运行期间不改变。

重定位和存储保护

重定位

链接器连接的时候读取地址为绝对地址,造成连接失败。
解决方案:当一个程序被装入内存时,直接对指令代码进行修改,一次性地实现从文件内的相对地址到内存中的绝对地址的转换。为了执行这种操作,链接器必须在可执行文件中包含一个链表或位图,列出各个需要重定位的地址位置。

存储保护

一个恶意程序总可以生产一条新指令去访问他想访问的地址。
解决方案:将内存划分为2KB的块,并为每个块分配一个4位的保护码,在CPU的程序状态字(Program Status Word,PSW)中包含一个4位**,当一个进程运行,访问的内存单元的保护码与PSW中的**不符,则引起一个陷入。

重定位和存储保护的解决方案

在机器中增加基地址寄存器边界寄存器
当一个进程被调度时,把该进程所在的分区的起始地址放在基地址寄存器中,并且把这个分区的长度放在边界寄存器中。当进程访问内存单元时,硬件自动加上基地址寄存器的值。
对于每一次内存访问,都要把该地址与边界寄存器的值比较,以防止它去访问分区以外的内存。

交换技术

交换技术:把各个进程完整地调入内存,运行一段时间,再放回磁盘上。
虚拟存储器:进程即使只有一部分内容存放在内存中也能运行。

固定分区和可变分区
可变分区中,分区的个数、位置和大小随进程的进出变化,而固定分区则是固定不变的。
优点:提高内存利用率
缺点:内存的分配、回收和管理更加复杂

内存紧缩技术
把所有的进程都尽可能地往内存地址低端移动,空闲区往高端移动。

可变内存策略
进程分配内存固定,需要扩大内存时需将其移动到内存中一个足够大的空间中。
进程被换进或移动时为其分配一点额外的内存。

基于位图的存储管理

内存被划分为可能小到几个字或大到几千字节的分配单位,每个分配单位对应于位图中的一位,0表示空闲,1表示占用。

位图的大小仅仅取决于内存和分配单位的大小,分配单位越小,位图越大。
缺点:在位图中查找指定长度的连续0串是一个缓慢的操作。

基于链表的存储管理

维持一个已分配和空闲的内存段的链表,链表中的每一个表项都包含下列内容:指明是空洞(H)还是进程(P)的标志,开始地址、长度和指向下一个表项的指针。

内存释放

一个要结束的进程一般有两个邻居(除非是最低端或最高端的),他们可能是进程也可能是空洞,因此当一个进程结束时,内存空间会有四种组合。
操作系统 第四章

内存分配算法

**最先匹配法:**存储管理器沿着内存段链表找到一个大小大于或等于新进程的空洞,除非空洞大小和新进程大小一样,否则会把空洞分为两个部分,一部分装进程,另一部分成新的空洞,并修改相应内容。

**下次匹配法:**每次找到合适的空洞时都会记住当时的位置,下次寻找空洞时从上次结束的地方开始搜索。

**最佳匹配法:**搜索整个链表,找到能够装得下该进程的最小空闲分区。

**最坏匹配法:**每次分配空间总将最大的那个空闲区切去一部分,会导致大进程找不到合适空间。

可以将进程链表和空闲链表分离,但回收会更加复杂。
可以把空闲链表按照从小到大顺序进行排序,以提高最佳匹配法速度,在这种排序方法下,最先匹配法和最佳匹配法速度一样,而下次匹配法则没有意义。

**快速匹配法:**对于一些常用的请求大小,为他们分别设置各自的链表。
缺点:一个进程结束或被换出时寻找它的邻接块以查看是否可以合并很费时间。

虚拟内存管理

虚拟存储器

程序的代码、数据和栈的总大小可以超过实际可用的物理内存大小,操作系统把当前需要用到的那些部分保留在内存中,而把其余部分保存在磁盘上。

虚拟页式存储管理

分页

虚地址空间被划分成称为“页面(pages)”的单位,在物理存储器中对于的单位称为页框,页和页框总是相同大小的。
由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。如果没有虚拟存储机制,那么虚拟地址就是物理地址,会被直接送到内存总线上;若使用虚拟存储机制,那么虚拟地址被送到存储管理单元(MMU),由它负责把虚拟地址映射为物理地址。

把物理内存划分为许多个固定大小的内存块称为物理页面,或称为页框(frame)
把虚拟地址空间划分为相同大小的块,称为虚拟页面,或简称为页面(page)
页面的大小要求是2的整数次幂。

在实际的硬件上,会有一个有效位来描述每个虚拟页面是否在内存中。
如果程序访问了一个未被映射的页面,就会引发一个缺页中断(page fault),操作系统会从内存中挑选一个使用不多的物理页面,把它的内容写回到磁盘,从而腾出一个空闲页面,然后把引发缺页的那个虚拟页面装入该空闲页面,并更新地址映射关系。

16位的虚拟地址被划分为4位的页号和12位的偏移量。
在进行地址映射时,使用虚拟页面号作为索引去访问页表,从而得到相应的物理页面号。
如果有效位的值为0,则引发一个缺页中断,陷入到操作系统;
如果有效位的值为1,则将页表中查到的物理页面号复制到输出寄存器的高三位中,再加上输入的虚拟地址中的12位偏移量,构成15位的物理地址。
输出寄存器的内容被作为物理地址送到内存总线。

页表

虚拟地址被分成虚拟页面号(高位)和偏移量(低位)两部分。在地址映射时,使用虚拟页面号作为索引去访问页表,从而得到相应的物理页面号。然后用物理页面号来取代虚拟页面号,与虚拟地址中的偏移量进行组合,从而得到最终的物理地址。
页表的用途就是将虚拟页面映射为相应的物理页面。

页表的两个重要问题:
1.页表可能会非常大
虚拟地址空间为4GB,页面大小可以上百万个。
2.地址映射必须十分迅速
每次内存访问时都必须进行虚拟地址到物理地址的映射。

多级页表

虽然进程的虚拟地址空间很大,但当进程在运行时,并不会用到所有的虚拟地址,所以没有必要把所有的页表项都保存在内存中。
因此可以讲虚拟地址的页面号再进一步划分。

例:虚拟地址0x00403004(十进制4 206 596),该地址位于数据段12292处。
①将地址除以4MB,结果为1,余数为12292,因此PT1字段为1.
②将12292除以4KB,结果为3,余数为4,因此PT2字段为3,偏移量为4.
MMU用PT1访问第一级页表的表项1,对应于虚拟地址4MB~8MB,
在找到第二级页表后,再使用PT2来访问表项3,对应于4MB的12288~16383。
如果该页面未在内存中,则页表项中的有效位的值为0,会触发缺页中断。
如果页面在内存中,那么久吧物理页面号与页内偏移地址4组合,得到最终物理地址。

多级页表优缺点
优点:避免将进程的所有页表一直保存在内存中
缺点:需要多次访问内存以查找页表

页表项的结构

位段 说明
页框号 物理页面号
有效位 表示该页面现在是否在内存中
保护位 表示允许对这个页面做何种类型访问(只读、可读写、可执行)
修改位 记录页面是否被修改,回收物理页面时会检查
访问位 记录页面是否被访问过,用于页面置换算法
禁止缓存位 对于那些页面被映射到设备寄存器而不是常规内存页面很重要

关联存储器TLB

程序局部性原理:对于绝大部分的程序,运行时倾向于集中地访问一小部分的页面。

快速查找硬件:
TLB(Translation Lookaside Buffer)或者关联存储器:用来存放那些最常用的页表项,直接把虚拟页面号映射为相应的物理页面号,不需要去访问内存中的页表,缩短了查找时间。
TLB通常位于MMU中,包含了少量的表项,一般不超过64个。每个表项包含了一个页面信息,包括虚拟页面号、访问位、保护码和物理页面号,还有一个位表示该表项是否有效。

当一个虚拟地址到来时,MMU首先会到TLB中查找:
如果能够找到且访问合法,那么直接从TLB中把相应的物理页面号取出来;
如果能找到虚拟页面号但违反了访问权限,则引发一个保护中断;
如果未找到该虚拟页面号,那么就要采用通常办法访问内存中页表,取出相应物理页面号,同时会把TLB中某一表项改为现在这个表项,被改的表项的修改位要复制到页表项中。

反置页表

根据内存中的物理页面号来组织页表,用物理页面号来作为访问页表的索引。有多少个物理页面,就在页表中设置多少个页表项。

例:虚拟地址为64位,页面大小为4KB,物理内存大小为256MB,总页表项的个数等于256MB除以4KB,即65536,在每个页表项中,记录了在相应的物理页面中所保存的是哪个进程的哪个虚拟页面。

优点:节省大量为保存页表所需要的内存空间
缺点:查找过程复杂,因为反置页表是根据物理页面号的顺序来存放的,所以在给定了一个虚拟页面号以后,必须搜索整个页表,才能找到它所对应的物理页面号。
例如,有一个进程n,要去访问第p个虚拟页面,他不能用p作为索引查页表得到物理地址,他需要在逆向页表中查找(n,p)。
使用TLB可以解决这一的问题。另外可以用哈希表加快映射速度。

页面置换算法

最优页面置换算法

基本思路:当一个缺页中断发生时,对于内存中的每一个虚拟页面,计算在它的下一次访问之前,还需要等待多长的时间(用指令数表示),选择等待时间最长的页面。
缺点:无法实现,但可以用作其他算法的性能评价的依据。

最近未使用页面置换算法

大多数支持虚拟存储的计算机中,每个页面都有两个状态标志位:
访问位R:当一个页面被访问时,它的R位被设为1;
修改位M:当一个页面被修改时,它的M位被设为1
这两个标志位存放在页面的页表项中,要求每一次内存访问时都要进行更新,必须由硬件来自动进行置位。

R位和M位构造的简单置换算法

当一个进程被启动时,操作系统会把所有页面的这两个位设为0,在运行过程中,R位会被定期地清零。当一个缺页中断发生时,操作系统会去检查所有的页面,并根据当前的R位和M位的值把他们分为4类:

类别 说明
第0类 未被访问,未被修改
第1类 未被访问,已被修改
第2类 已被访问,未被修改
第3类 已被访问,已被修改

第1类发生在上个时钟中断时R位被清零,而到目前未被访问的时候。

最近未使用算法(NRU)

随机地从编号最小的非空类中挑选一个页面,把它淘汰出去。
如果有两个页面,一个曾经被修改过,但在最少的一个时钟周期内(通常是20ms)未被访问;另一个未被修改过,但在最近的一个时钟周期内肯定被访问过,就会淘汰前者。

优点:易于理解、实现效率高

先进先出页面置换算法

操作系统维护着一个链表,链表记录了所有内存中的虚拟页面,链首页面的驻留时间最长,链尾页面的驻留时间最短。当发生一个却也中断时,首先把链首页面淘汰,并把新的页面添加到链表末尾。

缺点:可能会淘汰常用页面,因此很少被单独使用。

第二次机会页面置换算法

基于FIFO算法,对于最古老的那个页面,检查访问R位,若R位为0;说明这个页面老且无用,应淘汰;若R为1,说明该页面曾经被访问过,因此给予第二次机会,即先把R位清零,然后把页面放到链表尾端,并修改它的装入时间,然后继续往下搜索。

优点:合理有效
缺点:需要在链表中移动页面,降低效率

时钟页面置换算法

基于FIFO和第二次机会算法,把各个页面组织成环形链表,把指针指向最古老的页面,当一个缺页中断发生时,检查指针所指向的页面,如果它的R位为0,则淘汰该页面;如果R位为1,则清零R位,并把指针指向下一个页面。
操作系统 第四章

最近最久未使用页面置换算法(LRU)

基本思路:当一个缺页中断发生时,从内存中选择最久未被使用的那个页面,把它淘汰出局。
这个算法是对最优页面置换算法的一个近似,理论依据是程序的局部性原理。
缺点:系统开销大,要维护一个链表,每次都要更新。

硬件LRU算法

要求硬件上有一个计数器C,在每条指令执行完后,C的值会增加1。在每个页表项中,必须有一个足够大的字段来存放该计数器C。每一次内存访问后,C的当前值会被存放在被访问的页面的页表项中(作为时间戳)。当发生一个缺页中断,操作系统检查页表中所有表项计数器的值,找出最小的值,就是最近最久未使用页面。

一台机器上有n个物理页面,LRU硬件可以维护一个n×n位的矩阵,初始化时全部设置。
如果第k个物理页面被访问,硬件首先会把第k行全部都设置为1,再把第k列全部都设置为0.在任何时刻,其二进制最小的那一行就是最近最久未使用的,第二小的那行就是下一个最久未使用的,以此类推。

LRU算法的软件模拟

最不经常使用算法(NFU)

对于每一个页面,算法设置一个软件技术企,初始值为0。每次时钟中断,操作系统将对内存中的所有页面进行扫描,把每个页面的R位的值加到计数器上,算法使用这个计数器来记录页面被访问的频繁程度。当发生一个缺页中断时,计数器值最小的那个页面将被淘汰出局。
操作系统 第四章
主要问题:从不忘记任何事情

老化算法

基于NFU算法,先把计数器的值右移一位,然后再把R位的值加进来;把R位加到计数器的最左端。
操作系统 第四章

页式存储管理的设计问题

工作集模型

访问的局部性:在进程运行的任何一个阶段,它只会去访问一小部分的页面。
工作集:一个进程当前使用的页的集合。
抖动:分配给进程的物理页面数太小,无法包含其工作集,频繁地在内存和外存间换页
工作集模型:页式存储管理系统跟踪进程的工作集,并保证在进程运行以前它的工作集就已经在内存中了。在进程运行之前预先装入页面,也叫做预先调页。随着时间变化,进程的工作集的内容也会发生变化。

一个进程的工作集可以用一个二元函数w(k,t)w(k,t)来表示,其中tt指的是当前执行时刻,kk称为工作集窗口。
工作集ww等于在当前时刻t之前的k次内存访问的所有页面所组成的集合,w(k,t)w(k,t)kk的一个单调非递减函数。
操作系统 第四章
假设一个进程开始时被中止运行,后来又要把它调入内存重新运行,这时就可以根据进程在中止之前的工作及内容,猜测出当它重新开始运行后,可能会去访问那些页面,这样在开始运行该进程之前,就可以预先载入这些页面,减少缺页中断的次数。

局部与全局分配策略

局部页面置换算法

在进程所分配的页面范围内选取将被置换的页面,每个进程分配固定大小的内存空间。

全局页面置换算法

在内存中所有的页面范围内选取被置换的页面,所有进程动态共享系统的物理页面,分配给每个进程的页面数动态变化。

通常情况下,全局算法的性能比局部算法的好。如果在运行期间工作集大小发生较大变化,那么全局算法的优势更明显。

在全局算法下,当一个进程刚开始运行时,可以按照进程大小的比例来分配物理页面数,再动态地调整大小。
具体可以用缺页率算法,该算法用于控制分配集的大小,即什么时候应该增加、什么时候应该减少分配,但不关心缺页中断。
缺页率:一秒钟内出现的缺页次数
定义缺页率上下界,使得进程缺页率在其范围内。
超出上届A就会出现抖动现象,低于下届B则说明进程得到的物理页面数太多。

负载控制:系统内运行的进程过多,无法使所有进程的缺页率都低于A,则需要将一些进程换出至外存。

页面大小

内碎片

在页式存储管理中,系统会自动把一个进程分划为大小固定的页面,所以在最后一个页面内可能没有装满,存在空闲的地方。
内碎片的大小一般是半个页面,设内存中有nn个段,每个页面的大小为pp字节,总的内存碎片大小就是np/2np/2,页面越小,内碎片越小。

页面越小,对于同一个程序,就需要越多页表项,所以页表就越大;而且传输一个小页面和大页面时间差不多,这样页面越小,时间越长。

假设进程平均大小ss字节,页面大小pp字节,每个页表项需要ee字节,每个进程需要的页面数为s/ps/p,占用页表空间为se/pse/p字节,浪费的内存为p/2p/2,因此总开销为
=se/p+p/2总开销=se/p+p/2
如果页面比较像,那么上式第一项就越大;如果页面比较大,那么第二项就越大。通过对pp求导得p=2sep=\sqrt{2se}

虚拟存储器接口

通常虚拟存储器对进程和程序员是透明的,即所能看到的全部是在一个带有较小的物理存储器之上一个大的虚地址空间。
允许程序员对内存映射进行某些控制,可以实现两个或多个进程共享同一段内存空间,即页面共享
页面共享可以用来实现高性能的消息传递。
分布式共享存储器:允许在网络上的多个进程共享一组页面,这些页面可以组成一个共享的线性地址空间。

段式存储管理

段式管理把内存视为二维空间,将程序的地址空间划分为若干个段,程序加载时,分配器所需的所有段(内存分区),这些段不必连续;物理内存的管理采用动态分区。
程序通过分段划分为多个模块,如代码段、数据段、共享段。

  • 可以分别编写和编译
  • 可以针对不同的类型的段采取不同的保护
  • 可以按段为单位来进行共享,包括动态链接进行代码共享

优点:

  • 没有内碎片
  • 便于改变进程占用空间的大小
  • 易于实现代码和数据共享,如共享库

问题:存在外碎片,需要通过内存压缩来消除

外碎片

段式存储系统的页面是定长的,段不是定长的。
当系统运行一段时间后,内存将会被划分为许多块,其中有些是段,有些是空洞,这种现象称为跳棋盘或外碎片。
可以通过内存压缩来消除外碎片。

段式管理的地址变换

局部描述符表(LDT):每个程序自己的代码段、数据段、栈段等。
全局描述符表(GDT):系统的段,包括操作系统本身。
为了访问某个段,Pentium程序必须先把这个段的选择符装到机器的6个段寄存器之一,CS寄存器存放代码段选择符,DS寄存器存放数据段。选择符有一位用来指出这个段是局部(LDT)的还是全局(LDT)的,另外13位用作LDT或GDT的表项号,还有两位是特权级。LDT和GDT中,描述符0是禁止的,表示该段寄存器暂且不指向任何段,如果使用这个段寄存器会陷入中断。
当一个选择符被装入某个段寄存器时,存放在LDT或GDT中的相应的描述符就会被取出,存放在微程序寄存器中,每个描述符由8个字符组成,包括段的基地址、长度和其他信息。

描述符转换过程:

  1. 根据选择符的第二位,选择LDT或GDT表格
  2. 把选择符复制到一个内部寄存器,并且把它的低3位清0
  3. 将LDT或GDT的起始地址加上去
    得到目标描述符所在位置

虚拟地址转化为物理地址

  1. 只要微程序知道正在使用的是哪个段寄存器,它就能根据该寄存器的值,从内部寄存器找到相应描述符。如果目标段不在(即选择符为0),或已经被换出,就会发生一次陷阱中断。
  2. 检查段内偏移量是否超出段的末尾。若是,也会引发一次陷阱中断。
  3. 如果目标段在内存中且段内偏移也在范围内,Pentium会把描述符中32位的基地址与段内偏移量相加,形成线性地址
  4. 一个线性地址被分为3个字段:dir,page和offset,dir用作页面目录索引,目录项存放了目标页表的起始地址,再以page索引查页表,从中得到物理页面的起始地址,最后把页内偏移量offset加上起始地址就得到物理地址,根据物理地址访问相应的内存单元。

MINIX3进程管理器概述

MINIX3不支持页式管理存储。
进程管理器:负责处理与进程管理有关的系统调用,包括存储管理(fork,exec,brk等)。另外一些调用则与信号处理、进程属性的设置和检查、CPU使用时间、实时时钟的设置和查询等相关。
PM维护着一张按照内存地址排列的空洞列表,当由于执行fork或exec需要内存时,系统将用最先匹配算法对空洞列表进行搜索,找出一个足够大的空洞。一旦一个程序被装入内存,它将一直保持在原来的位置直到运行结束,不会被换出或移动到内存的其他位置,为它分配的空间也不会增长或者缩小。

策略与机制分离

哪个进程应该被放在哪个位置的决定由存储管理器做出,具体的为进程设置的内存映像的操作由内核中的系统任务完成。
这个划分使得修改存储管理策略比较容易实现,不需要修改操作系统底层。

内存布局

组合的I和D空间

MINIX3程序可以被编译为使用组合I和D空间,即进程的各个部分共同用一个内存块作为一个整体来申请和释放

MINIX3中有两种情况需要分配内存:

  1. 在一个进程执行fork时,为子进程分配所需要的空间
  2. 在一个进程通过exec系统调用修改内存映像时,老的映像被作为空洞送到空闲表,需啊哟为新的映像分配内存。

独立的I和D空间

当一个进程FORK时,只需要分配为新进程做一个堆栈段和数据段拷贝所需数量的内存。
父进程和子进程将共享已经由父进程使用的执行的代码,即共享代码
当一个进程执行EXEC时,系统将查找进程表看看是否有另一个进程已经在使用需要执行的代码,如果找到了就只为数据和堆栈分配新内存,已经在内存的代码段被共享。
当一个进程结束时,总是要释放它的数据和堆栈占用的内存,但是只有在搜索了进程表并没有其他进程使用代码段后才会释放代码段所占用的内存。

程序文件及内存布局

磁盘文件的头部包含了进程映像各个部分的大小以及总的大小的信息。
在具有给定的I和D空间的程序头部,有一个域指出代码和数据部分的总长度,这些部分被直接拷到内存映像中。
映像中的数据部分增扩了头部的bss域指出的数量,扩大部分被清0,用于未初始化的静态数据。
总共分配的内存数量是由头文件中的total说明。
数据段的界限只有通过BRK系统调用才能修改,这个操作涉及的只是当初分配给进程的内存区,系统操作不会分配额外的内存。
操作系统 第四章
对于组合I和D空间,假如一个程序有4K代码段,2K的数据段与bss,和1K的堆栈,若文件头中说明的需要分配的内存总量是40K,那么未使用的内存将是33K。
对于独立使用I和D段的程序,total域只对结合的数据段和堆栈段有用。一个有4K正文、2K数据、1K堆栈,total域为64K的程序将被分配68K的空间(4K指令,64K数据),流出61K空间给数据段和堆栈段在运行时使用。

消息处理

进程管理器是消息驱动的。在系统初始化完成后,进程管理器就进入它的主循环,包括等待消息、执行消息中包含的请求、和发送应答。
进程管理器可以接受两种类别的消息:

  1. 系统通知消息,对于内核与系统服务器之间的高优先级通信
  2. 来源于用户进程所启动的系统调用

消息处理中的一个核心数据结构是在table.c中声明的call_vec表,它包含了一些函数指针,用来指向不同类型消息的处理函数。当一条消息达到PM时,主循环会抽出消息的类型,并把它放在全局变量call_nr中,然后去访问call_vec,从而找到相应的消息处理函数。然后调用这个函数来执行系统调用,它的返回值被放在应答消息中送会给调用者,一报告调用成功还是失败。

数据结构和算法

核心数据结构:进程表和空闲链表

进程表

进程表被分为三部分,分别被内核、进程管理器和文件管理器所用。
为了简单起见,表项是精确对应的,因此进程管理器的表项k和文件系统的表项k对应的是同一个进程。为了保持同步,在进程创建或结束时,这三个部分都要更新他们的表。

内存中的进程

PM管理器的进程表叫做mproc,定义在mproc.h中,包含了与进程内存分配有关的全部域和一些附加的信息。
最重要的域是mp_seg数组,它有三个表项,分别用于代码段、数据段和堆栈段;
各个段是一个由虚地址、物理地址和段长度组成的结构,都用块而不是字节度量。

内存分配记录

该图中的一个进程有3K代码,4K数据、1K空隙、2K堆栈内存分配是10K;
假设这个进程没有独立的I和D空间,在(b)中看到这三个段鸽子的虚地址、物理地址和长度域。在这个模型中,代码段总是空的,数据段包含了代码和数据。当进程引用虚地址0时,不管是跳转到它还是读它,将使用物理地址0x32000,这个地址位于第0xc8个click中;
内存布局在具有独立的I和D空间的情况下如(c)所示,代码段和数据段的长度都不为0。mm_seg数组主要是用于把虚地址映射成物理地址。给定一个虚地址和它所属的空间,确定虚地址是否合法,若合法再确定其对于的物理地址。
操作系统 第四章
在虚拟地址空间中,栈的起始地址取决于分配给进程的内存总量。
如果为了提供更大的动态空间,则可以使用chmem命令修改文件头,那么在下一次文件执行时堆栈将从一个更高的虚地址开始。
如果堆栈增长了一块,那么上图的堆栈表项应该从(0x8,0xd0,0x2)变成(0x7,0xcf,0x3)

当栈指针跨越了它的段边界时,MINIX3也不会发生陷入。在MINIX3中,数据段描述符和栈段描述符总是一样的。MINIX3的数据段和栈段各自使用这个空间的一部分,因此两者都可以扩展到它们之间的空隙中。

共享代码段

一个进程的数据和堆栈区域的内容可能会随着进程的执行而改变,但代码不会改变。几个进程同时执行一个程序的拷贝很常见。
当exec装入一个程序时,它首先打开保存着该程序的磁盘映像文件并读入文件头;
如果进程使用的是独立的I和D空间,系统将搜索每个mproc表项,如果找到了一个正在执行相同程序的进程,那么就没有必要为新进程代码分配内存,只要把新进程内存映射的mm_seg[T]指向已经装入代码段的位置。只需为数据段和堆栈段分配相应的内存空间。

空闲链表

定义在alloc.c中,它按照内存地址递增的顺序列出了内存中的各个空洞。
在空闲链表上的主要操作是分配一块指定大小的内存和回收一块已经分配的内存。
空闲区表项有三个字段:起始地址,长度以及指向下一个链表节点的指针。
在分配内存时,首先从最低的地址开始搜索空洞表,直到找到一个足够大的空闲区,并从空闲区中减去需要的空间。
在进程结束后,它的数据和堆栈内存区将还给空闲链表。如果进程使用的是组合I和D空间,那么它的内存都将被释放;如果使用的是独立的I和D空间并搜索进程表发现没有其他进程在共享其代码,那么它的代码段也将被释放。
对每个被归还的内存区域,如果它的任何一侧或者两侧邻接的区域也是空闲的,则将他们合并。

创建进程系统调用

FORK系统调用

fork调用过程:

  1. 检查进程表是否已满
  2. 试着为子进程的数据段和栈段分配内存
  3. 把父进程的数据和栈复制到子进程的内存中
  4. 找到一个空闲的进程表项并把父进程的表项内容复制进去
  5. 在进程表中填入子进程的内存映射
  6. 为子进程选择一个进程号
  7. 告诉内核和文件系统该进程的情况
  8. 向内核报告子进程的内存映射
  9. 向父进程和子进程发送应答消息

在下列两个时间都已经发生的情况下,进程才会完全终止:

  1. 进程自己退出(或被一个信号杀死)
  2. 父进程已经执行WAIT

已经退出或被杀死而父进程还没有为子进程执行WAIT的进程将进入某种挂起状态,有时被称为僵死状态,这种进程不再参与调度,内存被释放。僵死是一种临时状态,父进程执行WAIT时,将释放进程表项。

EXEC系统调用

exec用新的内存映像替换当前内存映像,包括设置新的堆栈
exec的过程:

  1. 检察权限——文件是否可执行?
  2. 读取文件头以获得各个段的长度和总长度
  3. 从调用者处获取参数和环境
  4. 分配新内存并释放不需要的旧内存
  5. 把栈复制到新的内存映像
  6. 把数据段(可能还有代码段)复制到新的内存映像中
  7. 检查并处理setuid和setgid位
  8. 设置相应进程表项
  9. 告诉内核,进程已准备运行

在实现过程中,需要考虑可执行文件是否能放得进虚地址空间,因为内存是以块而不是字节为段位分配的。

brk系统调用

库过程brk和sbrk用来调整数据段的上限,前者参数是绝对长度(字节),后者参数是相对于当前长度的正负增量,计算新长度再调用brk。
调用过程只需要检查各部分是否仍在地址空间中,调整表、并通知内核。

信号处理系统调用

系统中定义了一组信号,每个信号都有一个默认的动作:kill进程或忽略信号。
也可以通过系统调用改变信号响应方式,即将一个信号处理函数与特定信号绑定。
信号处理三阶段:
准备阶段:给信号准备相应的程序代码
响应阶段:信号已收到,相应的动作被执行
清理阶段:恢复进程的正常操作

准备阶段

调用系统调用以修改信号相应方式。
最常用的是SIGACTION,可以去设置信号处理方式:忽略、捕获或者恢复对某个信号的默认处理方式。
SIGPROCMASK可以阻塞一个信号,使得一个信号暂时被保存起来,只有当进程在后来某个时候接触对这个信号的阻塞才会响应。
MINIX3信号处理准备阶段完全由PM处理。

响应阶段

在信号发生时,进程管理器开始确定哪个进程应该得到这个信号。
如果信号被捕获,就必须把它传递给目的进程,这需要保存有关进状态的信息以使进程可以恢复正常运行。
如果信号不需要捕获,缺省动作将被执行,可能生成core文件,同时终止进程。
一个信号可能需要传送给一组进程,所以内存管理器可能使这些动作重复多次。

处理过程

  1. 如果信号应该被捕获,就必须把它传递给目的进程,这需要保存有关进程状态的信息以使进程可以恢复正常运行。这些信息被保存在接收信号进程的堆栈上,因此必须检查 是否有足够的堆栈空间。
  2. 随后内存管理器调用在内核中的一个系统任务把这些信息放到堆栈上。系统任务还处理进程的程序计数器,使进程能够执行信号处理过程的代码。
  3. 在信号处理过程结束时将执行一个SIGRETURN系统调用,通过这个调用内存管理器和内核共同恢复进程的信号上下 文和寄存器,使进程可以恢复正常运行。

清理阶段

在信号处理过程结束时进程应该象什么都没有发生一样继续执行,需要记录CPU所有寄存器当前值。进程表由于空间有限,只能保存一份副本,存放将进程恢复到原来状态所需的所有CPU寄存器的内容。

解决这个问题的方法

  • 进程在中断发生后,CPU所有的寄存器都被拷贝到进程表中的栈帧中
  • 在准备处理阶段,进程表中的栈桢被复制到进程自己的栈中,以保存
  • 信号处理过程是一个普通的过程,在它结束时SIGRETURN将被执行。
  • SIGRETURN的工作是把各个部分恢复成他们接收信号以前的状态,并进 行清理。最重要的是通过使用保存在接收信号进程堆栈中的拷贝,进程表 中的栈框被恢复到它原来的状态。

用户空间定时器

MIINIX3中内核空间的定时器仅用于系统进程,用户进程的定时器由进程管理器来维护。
进程管理器维护一个定时器队列,每次根据队列头部的定。时器向时钟任务发出警报请求。
在一个时钟中断后,若系统检测到一个到期的警报,进程管理器会收到一个通知,然后,它会检查自己的定时器队 列,并向相应的用户进程发送信号。