1. 进程状态理论概述
在操作系统理论中,进程状态描述了进程在其生命周期中的不同阶段。在 Linux 环境下,进程状态主要有以下五种。需要注意的是,不同语境或理论体系下描述可能略有区别,本课程基于 Linux 内核视角。
1.1 五种基本状态
- 就绪态 (Ready)
- 进程已创建,具备运行条件,但尚未获得 CPU 资源。
- 原因:CPU 资源有限,进程数量远多于 CPU 核心数,需排队等待调度。
- 转换:当调度器分配 CPU 时间片后,转为运行态。
- 运行态 (Running)
- 进程正在 CPU 上执行指令。
- 转换:时间片用完或被更高优先级进程抢占时,转回就绪态。
- 睡眠态/阻塞态 (Sleep/Blocked)
- 进程因等待某些事件(如 I/O 资源、用户输入)而主动放弃 CPU。
- 也称为 阻塞态 或 等待态。
- 转换:当等待的资源到位(如键盘输入完成),转回就绪态。
- 分类:
- 可中断睡眠 (Interruptible Sleep):浅度睡眠。可以被信号(Signal)打断唤醒。
- 不可中断睡眠 (Uninterruptible Sleep):深度睡眠。通常涉及硬件交互,不能被信号打断,必须等待硬件响应。
- 停止态/暂停态 (Stop/Stopped)
- 进程被强制暂停执行,并非永久终止。
- 英文对应
Stopped,有时直译为“停止”,但实际含义是“暂停”。 - 原因:通常由信号引起(如用户按下
Ctrl+Z),或调试器控制。 - 区别:与睡眠态不同,睡眠是进程主动等待资源,停止态往往是被动被外部信号挂起。即使资源到位,若未收到继续信号,进程也不会运行。
- 转换:收到继续信号(如
fg,bg)后,转回就绪态。
- 僵尸态 (Zombie)
- 进程已终止,但其进程描述符(PCB)仍保留在内核进程表中。
- 原因:子进程结束,但父进程尚未读取其退出状态。
- 特点:无法再次运行,必须被父进程回收资源后才能彻底消失。
1.2 状态转换逻辑
- 创建:使用
fork系统调用创建进程,初始进入就绪态。 - 调度:就绪态 \leftrightarrow 运行态(取决于 CPU 调度)。
- 阻塞:运行态 \rightarrow 睡眠态(调用
sleep,scanf等等待资源)。 - 暂停:运行态 \rightarrow 停止态(收到
SIGSTOP或Ctrl+Z)。 - 终止:运行态 \rightarrow 僵尸态(进程代码执行完毕或调用
exit,等待父进程wait)。
2. Linux 进程状态实验环境与工具
实验主要在 Linux 终端下进行,涉及编译、运行及监控工具。
2.1 常用命令
- gcc: 编译器。
- 示例:
gcc test.c -o test(编译并指定输出文件名)。 - 示例:
gcc -Wall test.c(显示所有警告)。
- 示例:
- top: 实时监控系统进程状态。
- 用法:
top -p <PID>(监控指定进程)。 - 状态列标识:
R: Running (运行态)S: Sleep (睡眠态)T: Stop (停止态)Z: Zombie (僵尸态)I: Idle (空闲)
- 用法:
- ps: 进程快照。
- 用法:
ps -ef | grep <process_name>。
- 用法:
- kill: 发送信号。
- 用法:
kill -9 <PID>(发送 SIGKILL 信号强制终止)。
- 用法:
2.2 前台与后台进程控制
- 前台运行: 进程占用终端,可接收键盘信号。
- 后台运行: 进程在后台执行,通常使用
&启动或bg命令。 - 快捷键操作:
Ctrl+C: 发送SIGINT信号,终止前台进程。Ctrl+Z: 发送SIGTSTP信号,暂停前台进程(进入停止态)。fg: 将后台或暂停的作业调到前台继续运行。bg: 将暂停的作业调到后台继续运行。
- 注意:
Ctrl+C和Ctrl+Z仅对前台进程有效。若进程在后台运行,终端键盘信号无法直接作用于该进程。
3. 进程状态实验步骤详解
3.1 实验一:观察运行态 (Running)
-
代码准备 (
test.c):- 逻辑:无限循环打印字符
1。 - 关键点:使用
fflush(stdout)刷新缓冲区,确保字符立即显示。 - 自定义延时函数:通过空循环占用 CPU 时间,模拟繁忙状态(区别于
sleep让出 CPU)。
while(1) { printf("1"); fflush(stdout); delay(); // 自定义 CPU busy 循环 } - 逻辑:无限循环打印字符
-
运行与监控:
- 编译:
gcc test.c -o test - 运行:
./test &(后台运行,获取 PID)。 - 监控:新开终端,
top -p <PID>。 - 现象: 状态显示为
R,表示进程正在占用 CPU 运行。 - 说明: 就绪态很难捕捉,因为 CPU 切换极快,通常看到的都是运行态。
- 编译:
3.2 实验二:观察停止态 (Stopped)
- 操作步骤:
- 将进程调至前台:
fg。 - 发送暂停信号:按下
Ctrl+Z。 - 现象: 终端提示
Stopped,进程不再输出字符。 - 监控:
top -p <PID>显示状态为T。
- 将进程调至前台:
- 恢复与终止:
- 恢复运行:
bg(后台) 或fg(前台)。 - 终止进程:前台状态下按
Ctrl+C。 - 现象: 进程消失,
top中不再显示。
- 恢复运行:
3.3 实验三:观察睡眠态 (Sleep)
-
场景 A: 时间睡眠
- 命令:
sleep 100。 - 监控:
top显示状态为S。 - 原理:进程主动让出 CPU,等待定时器中断。
- 命令:
-
场景 B: 资源等待睡眠
- 代码:使用
scanf等待用户输入。
scanf("%d", &val); // 等待键盘输入- 监控:在输入前,
top显示状态为S。 - 原理:进程阻塞在 I/O 操作上,属于可中断睡眠。
- 代码:使用
4. 僵尸进程 (Zombie Process)
4.1 产生原因
- 场景: 父进程创建子进程 (
fork),子进程先于父进程终止 (exit)。 - 机制: 子进程终止后,内核需要保留其进程表项(PCB),以便父进程获取子进程的退出状态(返回值、终止信号等)。
- 状态: 此时子进程进入 僵尸态 (Z)。
- 代码示例:
pid_t cpid = fork(); if (cpid == 0) { // 子进程 exit(0); // 子进程立即退出 } else { // 父进程 while(1); // 父进程死循环,不调用 wait } - 结果:
ps或top中可见子进程状态为Z。
4.2 危害
- 资源占用: 僵尸进程不占用内存代码段,但占用内核进程表项。
- 风险: 若大量进程变为僵尸态,会导致进程表满,系统无法创建新进程,甚至崩溃。
- 避免方法: 父进程必须及时回收子进程资源。
4.3 解决方案
- 父进程调用
wait或waitpid系统调用。 - 作用:阻塞父进程直到子进程结束,并读取子进程退出状态,随后内核彻底释放子进程资源。
5. wait 系统调用详解
5.1 函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 参数:
status是一个整型指针,用于存储子进程的退出状态信息。若不需要可传NULL。 - 返回值:
- 成功:返回终止的子进程 PID。
- 失败:返回
-1(例如没有子进程)。
- 行为: 阻塞调用。若子进程未结束,父进程会进入睡眠态等待。
5.2 退出状态分析宏
通过 status 变量分析子进程终止原因,需使用以下宏:
- 正常退出判断:
WIFEXITED(status): 若子进程正常终止(调用exit或返回),返回真。WEXITSTATUS(status): 若WIFEXITED为真,获取子进程的返回值(exit code)。
- 信号终止判断:
WIFSIGNALED(status): 若子进程被信号终止,返回真。WTERMSIG(status): 若WIFSIGNALED为真,获取终止该进程的信号编号。
5.3 实验示例
- 正常退出: 子进程
exit(10)。父进程wait后,WIFEXITED为真,WEXITSTATUS为 10。 - 信号终止: 子进程运行中,父进程或其他进程发送
kill -9(SIGKILL)。父进程wait后,WIFSIGNALED为真,WTERMSIG为 9。 - 错误处理: 若父进程循环调用
wait但子进程已结束,wait返回-1,错误码为ECHILD(No child processes)。
5.4 多子进程处理
- 若父进程创建多个子进程,需循环调用
wait直到返回-1。 - 示例逻辑:
while(1) { pid_t pid = wait(&status); if (pid < 0) break; // 无子进程可等待 // 处理 status }
6. waitpid 系统调用详解
6.1 函数原型
pid_t waitpid(pid_t pid, int *status, int options);
- 相比
wait,waitpid提供了更灵活的控制。
6.2 参数详解
- pid (等待哪个进程):
-1: 等待任意子进程(等效于wait)。>0: 等待指定 PID 的子进程。0: 等待同一进程组的任意子进程。<-1: 等待指定进程组的任意子进程。
- status: 同
wait,存储退出状态。 - options (选项):
0: 阻塞等待(默认行为)。WUNTRACED: 若子进程被停止(而非终止),也返回。用于调试器跟踪。WNOHANG: 非阻塞模式。若子进程未结束,立即返回 0,而不阻塞父进程。
6.3 高级用法示例
- 等待指定子进程:
- 设置
pid为特定子进程 ID,仅回收该进程资源。
- 设置
- 非阻塞轮询 (WNOHANG):
- 场景:父进程需同时处理其他任务,不能一直阻塞在
wait上。 - 逻辑:
while(1) { pid_t pid = waitpid(-1, &status, WNOHANG); if (pid > 0) { // 有子进程结束,处理回收 } else if (pid == 0) { // 无子进程结束,继续做其他事 } else { // 出错或无子进程 break; } // 执行其他任务 }
- 场景:父进程需同时处理其他任务,不能一直阻塞在
- 跟踪停止状态 (WUNTRACED):
- 配合
WUNTRACED,当子进程被信号停止时,waitpid也会返回,父进程可获取子进程停止的状态信息。
- 配合
6.4 注意事项
- 头文件: 需包含
<sys/types.h>,<sys/wait.h>,<unistd.h>,<stdio.h>,<stdlib.h>。 - 返回值检查: 始终检查
waitpid返回值,区分成功 PID、0 (非阻塞无结果) 和 -1 (错误)。 - 资源回收: 无论使用
wait还是waitpid,核心目的都是避免僵尸进程,确保内核资源释放。