1. 实验想说的第一件事:状态不是给你看的,是给调度器用的
很多同学拿到“观察Linux进程状态”这个实验,第一反应是敲几条 ps 命令,把输出截图往报告里一贴就算完事。如果只是这样,那这个实验的意义就丢掉了一大半。
Linux里的进程状态,本质上不是设计出来让你看着“这进程活着还是死着”的,它是操作系统内核调度器赖以决策的一套标记体系。调度器每时每刻都要回答一个问题:当前CPU空出来了,下一个该让谁上?这个“谁”,就是靠进程状态来筛选的。处于R状态的进程排进运行队列,等着被调度;处于S状态的进程说明它在等待某件事完成,暂时不参与调度;处于Z状态的进程已经死了,只是尸体还没被收走。你可以把状态理解成一张“停车位标签”——每辆车(进程)停在不同区域,哪些车可以挪动、哪些车在等人、哪些车已经报废但还没拖走,调度器一眼扫过去就知道该处理谁。
这个实验放在操作系统课程系列实验的第二篇,意图也很明显。上一次实验大概率是让你熟悉Linux基本操作和常用命令,而这次开始真正触碰操作系统的核心机制。所以观察进程状态,不只是学 ps -aux 这个命令本身,而是要通过状态变化,反过来理解进程的生命周期、调度模型、以及资源等待这些看不见的概念。
准备这个实验前,建议先把教材里的进程三态模型/五态模型复习一遍。就是那张经典的图:新建态、就绪态、运行态、阻塞态、退出态。实验课上你会发现的第一个“认知冲突”是——Linux里 ps 命令输出的状态码跟理论模型不是一一对应的,R不完全等于运行态,S不等于就绪态,还多出D、T、Z这些你看教材时没怎么当回事的状态。这个差异正是本次实验最有价值的部分:真实操作系统里的状态划分,要比课堂教学模型更细致、更复杂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ps命令和STAT列:读懂那串看似简单的字母
实验的基础操作是使用 ps 查看进程状态。这条命令每个人都会敲,但STAT列那串字母,很多人从来没认真读过。
2.1 查看进程状态的基本姿势
先推荐一套实验中最常用的查看方式,直接执行:
bash复制ps -elf
如果你习惯用 BSD风格 的写法,也可以:
bash复制ps aux
两套命令输出格式有差异,但都能看到STAT列(ps -elf 里叫 S 列,对应标题为 S;在 ps aux 里叫 STAT 列)。为了实验报告里说明起来更严谨,建议统一用 ps -elf,因为它的输出带有PPID(父进程PID)、PRI(优先级)、NI(nice值)这些字段,方便后面做联动分析。
做一个实验里最常用的组合:
bash复制# 实时刷新查看,效果更接近观察实验
watch -n 1 'ps -elf --sort=pid'
--sort=pid 按PID排序,方便你在一堆输出里找到自己启动的进程。watch -n 1 每秒刷新一次,这样进程状态发生变化时你能实时看到,而不是敲一次命令截一张静态图。
2.2 STAT列每一位都代表什么
STAT列大约由2到4个字符组成。第一个字符是主状态,是实验记录中最重要的信息;后面的字符是附加标志位,一般辅助判断。
主状态字段中,最常出现在你实验截图里的是这么几个:
| 状态码 | 含义 | 触发场景 |
|---|---|---|
| R | 可运行(running/runnable) | 进程正在占用CPU运行,或者处于运行队列中等待调度 |
| S | 可中断睡眠(sleeping) | 进程在等待某个条件满足,比如等待IO、等待定时器、等待用户输入 |
| D | 不可中断睡眠(uninterruptible sleep) | 进程在内核态等待IO完成,期间不响应普通信号 |
| T | 停止(stopped) | 进程被暂停执行,通常是被Ctrl+Z、SIGSTOP信号暂停 |
| t | 跟踪停止 | 进程被调试器(如gdb)暂停 |
| Z | 僵尸进程(zombie) | 子进程已终止,但父进程尚未调用wait()获取其退出状态 |
| I | 空闲(idle) | 内核线程,通常在较新的内核中出现,类似S但不会积累负载 |
实验中最容易犯糊涂的是R和S的区分。很多同学看到输出里大部分进程显示S,就以为出问题了,觉得“怎么只有少数进程是R,Linux是不是卡了”。恰恰相反,一台健康的Linux机器上,绝大多数用户进程长时间处于S状态是正常的。因为它们大部分时间都在等待——等网络数据、等磁盘IO、等用户敲键盘。真正在CPU上跑的进程只是极少数。如果一台普通服务器上大量进程处于R状态,那才说明CPU已经过载,运行队列排长队了。
2.3 附加标志位里藏着的额外信息
STAT列在R/S/D/Z这些主状态后还可能跟着一个小写字母,常见的有:
+表示该进程位于前台进程组中。如果你在终端直接启动一个程序,状态列会显示S+;如果放后台跑,就是S(没有加号)。<表示该进程拥有高优先级。N表示该进程拥有低优先级(nice值大于0)。l表示该进程是多线程的。s表示该进程是会话首进程(session leader),比如你开的Shell本身,状态通常是Ss。
比如你写了一个死循环程序放前台跑,ps 输出大概率是 R+。你把这个程序用 ./test & 丢到后台,再查看它,状态会变成 R(加号消失)。加号的有无反映的是“前后台”维度的信息,跟主状态是独立的。
建议做实验时,针对同一个进程在不同场景下各截一次STAT列,包括:正常运行(S)、跑CPU密集程序(R)、按下Ctrl+Z(T)、kill -19暂停(T)、kill -18恢复(R/S)。这一套截图基本上就能撑起实验报告中“状态变化观察”的核心部分了。
3. 用top观察动态变化:状态不是凝固的,是实时流动的
ps 是瞬间快照,适合观察某一时刻进程处于什么状态。但进程状态本质上是个动态过程,如果你只在某个时间点截一张图,会漏掉很多关键变化。这个时候要借助 top 命令。
3.1 top里看状态的正确读法
在终端输入 top,顶部几行是系统总体负载信息,往下是进程列表。列表里有一个 S 列,就是进程状态列。与 ps 不同的是,top 默认每隔约3秒自动刷新一次(可以通过 top -d 1 改成1秒刷新),你能直观看到状态列在跳动。
top 输出里还有一个容易被忽略的细节——顶部汇总行里有统计数据:
bash复制Tasks: 214 total, 1 running, 213 sleeping, 0 stopped, 0 zombie
这行会统计当前系统里各种状态进程的总数。如果这个数字比例长期处于异常状态,比如running长期接近CPU核数而且负载很高,或者zombie长期不为0,说明系统里有进程“卡住”了。
做实验时建议开两个终端窗口,一个跑 top,另一个跑你要观察的测试程序,这样状态变化一目了然。
3.2 CPU密集程序的状态:从S跳到R的瞬间
我自己带实验时最喜欢让学生做的事,是先休眠一下看看自己程序什么状态。先写一个最简单的“空转”程序,比如 C 语言:
c复制#include <stdio.h>
int main() {
while (1) {
// 空循环,持续占CPU
}
return 0;
}
编译后放到后台运行:
bash复制gcc -o spin spin.c
./spin &
此时你用 top 看它的状态,会看到 R,CPU占用率接近100%。这很容易理解。但接下来有个微妙的问题:你在Shell里敲 ps -elf 时会发现,这个spin进程在某个瞬间可能显示成 R,也可能显示成 S——因为你的 ps 命令执行时,它可能恰好被调度器切出去了,处于可运行队列(这时候理论上应该也是R,但如果你用的是 ps aux,某些内核版本中R状态的判定有所差异)。
真正的判断标准是CPU时间。如果该进程的CPU时间在持续增长,说明它确实在被调度执行;如果CPU时间不变但状态是R,说明它在运行队列里排队等CPU。要观察细微差别,可以看:
bash复制# 每秒输出一次进程CPU时间
pidstat -p [PID] 1
pidstat 需要安装 sysstat 包。有些实验环境不带这个工具,你也可以从 top 或 ps -o time= 里看CPU TIME字段。
3.3 睡眠状态程序:等待是常态
再写一个一直在睡觉的程序,这一步要在实验报告里体现“阻塞/睡眠”状态:
c复制#include <unistd.h>
int main() {
while (1) {
sleep(100); // 睡眠100秒
}
return 0;
}
运行起来后查看它的状态,你会看到S。进程的大部分时间都在等待定时器到期,不参与CPU调度,所以CPU占用几乎为0。S状态的进程可以响应信号,比如你用 kill 命令给它发信号,内核会唤醒它处理。这就是“可中断睡眠”——它虽然睡着了,但能被叫醒。
3.4 top里R与S的直观差异
拿两个程序放在一起对比:
| 测试程序 | 状态 | CPU% | 说明 |
|---|---|---|---|
| 死循环程序 | R | 接近100 | 持续占用CPU,一直在运行 |
| sleep程序 | S | 0 | 主动让出CPU,等定时器唤醒 |
这个对照实验强烈建议写进报告。它用最直观的数字说明“进程状态决定进程是否能占用CPU资源”。
4. 动手设计实验:人为制造T状态和Z状态,这是观察的高潮
只看现成进程的状态,实验深度不够。接下来这一步是整个实验里“动手”含量最高、理解价值最大的环节:你自己去制造状态异常的进程。
4.1 在另一个终端里观察暂停与恢复
最简单的暂停观察不需要写代码。随便运行一个耗时长的命令,比如:
bash复制sleep 300
在另一个终端找到它的PID:
bash复制ps -elf | grep sleep
然后回到运行这个命令的终端,按下 Ctrl+Z。这个操作会向前台进程发送 SIGTSTP 信号,进程被暂停。此时去另一个终端查看,你会发现它的状态变成了 T。
继续做恢复实验:
bash复制# 向该PID发送SIGCONT信号
kill -18 [PID]
回到查看终端,状态会从T变成S(睡眠中,因为sleep还在等300秒倒计时)。再发一次暂停:
bash复制# 向该PID发送SIGSTOP信号
kill -19 [PID]
进程又回到T状态。这里注意 Ctrl+Z 发的是SIGTSTP,kill -19 发的是SIGSTOP,两者效果都是暂停进程,但SIGTSTP可以被进程捕获并忽略,SIGSTOP则不能,普通进程无法拒绝暂停。
这个实验做完,你对T状态的理解就不只是停留在“暂停”这个词上了。你能感知到暂停与恢复背后的信号机制:SIGSTOP让进程冻结,SIGCONT让进程继续。实验报告中记录状态变化的同时,最好把所用的信号编号和名称一并写上。
4.2 亲手制造一个僵尸进程
Z状态是很多同学只在书上见过、没见过真身的。制造僵尸进程其实不难,而且实验做完你会对“父进程必须回收子进程”这件事有生理级别的记忆。
写一段C程序:
c复制#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:立刻退出
printf("Child PID=%d exiting...\n", getpid());
_exit(0);
} else if (pid > 0) {
// 父进程:睡眠30秒,故意不调用wait()
printf("Parent PID=%d sleeping...\n", getpid());
sleep(30);
printf("Parent wakes up and exits.\n");
}
return 0;
}
这段代码的关键在于:子进程退出后,父进程没有调用 wait() 或 waitpid() 来回收子进程退出状态。子进程的退出信息会保留在进程表中,直到父进程读取,或者父进程自己也退出后由init进程(PID 1)收养并清理。
编译运行:
bash复制gcc -o zombie_test zombie_test.c
./zombie_test
等程序输出Child exiting后,马上在另一个终端执行:
bash复制ps -elf | grep zombie_test
此时你会看到两条记录:父进程状态是S,子进程状态是Z。子进程的名字显示为 <defunct> 或 [zombie_test] <defunct>,STAT列就是Z。
这里要提醒一个容易踩的坑:如果在第一个终端没有及时查看,30秒后父进程自己也退出了,僵尸进程就会被init进程收走,然后消失。所以查看之前最好把父进程的睡眠时间调长一点,比如改成 sleep(300),留足观察窗口。
当父进程退出后,整个系统的僵尸进程会逐渐消失,其他终端里看不到。这是“孤儿进程”和“僵尸进程”的一个典型区别:孤儿是被父进程抛弃、但被init收编后继续正常运行的进程;僵尸是已经终止、但退出状态还没被父进程取走的进程。
4.3 体验一下不可中断睡眠D
D状态是几种状态里最有“脾性”的。理论课上讲到D状态(不可中断睡眠)时,一般解释为:进程在内核态执行IO操作,此时不能被信号打断,直到IO完成。但这个状态比较难手工制造,因为普通用户的程序很少会直接触发不可中断的内核IO路径。
如果实验环境里有虚拟机管理权限,可以尝试加载一个阻塞在IO上的内核模块;大多数学生实验环境下,不需要刻意去制造D状态。只要知道,如果你在实验中看到某个进程长期处于D状态,并且IO负载异常高,那基本可以判断它在等待一次缓慢的磁盘或网络IO。这种状态通常无法通过 kill 杀掉,kill -9 也拿它没办法,只能等IO超时或者系统重启。
这也是很多Linux运维新手困惑的点——为什么有时候进程杀不掉?先看D状态。D状态的进程不是“拒绝被杀”,而是在内核态执行不可中断操作时,还没到处理信号的那一步。类比一下就是:一个人在做开颅手术,怎么喊他都没反应,只能等手术做完,不能直接拖着人走。
5. 深入一层:通过/proc文件系统观察进程的运行痕迹
对学操作系统的人来说,ps 和 top 只是外壳,真正有价值的是理解这些工具的数据来源。Linux系统里每个进程都有一个对应的目录,位于 /proc/[PID]/,这可不是普通的普通文件目录,它是内核在内存里虚拟出来的文件系统,实时反映进程状态。
5.1 /proc/[PID]/stat与status
进入你正在运行sleep的进程目录:
bash复制ls -l /proc/[PID]/
这里能看到一堆文件和链接。其中关键的有:
/proc/[PID]/stat:这是ps命令读取的核心来源之一,里面全是数字信息,第3个字段就是进程状态。/proc/[PID]/status:人类可读的进程信息。/proc/[PID]/cmdline:这个进程的启动命令行。
直接用 cat 查看 /proc/[PID]/status:
bash复制cat /proc/[PID]/status | grep -E "State|Name|Pid"
输出中 State: 行会直接显示状态字符,例如:
text复制State: S (sleeping)
比ps输出的字母更直白,后面的括号里写着完整的英文状态名。
这个层面能让实验报告多一层硬核色彩。一般学生只会贴ps截图,如果你能从 /proc 文件系统层面解释“状态到底是存在哪里的”,报告的深度立刻就拉开了。因为这意味着你理解了:进程状态并不是凭空捏造的抽象概念,而是内核在管理进程时维护的具体数据结构,并且通过procfs暴露到了用户空间。
5.2 通过状态字段体会PCB的概念
操作系统的理论课一定会讲PCB(进程控制块)。教材上说PCB是内核中记录进程信息的数据结构,包含进程状态、PID、CPU上下文、内存分配信息等。你以前可能觉得这是个抽象概念。
实际到了Linux里,进程的task_struct结构体就是PCB的具体实现。而你通过 /proc/[PID]/ 看到的每一个文件,恰好是task_struct中某一部分信息的投影。
可以做个对应:
- PID/PID号:对应task_struct里的pid字段;
- status文件里的State:对应task_struct里的__state字段;
- status文件里的VmRSS:对应task_struct里的mm_struct内存信息;
- stat文件里的starttime:对应task_struct里的start_time。
做实验时可以临时充当一次“内核窗口”,从 ps 输出追到 /proc 中的具体数据来源,再对照教材PCB的结构,理解“内核给每个进程维护一张信息表”是怎么回事。
如果你愿意再往前走一步,还可以用一个小脚本,循环读取某个进程的state变化:
bash复制while true; do
awk '{print $3}' /proc/[PID]/stat
sleep 0.5
done
这里 [PID] 换成运行sleep程序的进程,脚本会每0.5秒输出一次状态字符。运行几秒后你会看到一直是S。如果换成一个持续被SIGSTOP暂停的进程,你会稳定看到T。这些都是状态变化的实时证据。
6. 实验报告中能拉开差距的几个细节
最后的这部分算是一些过来人的经验。实验本身不复杂,但同样的实验,不同人做完留下的收获差距可以非常大。以下几个点是批改报告和答疑时最常被问到、同时也最能体现“你真的做了实验”的地方。
6.1 分清“就绪态”和“运行态”在Linux里如何表达
课堂教学里的进程三态模型包含就绪态和运行态。但在Linux的进程状态标记里,这两个状态统一用字母R表示(TASK_RUNNING)。也就是说,一个正在排队等CPU的进程和正在CPU上执行的进程,表面状态都是R,本质区别在于它是否获得了CPU使用权。
要判断一个R状态进程到底是正在运行还是正在等待调度,可以用 top 查看它的 %CPU 或者 TIME+。如果CPU时间不停增长,说明它确实在被调度执行。如果CPU时间停止在一个值附近,但状态还是R,说明它正排在运行队列里等待其他进程释放CPU。
这个细节在很多教材里都没明确写,但考试和面试经常考。Linux为什么把就绪和运行合并成一个状态?因为在这个时间片轮转的调度模型里,就绪和运行之间切换太频繁了,单独拆开没有实际管理意义,用一个R状态加一个运行队列就够了。对比Windows或教材里的简化模型,你会意识到Linux为了高效做了很多工程上的取舍。
6.2 观察状态切换的全过程,而不是单个状态
实验报告只写“某进程处于S状态”价值有限。建议按时间线记录一次完整的状态切换过程。比如:
- 启动sleep进程 -> 状态为S,等待睡眠结束;
- 执行kill -19 -> 状态变为T,进程暂停;
- 执行kill -18 -> 状态还原为S,继续睡眠;
- 睡眠到期 -> 进程退出。
把这几个时间点连同执行的命令、输出的STAT列统一记录下来,形成一条状态变化时间线。这才是“观察状态”而不是“看一眼状态”。
这个习惯对你后续理解进程同步、信号处理、僵尸进程等主题都有帮助。状态切换意味着进程在内核里发生了某种上下文切换或资源状态变化,而操作系统课程的核心——并发、同步、锁——本质上都建立在进程状态的不断变化之上。
6.3 关于虚拟机里做实验的两个提醒
如果你是使用虚拟机完成的实验,有一个情况要特别注意:在VMware或VirtualBox里运行Linux时,偶尔会在 top 中看到CPU占用率显示成100%或异常波动,而进程状态看起来没问题。这并不完全是Linux的错误,多数情况是虚拟化环境下时间同步和CPU调度机制与物理机不同导致的,不能作为判断进程状态的充分根据。
另外,实验过程中如果按Ctrl+Z暂停了一个Shell中的命令,而你又想继续在该终端做其他操作,记得用 fg 命令把暂停的任务恢复到前台,或 bg 把它放到后台继续运行,否则会发现终端输入没有反应。很多同学做完暂停实验后以为终端卡死了,实际上只是有一个被暂停的前台任务还占着终端,按一下 fg 就回来了。
6.4 学会在实验报告里引用/proc数据
这门课以后如果做更深入的实验,你大概率会与内核模块、驱动打交道。那时候你会发现 /proc 目录里除了进程信息还遍布着各种内核状态的导出入口,比如 /proc/cpuinfo、/proc/meminfo、/proc/interrupts。而现在这个实验正是你第一次以“读状态”的方式认识procfs的绝佳机会。报告上操作记录可以从 ps 命令的输出和 cat /proc/[PID]/status 的多个状态截图来展示。既能看到用户态工具如何呈现状态,又能看到内核实际导出的状态数据,两者互相验证的写法比单纯贴一张ps打印结果成熟得多。
我实际带实验时经常说一句话:这个实验所有步骤做完,你至少应该能回答三个问题——一个进程为什么进入休眠状态?为什么杀不掉Z状态的进程?为什么D状态的进程连kill -9也没反应?如果都能不查资料说出个一二三,实验就算真正过关了。
