3操作系统概念(第10版)

进程

执行的基本单位

进程进程状态进程调度上下文切换

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章 进程(执行的基本单位)

一、导读

1.1 本章学习目标

理解进程的定义及其与程序的区别

掌握进程的状态模型(五状态模型和七状态模型)及状态转换的触发条件

理解进程控制块(PCB)的结构和各字段的作用

掌握进程调度的基本概念,包括调度队列和调度器类型

理解上下文切换的过程及其性能开销

掌握进程创建和终止的机制,包括fork/exec和僵尸/孤儿进程

理解进程间通信(IPC)的主要方式:共享内存和消息传递

了解RPC(远程过程调用)和管道通信的基本原理

1.2 为什么本章如此重要

进程是操作系统中最核心的抽象概念之一,它代表了一个正在执行的程序实例。理解进程的概念是掌握操作系统工作原理的关键,因为现代操作系统的几乎所有功能都围绕进程管理展开。CPU调度本质上是决定哪个进程获得CPU执行权,内存管理是为每个进程分配和管理地址空间,文件系统为进程提供持久化数据访问,I/O系统为进程提供设备访问接口。可以说,进程管理是操作系统的核心任务。

从实践角度看,进程的概念对于系统编程至关重要。在Linux/UNIX系统中,进程是资源分配的基本单位,理解进程的创建(fork)、执行(exec)和终止(exit)机制是编写系统程序的基础。理解进程间通信机制有助于设计分布式应用和微服务架构。理解进程调度有助于优化程序的性能特征,例如通过调整进程的优先级和调度策略来满足不同场景的需求。

此外,进程的概念是理解后续章节的基础。第4章的线程是进程内的执行单元,第5章的进程同步解决的是并发进程之间的协调问题,第6章的死锁涉及进程对资源的竞争,第7章的内存管理以进程地址空间为管理对象。没有对进程的深入理解,后续章节的学习将缺乏基础。

1.3 核心问题引导

程序和进程有什么本质区别?为什么同一个程序可以同时有多个进程在运行?

进程的状态转换是如何触发的?哪些由操作系统控制,哪些由进程自身行为触发?

上下文切换的开销包括哪些?为什么上下文切换被认为是"纯开销"?

fork()系统调用的返回值为什么有两个不同的值?子进程和父进程如何区分?

共享内存和消息传递各有什么优缺点?在什么场景下应该选择哪种方式?

僵尸进程和孤儿进程是如何产生的?操作系统如何处理它们?

进程调度中的抢占式调度和非抢占式调度有什么本质区别?


二、核心概念详解

2.1 进程的概念与组成

进程(Process)是正在执行的程序的实例,是系统进行资源分配和调度的基本单位。进程与程序有本质区别:程序是存储在磁盘上的静态指令集合,是被动实体;进程是加载到内存中正在执行的活跃实体,具有动态的执行状态。同一个程序可以同时对应多个进程,例如多个用户同时运行文本编辑器,每个都创建一个独立的进程。

一个进程在内存中的布局通常包括以下几个部分。文本段(Text Section)包含编译后的机器指令,通常是只读的。数据段(Data Section)包含已初始化的全局变量和静态变量。BSS段包含未初始化的全局变量和静态变量,系统自动将其初始化为零。堆(Heap)用于动态内存分配(如malloc/new),从低地址向高地址增长。栈(Stack)用于存储局部变量、函数参数和返回地址,从高地址向低地址增长。

进程控制块(Process Control Block, PCB)是操作系统为每个进程维护的数据结构,包含进程的所有关键信息。PCB的主要字段包括:进程状态(当前处于什么状态)、程序计数器(下一条要执行的指令地址)、CPU寄存器(用于进程被中断时保存的寄存器值)、CPU调度信息(优先级、调度队列指针、时间片等)、内存管理信息(基址寄存器、页表等)、I/O状态信息(分配的I/O设备列表、打开的文件列表等)。PCB是操作系统进行进程管理的核心数据结构,进程的所有信息都存储在PCB中。

2.2 进程状态与生命周期

进程在其生命周期中会经历多个状态,经典的五状态模型包括:新建(New)——进程正在被创建;就绪(Ready)——进程已加载到内存并等待分配CPU;运行(Running)——进程正在CPU上执行;阻塞/等待(Blocked/Waiting)——进程等待某个事件发生(如I/O完成、信号量就绪);终止(Terminated)——进程完成执行或被强制终止。

状态转换由特定事件触发。就绪到运行由调度器选择进程上CPU执行触发(admit/dispatch)。运行到就绪由时间片用完或被更高优先级进程抢占触发(preemption)。运行到阻塞由进程主动请求等待的事件触发(如调用read()等待I/O)。阻塞到就绪由等待的事件发生触发(如I/O完成中断)。就绪到终止由进程正常退出或异常终止触发。运行到终止同样由进程退出触发。

扩展的七状态模型增加了挂起就绪(Suspend Ready)和挂起阻塞(Suspend Blocked)两个状态。挂起状态表示进程被换出到磁盘(交换空间),不占用主内存。当系统内存紧张时,操作系统会将某些进程换出到磁盘以释放内存。挂起就绪的进程被换入后进入就绪状态,挂起阻塞的进程在等待的事件发生且被换入后进入就绪状态。

2.3 进程调度与上下文切换

进程调度是操作系统选择就绪进程分配CPU的过程。调度器维护一个或多个就绪队列(Ready Queue),每个队列中的进程按照特定顺序排列。调度算法决定从就绪队列中选择哪个进程上CPU。调度器分为长期调度器(Long-Term Scheduler,控制多道程序度,决定哪些进程进入就绪队列)、短期调度器(Short-Term Scheduler,频繁执行,选择当前运行进程)和中期调度器(Medium-Term Scheduler,负责进程的换入换出)。

上下文切换(Context Switch)是CPU从一个进程切换到另一个进程的过程。切换时,操作系统需要保存当前进程的CPU上下文(寄存器值、程序计数器等)到其PCB中,然后从目标进程的PCB中恢复其CPU上下文。上下文切换还涉及更新各种系统表和内存管理信息。上下文切换是纯开销——切换期间CPU不做任何有用工作。切换速度取决于内存带宽、寄存器数量和缓存刷新等因素,典型的上下文切换时间为几微秒。

2.4 进程创建与终止

在UNIX系统中,进程通过fork()系统调用创建新进程。fork()创建一个与父进程几乎完全相同的子进程,子进程获得父进程地址空间的副本(现代系统使用写时复制Copy-on-Write优化,实际不复制内存页直到任一方修改)。fork()在父进程中返回子进程的PID,在子进程中返回0,通过返回值区分父子进程。子进程通常接着调用exec()系统调用加载并执行一个新程序,exec()替换当前进程的地址空间为新的程序。

进程终止时,操作系统需要回收该进程占用的所有资源,包括内存、打开的文件和I/O缓冲区等。在UNIX中,进程通过exit()系统调用自愿终止,或因收到信号、未处理异常等原因被强制终止。终止时,内核向父进程发送SIGCHLD信号,进程的PCB中保留退出状态供父进程读取。

僵尸进程(Zombie Process)是已经终止但父进程尚未调用wait()/waitpid()获取其退出状态的进程。僵尸进程的PCB仍然保留在进程表中,占用一个进程ID。如果父进程长期不调用wait(),僵尸进程会累积。孤儿进程(Orphan Process)是父进程已终止但自身仍在运行的进程,孤儿进程会被init进程(PID=1)收养,由init负责在其终止后调用wait()回收。

2.5 进程间通信

进程间通信(Inter-Process Communication, IPC)机制允许进程之间交换数据和协调行为。主要的IPC方式有两种基本模型:共享内存(Shared Memory)和消息传递(Message Passing)。

共享内存允许两个或多个进程共享一块内存区域,进程通过直接读写这块区域来交换数据。共享内存是最快的IPC方式,因为数据不需要在进程之间复制。但共享内存需要进程自行解决同步问题——如果一个进程正在写入共享区域而另一个进程同时读取,会导致数据不一致。通常需要使用信号量或互斥锁来同步对共享内存的访问。在POSIX系统中,共享内存通过shm_open()和mmap()系统调用创建和映射。

消息传递通过操作系统提供的消息传递设施在进程之间交换信息。进程使用send()和receive()原语发送和接收消息。消息传递比共享内存更安全,因为操作系统负责管理消息的传递和同步,不需要进程自行处理同步问题。但消息传递通常比共享内存慢,因为涉及系统调用和数据拷贝。消息传递可以是直接的(进程显式指定发送和接收对象)或间接的(通过邮箱/端口中转)。

管道(Pipe)是一种简单的IPC机制,允许一个进程将数据发送给另一个进程。普通管道(Pipe)具有单向数据流,只能在具有亲缘关系的进程(如父子进程)之间使用。命名管道(FIFO)允许不相关进程之间通信,在文件系统中有一个名字。


三、深入分析与讨论

3.1 写时复制(Copy-on-Write)技术

传统的fork()实现会完整复制父进程的整个地址空间给子进程,这在地址空间很大时开销巨大。现代操作系统采用写时复制(COW)技术优化这一过程:fork()时父子进程共享相同的物理内存页,这些页被标记为只读。当任一方尝试写入时,触发页错误(page fault),操作系统此时才为写入方分配新的物理页并复制数据。

COW技术极大地提高了fork()的效率,特别是在子进程紧接着调用exec()的场景中(如shell执行命令)。因为exec()会立即替换整个地址空间,之前对父进程地址空间的复制完全是浪费。COW确保了只有在真正需要写入时才进行复制,避免了不必要的内存分配和数据拷贝。

3.2 进程调度的层次结构

现代操作系统的进程调度通常采用多级反馈队列(Multilevel Feedback Queue)策略,结合了多种调度算法的优点。就绪队列被分为多个级别,每个级别使用不同的调度算法和时间片大小。高优先级队列的时间片较短(适合交互式进程),低优先级队列的时间片较长(适合计算密集型进程)。

新进程进入最高优先级队列。如果进程在时间片内用完CPU,则降级到下一个低优先级队列。如果进程在时间片内主动让出CPU(如等待I/O),则保持在当前优先级或升级。这种机制自动区分了I/O密集型和CPU密集型进程,实现了良好的响应时间和吞吐量平衡。长期不运行的低优先级进程会逐渐被提升到高优先级队列,防止饥饿。


四、实践案例

4.1 Linux进程管理实战

Linux使用task_struct结构体作为PCB,包含进程的完整信息。Linux的进程创建通过clone()系统调用实现(fork()和vfork()是clone()的特例),clone()允许精确控制父子进程共享哪些资源(地址空间、文件描述符表、信号处理等)。Linux使用写时复制优化fork(),使用CFS(Completely Fair Scheduler)作为默认调度器,通过虚拟运行时间(vruntime)实现公平调度。

Linux的进程组织采用树形结构,每个进程(除init外)有且仅有一个父进程。/proc文件系统提供了查看进程信息的接口,如/proc/[pid]/status显示进程状态,/proc/[pid]/maps显示内存映射。ps、top和htop等工具提供了用户友好的进程查看界面。

4.2 管道在Shell中的应用

Shell中的管道(|)是进程间通信的经典应用。当用户输入ls | grep ".txt" | wc -l时,shell创建三个子进程和两个管道。ls的输出通过第一个管道传给grep,grep的输出通过第二个管道传给wc。三个进程并发执行,形成流水线式的并行处理。

管道的实现依赖于内核的缓冲区(通常为64KB)。当管道满时,写入方被阻塞;当管道空时,读取方被阻塞。这种背压机制自然地实现了生产者和消费者之间的同步。管道是UNIX哲学"组合小工具完成复杂任务"的基础设施。


五、常见误区与注意事项

进程不等于程序:程序是静态的代码文件,进程是动态的执行实例。同一个程序可以有多个进程同时运行,每个进程拥有独立的地址空间和资源。

fork()后父子进程的执行顺序不确定:fork()返回后,父子进程是并发执行的,谁先执行取决于调度器的决策。程序不能假设fork()后子进程一定先执行或父进程一定先执行。

僵尸进程不消耗内存但占用进程表项:僵尸进程的内存和资源已经释放,但PCB仍然保留在进程表中。大量僵尸进程会耗尽进程表,导致无法创建新进程。

共享内存需要同步机制配合:共享内存本身不提供同步,多个进程同时访问共享内存可能导致竞态条件。必须配合信号量、互斥锁等同步原语使用。

上下文切换的开销不仅仅是保存寄存器:上下文切换还涉及TLB刷新、缓存失效和内存映射切换等隐性开销。频繁切换会导致缓存命中率下降,实际开销远大于理论值。


六、本章小结

本章深入讲解了进程的概念和管理机制。进程作为资源分配的基本单位,拥有独立的地址空间和系统资源,由PCB记录其完整状态。进程在其生命周期中经历新建、就绪、运行、阻塞和终止等状态,状态转换由调度器和事件触发。上下文切换实现了多任务并发执行,但带来不可忽视的性能开销。进程通过fork/exec机制创建,通过exit/wait机制终止。进程间通信通过共享内存和消息传递两种基本模型实现,各有优缺点。理解进程管理是掌握操作系统工作原理的关键,为后续学习线程、同步、死锁等主题奠定基础。


七、思考题与练习

比较进程与程序的区别,解释为什么需要引入进程的概念。

画出进程的五状态模型图,标注每个状态转换的触发条件。

分析fork()系统调用的执行过程,解释为什么fork()有两个不同的返回值。

比较共享内存和消息传递两种IPC方式的优缺点,各举一个适用场景。

解释僵尸进程和孤儿进程的产生原因,说明操作系统如何处理它们。

分析上下文切换的开销来源,讨论如何减少上下文切换的频率和影响。

设计一个场景,说明为什么需要多级反馈队列调度策略。

编写一个程序,使用fork()创建子进程,使用管道实现父子进程之间的通信。

3/10进程
0%