进程的状态
一个进程在其生命周期中,通常处于以下三种基本状态之一:
- 运行态 (running state):进程正在处理机上运行。
- 就绪态 (ready state):进程已具备运行条件,正在等待分配处理机。
- 阻塞态 (Blocked state):进程因等待某事件(如I/O完成)而暂时无法运行。
新创建的进程首先进入就绪态。当系统调度该进程执行时,它进入运行态。如果在运行期间被其他进程打断(例如,时间片用完),或者有更高优先级的进程需要执行,该进程将回到就绪态。如果进程在运行过程中需要等待某些资源(且该资源当前不可用),它将转入阻塞态。一旦所需资源准备就绪,进程将被转入就绪态,等待再次被调度执行。

其它状态
某些操作系统会定义更丰富的进程状态。
例如:
- 当一个新进程正在被创建,尚未进入就绪队列时,它的状态是新状态 (new state)。
- 当进程执行完毕或被异常终止,它的状态会变成僵死态 (Terminated state)。
分时系统
分时系统是上世纪六七十年代的一项伟大发明。它通过虚拟化技术,为每个进程分配了一个虚拟处理器 (virtual CPUs)。这种方式具有以下优势:
- 利用隔离机制,使计算机能够支持多用户并发使用。
- 可以模拟其他型号的处理器,从而实现程序的兼容性。
进程控制块
操作系统利用一种称为进程控制块 (Process Control Block, PCB) 的数据结构来管理其环境中的所有运行进程。用户登录后,通常执行的第一个程序是GUI(图形用户界面)或Shell。不同操作系统的进程控制块结构大致相同,下表列出了一个常见的PCB所包含的信息:
| 信息 | 解释 |
|---|---|
| 进程状态 (process_state) | 就绪/运行/阻塞 |
| 进程ID (process_id) | 进程的唯一标识符 |
| 内存 (memory) | 该进程被分配的内存上限、虚拟地址、物理地址映射等信息 |
| 调度情况 (scheduling_information) | 该进程的优先级、调度算法相关信息 |
| 已打开的文件 (open_files) | 记录了该进程打开的所有文件描述符 |
| 寄存器状态 (registers) | 进程上一次在CPU寄存器中的信息,用于进程上下文切换(Context Switch),在进程再次运行时恢复,在阻塞或暂停时保存 |
| 父进程 | 一个指向其父进程PCB的指针 |
| 子进程 | 一个指针,指向一个记录所有子进程的链表的首节点 |
进程控制块的管理
主要有两种常见的PCB管理方式:
- 静态数组存取:用数组直接存储PCB结构体。这种方式的优点是不需要进行动态内存管理,只需将新的PCB放入空闲位置即可;缺点是如果数组未装满,会造成空间浪费。

- 指针数组与动态内存结合:用数组存储一系列指针,这些指针指向动态分配的PCB块。这种方式可以节省空间,但需要复杂的动态内存管理。

注:所谓动态内存管理,意味着需要根据需要动态地分配和释放内存空间,以添加或删除进程块。
我整理了一个表格,方便复习这些管理方式:

子进程在PCB中的储存
相对于直接使用链表来记录子进程,Linux设计了一种更高效的链表结构。下图解释了这种优化:

在上面这幅图中,子进程1、2、3是通过标准的双向链表方式记录的。当父进程0创建子进程3时,它必须在链表的末尾添加一个指针,指向子进程3。

如果我们给每个PCB添加一个指向其“同辈”(Sibling,即具有相同父进程的进程)的指针,传统的子进程链表就显得多余了。当我们给0添加一个子进程3时,只需将它与已有的同辈进程(如2)相连即可。
PCB的管理
操作系统使用多个进程表 (Process List) 来管理所有进程。等待列表 (waiting list) 用于记录那些处于阻塞态的进程,即PCB中 process_state 为 blocked 的进程。(用来记录进程状态信息的元素类型通常并非string,而是int或char)
就绪列表 (Ready List) 用于记录那些处于就绪态的进程,即 process_state 为 ready 的进程。就绪列表会根据一定的规则(调度算法)对所有进程进行排序,我将在后续的笔记中详细整理这部分内容。就目前而言,我们只需要知道这个列表会持续更新,以确保所有进程都能得到公平调度,同时不影响它们的优先级关系。
进程的终止
当一个进程终止时,不同的操作系统会采用不同的方式来处理其子进程。有的操作系统会将这些进程的父进程改为初始进程(通常是所有其他进程的祖先进程);有的系统(如UNIX/Linux的一些情况)会将该进程的所有子进程一并删除(引发级联终止)。
资源控制块
注:这一部分的内容除了在某些教科书中出现,较少在其他地方找到参考,请谨慎参考。
操作系统除了进程控制块外,还可能维护一个资源控制块 (Resources Control Block)。与进程控制块类似,其结构大致如下表所示:
| 信息 | 解释 |
|---|---|
| 资源介绍 | 解释了该资源的属性和用途 |
| 状态 | 当前该资源的使用情况(如:空闲、被占用) |
| 等待列表 | 正在等待该资源的进程队列 |
当一个进程需要使用某个资源时,会查看该资源的状态。如果该资源目前被占用,系统会把该进程从运行态转为阻塞态,并在该资源的等待列表中添加一个指向该进程的指针。当另一个进程完成了该资源的使用并释放它时,等待列表中的某个进程会转成就绪态,等待操作系统的调度。
线程
前提知识:一种抽象的理解方式
我们上面提到PCB中有一个叫寄存器状态的值,它会记录进程的程序计数器 (Program Counter, PC) 以及堆栈指针 (Stack Pointer, SP) 等关键寄存器的信息。当我们说执行某个进程时,我们需要在内存中加载这个程序的代码(二进制,也能翻译成汇编)。PC和SP是两个指针。PC指向下一条要执行的代码指令,而SP则指向堆栈的底部。随着进程的执行,PC会在代码间移动,某些操作(例如调用系统服务或函数调用)需要用到SP来管理数据栈。

正题:线程是什么?
一个进程可以将自己某些部分的模块(线程)独立、并行地运行,这被称为线程 (Threads)。你可以想象一个进程被分开执行。在进程访问资源时,有些资源并非必需的,进程可以在等待资源的同时执行其他模块。我们可以创建一个专门的线程来同步等待该资源。
1 | resource; |
上面的伪代码展示了对资源的请求。如果我们的资源一直没有被释放,整个进程就会一直处于阻塞态。然而,result1 的计算并不需要 resource。通过创建一个线程,该进程可以一边在这个长久的 while 循环里等待资源,一边进行 result1 的计算。
1 | resource; |
由于调度的需要,在引入线程概念后,我们也需要为其准备一个线程控制块 (Threads Control Block, TCB)。当一个线程被创建时,其独立的PC和SP会被复制到TCB中。因此,实际上线程和进程一样,也拥有自己的状态(就绪、运行、阻塞)。在进程(主线程)处理 some_calculation(); 的同时,这个等待资源的线程会在 while 循环中持续运行,直到获得资源为止。
根据前提知识中的模型,现在由于多了线程的PC和SP,我们可以想象有两个PC和SP在同时作业:

用户级线程和内核级线程
线程分为两种类型:用户级线程 (User-level Threads, ULTs) 和内核级线程 (Kernel-level Threads, KLTs)。用户级线程由用户程序生成和管理,系统内核并不知晓该线程的存在;而内核级线程由操作系统内核直接管理,不能被进程直接访问,需要像创建进程一样使用内核调用(系统调用)来获取。
相对来说,普通线程(用户级线程)的优点有:
- 比内核级线程更方便管理,调度开销小。
- 相比内核级线程,能创建更多线程(不受内核资源限制)。
- 程序移植到别的平台上不需要太多的更改。
当然,缺点也很明显:
- 由于系统不知道普通线程的存在,如果某一个线程阻塞了,整个进程就进入了阻塞态。
- 不能充分利用多CPU环境,因为底层把它当成了一个单一线程的进程。
许多现代操作系统会结合两者的优缺点来提升性能。例如,将用户级线程映射到内核级线程中,这样一来,就算是某个用户级线程阻塞了,其他线程也能通过内核级线程池找到接口继续运行。