进程和线程
进程与线程的基本概念
进程的相关概念 是操作系统选择题的常见基础,关键在于进程与线程、状态转换、进程地址空间和进程间通信。本页保留“进程和线程、进程状态转换、进程内存空间的架构”这条学习线索,再补上容易混淆的父子进程与 IPC 边界。
在多道程序环境下,允许多个程序并发执行,为此操作系统引入了 进程 (Process) 的概念,以便更好地描述和控制程序的并发执行,实现操作系统的并发性。
进程
进程是系统资源分配的基本单位。进程拥有独立的系统资源,包括内存空间、文件描述符、CPU 时间片等,这些资源的分配由操作系统负责。
进程和程序
程序是 静态的,进程是 动态的,进程可以理解成运行的程序,两者的具体区别如下:
- 程序
- 程序是一组指令的静态集合,它存储在磁盘等持久性存储介质中。
- 程序是被动的,它本身并不执行任何操作。
- 一个程序可以多次运行,每次运行都会创建一个新的进程。
- 进程
- 进程是程序在内存中执行的实例。
- 当一个程序被加载到内存中并开始执行时,操作系统会创建一个进程。
- 进程是动态的,它代表了程序的执行过程。
进程控制块
操作系统通过 进程控制块(PCB,Process Control Block)对进程进行管理。 PCB 中保存了 操作系统管理进程所需的关键信息,当发生 进程切换(上下文切换) 时,需要将运行进程的状态保存到 PCB 中。
PCB 主要包含以下几类信息。字段的实际名称因系统而异;表中强调的是操作系统必须能够查询或恢复的信息语义。
| 信息类别 | 典型字段 | 操作系统为什么需要它 / 何时使用 |
|---|---|---|
| 进程标识信息 | PID、父进程 PID(PPID)、用户 / 组标识、会话或进程组标识 | PID 让内核能从进程表中唯一找到 PCB;PPID 维护创建关系,供 wait()、回收和作业控制使用;用户 / 组标识用于权限检查、资源统计和信号发送权限判断。PID 不是程序名,同一程序可同时对应多个 PID。 |
| 进程控制信息(调度信息) | 进程状态、进程优先级 / 调度策略、已用 CPU 时间、时间片、CPU 亲和性、就绪 / 阻塞队列指针 | 调度器据此判断“谁能运行、谁应先运行、谁在等什么”。状态决定进程位于运行、就绪还是某个阻塞队列;队列指针让内核可以把它迅速移入或移出相应队列,而不必遍历全部进程。 |
| 处理机状态信息 | 程序计数器(PC)、程序状态字(PSW)、栈指针(SP)、通用寄存器,以及与体系结构有关的控制寄存器 | 这是可恢复的 CPU 现场:PC 指出下次从哪条指令继续,PSW 保存运行模式和条件码,SP 与通用寄存器保证函数调用和中间计算不丢失。发生进程切换时,内核保存旧现场、恢复新现场,两个进程才能像各自连续运行一样继续执行。 |
| 资源与地址空间信息 | 页表 / 地址空间描述、代码和数据映射、打开文件或文件描述符引用、当前工作目录、I/O、信号和 IPC 相关信息 | 这些信息说明进程“拥有什么、可访问什么”。切换到该进程时,内核据此建立正确的地址空间并恢复其可用资源;很多实现保存的是指向资源对象的引用,不会在每次切换时复制实际文件或内存内容。 |
四类信息可以这样理解:
- 进程标识信息回答“它是谁、归谁管理”,即“用于唯一标识进程”。除了唯一定位 PCB,它还把父子关系、权限和账户归属连接起来;因此两个运行同一可执行文件的进程,仍能由不同 PID、PPID 和用户身份区分。最基本的典型字段是进程标识符(PID)和用户标识符(UID)。
- 进程控制信息(调度信息)回答“它现在能不能运行、应排在哪里”,它用于操作系统进行调度和管理。例如,等待磁盘 I/O 的进程处于阻塞态,不能被直接选中占用 CPU;I/O 完成后内核才将它从阻塞队列转入就绪队列。
- 处理机状态信息回答“换走后怎样从原处接着跑”,也就是保存 CPU 运行现场,实现进程切换。教材常把程序计数器(PC)、通用寄存器和程序状态字(PSW)列为典型内容;上下文切换不是重新启动程序:内核还要保存 SP 等寄存器值,随后把另一个进程的对应值装回 CPU。这样恢复后,进程看到的仍是原先那条尚未执行完的控制流。
- 资源信息回答“它能使用哪一套地址空间和对象”,即记录进程占用的系统资源,例如打开的文件、内存指针等资源。页表或地址空间信息把虚拟地址解释为该进程自己的内存;打开文件、I/O 与 IPC 信息则维持它与系统资源、其他进程之间尚未结束的关系。
**实现边界:**教材把这些统称为 PCB。真实系统可能把一部分调度现场放在内核栈、把线程专有现场放在 TCB(线程控制块)中;无论结构如何拆分,调度与恢复所需的上述语义都不能缺少。
进程控制块 在操作系统 进程 管理的多个阶段都发挥重要作用:
- 进程创建和终止:在进程创建时,操作系统为它新建一个PCB(进程控制块)。该结构在进程存在期间常驻内存,可以随时存取,并在进程结束时删除。
- 进程调度:当操作系统进行进程调度时,会根据PCB中存储的信息(如优先级、进程状态)来决定哪个进程应该获得CPU的控制权。
父子进程
在操作系统中,进程是程序执行的基本单位。父子进程是通过进程创建机制生成的一种层级关系:父进程是创建新进程的进程,子进程是由父进程创建的进程。这个关系记录在子进程的 PPID 中,便于父进程等待子进程、取得退出状态,以及让系统在父进程先结束时重新安排监护者。它是一种创建与管理关系,不表示父子进程共享同一个执行现场。
创建子进程:fork()
在类 UNIX 系统中,子进程通常由父进程通过 fork 系统调用建立;其声明为 pid_t fork(void)。调用成功后,父、子进程都从 fork() 后的下一条指令继续执行;区别来自同一个系统调用在两个执行流中的返回值:
| 执行流 | fork() 返回值 |
可以据此做什么 |
|---|---|---|
| 父进程 | 子进程的 PID,满足 | 记录子进程身份,继续自己的工作或随后调用 wait() / waitpid() 回收它。 |
| 子进程 | 0 |
执行子分支工作,必要时再调用 exec() 装入新程序。 |
| 创建失败 | -1(仅原父进程继续) |
没有产生子进程;父进程应检查错误并决定重试、报告或降级处理。 |
`fork()` 之后:父子进程、回收与两类易混状态
选择不同场景,观察父、子分支各自的返回值与状态,区分正常 `wait()` 回收、僵尸进程和孤儿进程。
`fork()` 后分成两条执行流父、子从同一处继续,但拥有独立 PID、PCB 与执行现场;它们之后的运行先后由调度器决定。
失败时 `fork()` 返回 -1,只有原父进程继续,不会产生子进程。创建:一次 fork,两个返回值两条执行流从 fork() 后的下一条指令继续;父进程得到子 PID,子进程得到 0,因此同一段代码能分出两条分支。
判断要点:返回值是区分父、子分支的可靠依据;父子谁先运行没有固定顺序。
可以把 fork() 的结果理解为“逻辑上复制一份地址空间,得到两个独立的执行流”。fork 调用后,父进程和子进程从 fork 调用后的下一条指令开始继续执行;子进程获得父进程的代码、数据和堆栈的副本,但两者地址空间独立。子进程拥有自己的 PID、PCB、寄存器现场和虚拟地址空间;两者之后修改各自变量不会直接改变对方。实际系统常用写时复制(Copy-on-Write)延迟物理页复制,因此“副本”不等于一开始就把每个物理页立即拷贝一遍。已打开文件描述符等资源也可能分别持有指向同一内核对象的引用,具体共享语义取决于资源类型。
父、子进程谁先运行并没有固定保证,调度器可以先运行任意一方;若需要确定顺序或交换数据,仍应使用进程间通信与同步机制。
fork()、exec() 与 wait() 要分开
除了 fork 系统调用外,还有 exec 系统调用;两者功能不同,需要能够区分。父进程还可以通过 wait 系统调用等待子进程执行结束,并获取子进程的退出状态,从而防止子进程长期成为僵尸进程。
- **
fork():创建。**产生一个新的子进程,父子进程继续运行原来的程序控制流;它带来并发的两个执行流。 exec():替换。在当前进程中装入新程序,替换代码、数据、堆栈等用户态映像;PID 通常保持不变,并不创建新进程。未设置 close-on-exec 的部分打开文件等资源可继续保留。- **
wait()/waitpid():回收。**父进程等待指定或任一子进程状态变化,并读取其退出状态。子进程已退出时可立即回收;尚未退出时,父进程通常会等待并进入阻塞态。
僵尸进程:子进程已经结束,但退出状态还没人回收
僵尸进程(Zombie Process)是指一个已经终止(完成其执行)的子进程,但其父进程尚未通过 wait 或 waitpid 系统调用回收其退出状态。它虽然不再运行,却仍在进程表中保留一个条目;用户态地址空间、CPU 执行现场和大部分资源已经释放,内核只保留最小的 PCB / PID 与退出状态,直到父进程回收或父进程终止后由新监护者处理。它不能再被调度;若长期无人回收,大量僵尸条目会耗尽可分配的 PID 或进程表项。
孤儿进程:父进程先结束,子进程仍可继续
孤儿进程(Orphan Process)是指父进程在子进程终止前退出,导致子进程失去原父进程的进程。它与僵尸进程不同,仍然在运行,只是失去了原来的父进程。系统会把它重新托付给 init、服务管理器或指定的子收割者(subreaper)等进程;因此它仍是一个可正常调度、可继续使用资源的普通进程,不是僵尸进程。它以后结束时,新监护者会负责回收其退出状态。
| 判断维度 | 僵尸进程(Zombie) | 孤儿进程(Orphan) |
|---|---|---|
| 子进程本身 | 已终止,不能再执行 | 仍在运行或尚未终止 |
| 原父进程 | 仍存在,但尚未执行回收 | 已经结束,无法再回收 |
| 内核接下来做什么 | 保留最小退出记录,等待 wait() / waitpid() 或由新监护者回收 |
修改其父子关系,由系统指定的新监护者接管 |
| 剩余资源 | 主要只剩进程表项、PID 与退出状态 | 仍按普通运行进程占用地址空间、打开文件等资源 |
| 一句辨析 | 子死父未收 | 父死子仍活 |
线程
线程是系统调度的基本单位。多核处理器可以将系统中的不同 线程 调度到不同的 CPU 逻辑核心上,所以在同一个时刻,系统中的 线程 可能会在不同的逻辑核心上并行运行。
每个 进程 启动的时候,都包含一个 线程(主线程),可以在主线程外创建更多的 线程。
如上图所示,一个 进程 中的所有 线程 都共享 虚拟地址空间 和系统 资源,但是每个 线程 都维护自己的 堆栈、寄存器和一些额外信息。
在一个 进程 中没有创建 线程 时,则该 进程 为 单线程进程,否则该 进程 中包含多个 线程。
单线程进程 只有一个线程使用进程的虚拟地址空间和系统资源,如文件描述符和内存。线程维护自己的栈和寄存器;进程内部不存在多个线程间的同步问题,也无法让该进程的多个执行流同时利用多个 CPU 核心。线程发生阻塞时,该进程没有其他线程可继续执行,因此适合简单任务,但并发性和响应性较差。
多线程进程 包含多个线程,共享虚拟地址空间和系统资源,如代码段和堆,便于高效通信。每个线程有独立的堆栈、寄存器和线程ID,需通过锁等机制同步以避免资源竞争。
进程的状态
进程状态 是指在操作系统中,一个 进程 在执行过程中的不同阶段。它反映了 进程 当前正在做什么,以及是否可以被 CPU 执行。
状态种类
不同的操作系统对 进程状态 的划分可能不同,但常见的包括以下几种:
- 创建状态(New):当 进程 被创建但还未分配资源或执行时,它处于 创建状态。
- 就绪状态(Ready):在 就绪状态 中,进程 已准备好执行,但由于操作系统调度算法或其他原因,尚未获得 CPU 时间片。
- 运行状态(Running):在 运行状态 中,进程 正在执行指令并占用 CPU。
- 阻塞状态(Blocked):当 进程 在等待某些事件发生时,如等待 I/O 操作完成或等待其他资源时,它会进入 阻塞状态。在 阻塞状态 下,进程 暂停执行,直到等待的事件发生。
- 终止状态(Terminated):当 进程 执行完毕或被操作系统终止时,它进入 终止状态。
状态转化
| 状态迁移 | 触发条件 | 资源与队列变化 |
|---|---|---|
| 就绪态 → 运行态 | 调度器在 CPU 可用时选中该进程 | 从就绪队列取出,获得 CPU |
| 运行态 → 就绪态 | 时间片耗尽、被更高优先级进程抢占,或主动让出 CPU | 保存运行现场并重新进入就绪队列;非阻塞系统调用返回本身不必引发该迁移 |
| 运行态 → 阻塞态 | 请求 I/O、等待锁/信号量/消息/条件、sleep()、wait(),或缺页后等待换入 |
放弃 CPU,进入与所等事件对应的阻塞队列;已持有资源是否释放取决于具体机制 |
| 阻塞态 → 就绪态 | I/O 完成、资源可用、消息/信号到达、条件成立或定时器到期 | 由内核唤醒并进入就绪队列,尚未直接获得 CPU |
**边界条件:**阻塞态不能直接迁移到运行态。唤醒只表示等待条件已经满足,进程必须先进入就绪队列,再由调度器选择。运行态进程也不会因为“等待 CPU”而阻塞;等待 CPU 的状态是就绪态。
常见迁移触发点的逐项辨析
- **就绪态 → 运行态:**调度器从就绪队列中选出进程,并分配 CPU;CPU 空闲时,调度器立即选择就绪进程运行。
- **运行态 → 就绪态:**进程用完时间片,被中断并放回就绪队列;更高优先级进程就绪时,当前进程可能被抢占;进程也可以主动让出 CPU,重新回到就绪队列。非阻塞系统调用返回后,进程仍可运行,但可能被调度器切换出去。
- **运行态 → 阻塞态:**进程等待 I/O 完成、请求的锁或信号量等资源暂不可用、等待信号/消息/条件满足、调用
sleep()等接口主动进入睡眠、调用wait()/waitpid()等待子进程结束,或访问未在内存中的页面而等待页面换入,都会使其放弃 CPU。 - **阻塞态 → 就绪态:**I/O 完成后,进程被唤醒并进入就绪队列;等待的资源可用、等待的信号/消息/条件已经出现、睡眠或定时等待到期、其他进程释放资源,或信号中断阻塞状态时,内核会让进程重新就绪。
一次事件如何推动进程状态迁移
从创建态开始选择调度、等待、唤醒或结束事件,观察进程必须经过就绪态才能重新获得 CPU。
选择一个事件,观察状态如何迁移。 当前状态:创建态。
进程内存空间
- 用户空间(User Space):包含 进程 执行的用户程序代码和数据。在 用户空间 中,进程可以执行各种任务,如运行应用程序、访问文件系统等,并通过系统调用请求文件等内核服务。用户空间对于应用程序可见;用户程序不能直接访问内核空间,内核则会在权限检查和地址校验后与用户空间交换数据。
- 代码区(Text Segment):也称为“可执行代码区”,存储了 进程 的可执行代码,包括程序的指令和只读数据。这个区域通常是只读的,因为程序的指令在运行时不应被修改。
- 数据区(Data Segment),数据区分为两个子区域:
- 初始化数据区(Initialized Data Segment):存储全局和静态变量以及初始化的数据。这些变量在程序运行前就已经分配了内存并初始化。
- 未初始化数据区(Uninitialized Data Segment),也称为 BSS(Block Started by Symbol)段,存储全局和静态变量,但这些变量没有显式的初始化值。操作系统会在程序启动时自动将这个区域初始化为零。
- 堆区(Heap):堆区 是 动态分配内存 的地方,用于存储程序运行时需要的变量和数据结构。在 堆 中分配的内存需要手动释放,以避免内存泄漏。
- 栈区(Stack):栈区 用于存储函数调用和局部变量。每个函数调用都会在栈上创建一个栈帧,栈帧包含了函数的参数、局部变量以及函数返回地址。栈 是一种后进先出(LIFO)的数据结构,它的大小通常有限,由操作系统或编程语言定义。
- 内存映射区域(Memory Mapped Region):这是一些操作系统或运行时库的扩展,用于存储动态链接库(DLL)和共享库的信息以及其他系统数据结构。
- 内核空间(Kernel Space):内核空间 包含了操作系统的核心代码和数据结构,如页表、调度程序和系统调用接口等。内核空间 具有更高的特权级别,可以执行特权指令并且访问系统的各种资源,内核空间 对于用户程序是不可见的。
函数调用时内存结构
在经典 32 位 x86 帧指针约定中,EBP(base pointer)指向函数栈(栈帧)的底部(高地址),函数执行过程中在栈帧中分配局部变量,栈帧由高地址向低地址增长,ESP(stack pointer)一直指向栈顶。具体保存内容和参数传递顺序由指令集与调用约定决定,不能把这一布局推广为所有体系结构的固定规则。
每个函数的栈帧中包含如下内容:
- 上一个函数的 EBP
- 该函数的局部变量
- 如果函数内有
call指令的话,还需要保存额外信息:- 下一个函数的参数:依次存储从第 n 个到第 1 个,从高地址到低地址
- 返回地址:当前 PC 指向的位置,即
call指令的下一条指令的地址
在函数的调用过程中,调用函数叫做 caller,被调用函数叫做 callee。当我们从 caller 中调用 callee 时,callee 的 EBP 指向的物理地址的上下存储单元分别包含 caller 的返回地址以及 caller 所在栈帧的 EBP,当我们在 callee 中执行 ret 指令时,计算机可以跳转到 caller 中 call 指令的下一条并开始执行,同时 caller 的栈帧也会被恢复。
补充
以下三个知识点可以放在一起学习:
进程间通信
偶尔在选择题考察,了解基本 IPC 方式,选择题看到能辨识即可。
进程间通信(Inter-Process Communication,IPC)是指在同一计算机系统中,两个或多个进程之间交换数据或信号的机制。常见方式主要包含共享内存、管道、消息队列、信号、套接字和信号量。408 中应先区分两件事:数据通过什么媒介传递(共享内存、管道、消息队列、套接字等),以及何时可以安全地读写(信号量、互斥锁、条件同步等)。信号量主要解决同步与互斥,并不是传递大块数据的载体。
IPC 方式怎么选:传数据、发通知还是跨主机通信
点击比较维度,横向查看管道、共享内存、消息队列、信号和套接字的传输单位、同步需求与适用范围。
当前比较:主要传递什么。
管道与 FIFO
管道是一种最基本的进程间通信方式,主要用于具有亲缘关系的进程之间的数据传递。数据先写入内核缓冲区,再按字节流被另一端读出。它分为无名管道与有名管道(FIFO):
- 无名管道通常由
fork()前后继承的文件描述符连接,适合父子或兄弟进程;一个管道通常是单向的,双向通信常用两条管道。 - **有名管道(FIFO)**在文件系统中有路径名,通信双方不必具有亲缘关系;双方按该特殊文件打开、读写。
管道保持写入顺序,但不保留消息边界,读进程必须自行解释字节流;缓冲区为空时读操作可能等待,所有写端关闭后读端才会读到 EOF。这些特性使它很适合命令的串联,却不适合需要随机访问或复杂消息分类的场景。
下面在 Linux shell 中使用 | 创建管道:cat 进程的标准输出(stdout)成为 grep 进程的标准输入(stdin)。
cat file.txt | grep word
共享内存
共享内存允许多个进程把同一段物理内存映射到各自的虚拟地址空间,从而直接读写同一批数据。在同机大块数据传递的常见比较中,它通常是效率最高的一种进程间通信方式;建立映射后,数据不必在每次通信时经由内核复制。
由于不需要内核频繁介入,通信效率极高,非常适合大规模数据的快速传输。它只解决“看见同一份数据”,不自动解决“谁先写、谁何时读”。多个进程同时访问时,仍必须用信号量、互斥锁等同步机制保护临界区,以确保数据一致性;否则会出现竞争条件、读到半写入数据等错误。
消息队列
消息队列是一种基于内核的数据结构,允许多个进程以消息为单位进行通信,具有良好的结构化和独立性。进程通过系统调用将消息发送到队列中或从中接收有边界的消息,可实现同步与异步的通信模式。发送方与接收方不必直接共享地址空间,消息可按类型或优先级组织,因此比字节流管道更适合结构化、分类的进程间协作。
相比管道,消息队列的通信更加灵活,支持优先级管理,适用于需要有序、分类传输数据的场景;但每次收发都需要内核参与,性能开销相对共享内存较大。
信号:轻量的异步通知
信号主要传递“发生了某件事”的控制信息,例如终止、定时器到期或子进程状态变化。进程可采取默认动作、忽略信号或安装处理函数;它适合通知和打断,不适合传递大块、有复杂结构的数据。复习时要把它与信号量区分:信号面向事件通知,信号量面向同步与资源计数。
套接字:本机或跨网络的端点通信
套接字把通信抽象为两个端点之间的数据交换。它既可用于同一台主机上的本地进程,也可用于网络中不同主机的进程;TCP 常提供有序字节流,UDP 常提供数据报。它的适用范围最广,但协议栈和数据复制通常带来比共享内存更高的开销。
选择方式时的快速判断
- 需要父子进程串联命令、按顺序传输字节流:优先想到无名管道;无亲缘关系但希望按路径连接:想到 FIFO。
- 需要高效交换大量数据:想到共享内存,并立刻补上“还要用同步机制”。
- 需要由内核保存带边界、可分类的消息:想到消息队列。
- 只需异步通知某个事件:想到信号;需要与远程主机通信:想到套接字。
用户级线程和内核级线程
偶尔在选择题考察,了解几种线程模型和映射关系,选择题看到能辨识即可。
- 用户级线程(ULT,User Level Thread)
- 用户级线程 是由应用程序通过线程库来实现的,操作系统内核并不直接感知到这些 线程 的存在。
- 线程的创建、调度和管理都由应用程序在 用户空间 完成。
- 内核级线程(KLT,Kernel Level Thread)
- 内核级线程 是由操作系统内核直接支持的线程。
- 线程的创建、调度和管理都由 操作系统内核 完成。
线程模型
操作系统中的 线程实现方式 称为 线程模型,不同的 线程模型 在 用户级线程(ULT)和 内核级线程(KLT)的设计上各有不同。
系统中的 线程模型 可以分为如下三种:
- 纯用户态:所有的线程操作都在用户空间中进行,内核对线程的存在一无所知。
- 纯系统态:线程由操作系统内核直接支持和管理,内核负责线程的创建、调度和管理。
- 混合方案:应用程序可以在用户空间管理多个用户级线程,这些线程映射到较少数目的内核级线程。
用户级线程与内核级线程的映射关系
按照用户级线程(ULT)与内核级线程(KLT)的数量关系,常见模型分为如下三种。它们不能全部笼统归入“混合方案”:多对一更接近纯用户级管理,一对一需要内核逐个支持,多对多才是典型的组合模型。
- 一对一:每一个用户级线程都对应一个内核级线程。一个线程阻塞不会使同进程其他线程全部阻塞,并可利用多核并行,但创建和切换的内核开销较大。
- 多对一:多个用户级线程映射到同一个内核级线程上。线程管理开销小,但其中一个线程执行阻塞系统调用时可能使整个映射实体阻塞,而且同一时刻不能让这些用户级线程分布到多个 CPU 核心执行。
- 多对多:多个用户级线程复用多个内核级线程。用户线程库负责用户级调度,内核调度其可见的内核级线程,在并行能力与管理开销之间折中。