操作系统的进程管理
需求
操作系统有 4 个重要的功能:文件管理、内存管理、外设管理和进程管理。因此,有必要深究一下操作系统管理进程的方式。
在计算机中,进程和操作系统的视角是不对等的。利用虚拟化技术,操作系统让进程产生一种幻象:进程占据了物理机中所有的计算资源。但实际上,操作系统可能同时运行许多进程。
理解这种不对等视角对理解进程管理有很大的帮助。操作系统就像一个花花公子,同时和许多妹子 date,而每个妹子都认为自己是这个花花公子的唯一。
进程的视角
进程的用户区虚地址空间
1 | |
一个程序的典型内存布局如上图所示,不同程序的逻辑段长度可能不同,比如代码段可能是 0x8000 或者 0xC000 的长度,但逻辑分布是不变的。
- 代码段存放了程序定义的所有函数,比如
main0、printf等。 - 数据段 存放了带初值的全局变量;
- 只读数据段 存放了常量;
- bss 段存放了不带初值的全局变量,你可能会在学习高程的时候有疑问,凭什么不带初值的全局变量会初始化为 0,实际上这是操作系统对程序分配内存时所做的工作,对 bss 段清 0;
- 堆存放了通过
malloc或new分配的动态内存; - 栈用来存放函数的局部变量,注意到栈从内存地址的最高端开始,这样方便堆和栈此消彼长地动态利用所有剩余的内存空间。
进程的完整虚地址空间
前面讲的是进程的用户区,在低地址,存放应用程序。而高地址区存放操作系统内核。
在 Unix V6++ 系统中,用户区是 $[0, 0\text{x}800000)$,核心区是 $[0\text{xC}0000000, 0\text{xC}0400000)$
在核心区中,会存放代码、全局变量,内核堆,页表区以及 User + 核心栈。

看到许多不熟悉的标识符,不要紧,下面会逐个讲解。
操作系统的视角
进程管理
在操作系统的视角中,为了维护一个进程的信息,需要如下三个部分:PCB,应用程序,内核。而一个进程私有的部分是:PCB、应用程序和核心栈。
应用程序前面已经讲解过了,就是一个程序的代码段、数据段等一堆逻辑段信息,分配了虚拟内存空间。没什么难度。
PCB 由两个内容组成:Process[n] + user。
Process 叫进程基本控制块,属于核心区中的全局变量部分,常驻内存。Process 是一个数组,每一项会存放进程的一些状态信息,如 PID、优先级、运行状态、一些指针等等。它必须常驻内存,否则操作系统就失去了对进程的控制。
user 结构存放了进程使用的寄存器保存区、文件描述符表、当前工作目录等信息。
核心栈存放了执行系统调用所需的子程序栈帧。
接下来你可能又有许多疑问,比如,页表区是什么?在虚地址空间中,每个进程在内核区都有代码、全局变量、页表区这些公共部分,会不会造成重复?这就是映射的魅力,看上去每个进程都有一个副本,实际上指针指向了同一个地方。
物理地址映射
虚拟内存是幻象,看上去每个程序都能独占无限的地址空间,实际上还是要映射到有限的物理内存中。
我觉得可以这样理解物理内存的语义划分。系统区存放了所有进程需要共用的信息,用户区的代码段存放了同一个程序的不同副本——同一程序不同进程之间可以共用的信息,如代码段和只读数据段(当然 Unix V6++ 把只读数据段放到了可交换区),可交换部分则存放了每个进程自己的内容,无法共享的内容。
这里有一个特殊的地方,核心空间页表的最后一项映射到了 p_addr 的起始位置,这是当前虚拟空间所对应进程 PPDA 区的首地址。因为确实是核心空间,但是却是可交换的,所以有这样一个孤立的映射逻辑。

注意到有两个指针,x_caddr 和 p_addr。
前者在 Text 结构中,属于内核的全局变量部分,和 Process 数组一样,表示 core address of text,指向共享代码段在物理内存中映射的位置。
后者在 Process 数组中,指向进程的可交换部分物理内存的首地址。

总之,在映射的时候,需要把逻辑结构打散,一一对应到物理结构中。上帝的归上帝,凯撒的归凯撒。例如下面这个例子。
一个地址映射的例子
Unix V6++ 系统,进程 PB 有 3 页代码段,1 页数据段,1 页 bss 段,2 页堆栈。x_caddr = 0x600000(代码段,物理内存首地址),p_addr = 0x800000(可交换部分,物理内存首地址)。
求进程 PB 的页表。
解:
| 虚拟页号 | 物理页号 |
|---|---|
| 0x401 | 0x600 |
| 0x402 | 0x601 |
| 0x403 | 0x602 |
| 0x404 | 0x801 |
| 0x405 | 0x802 |
| … | null |
| 0x7fe | 0x803 |
| 0x7ff | 0x804 |
| … | null |
| 0xC000 | 0 |
| 0xC001 | 1 |
| … | … |
| 0xC03fe | 0x3fe |
| 0xC03ff | 0x800 |
分析一下,首先,从 0x401 到 0x403 是代码段,从 x_caddr 开始映射。0x404 是数据段,0x405 是 bss 段,应该从 p_addr 指向的位置开始,但是由于 0x800 留给了 0# 进程,因此从 0x801 开始。
之后,从 0xC000 开始是系统区,前 1023 个页面按照递增的顺序映射,最后 0# 进程的 PPDA 映射到 p_addr 指向的位置。
一个类比
程序就像是和花花公子 date 的女孩子,而花花公子就是操作系统。在女孩子眼中,她是花花公子的唯一,因为在她目之所及的所有地方,她能够使用花花公子的全部资源与时间(虚拟内存空间)。但花花公子不一样,他需要通过 PCB 记录每个女孩子的信息,她是谁?她高兴吗?她在心中的优先级如何?上次聊天进行到了哪里?
在花花公子的秘密日记本中,process 数组是必须存在的,因为它记录了每个女孩子的信息。如果花花公子很忙,和女孩子说“过一会再联系(swap)”,那必须在 process 中知道女孩子被 swap 到了什么地方,不然就丢失了这一信息。
相比之下,核心栈和 user 结构可以 swap 走,只要能根据 process 重新得到它们便没问题。
你会说,其实女孩子也知道花花公子有个日记本(进程的虚拟内存空间包含系统区),会不会他的秘密暴露了?不会的,因为处在用户态的女孩子没有权限翻阅花花公子的日记本,只有在内核态时才有可能访问内核空间。内核态的女孩子已经被花花公子迷住了,早就忘记了日记本的事情。
如果觉得我总结的不好,那就看看 AI 的解读吧:
1 | |
