

.
个人主页: 晓风飞
专栏: 数据结构|Linux|C语言
路漫漫其修远兮,吾将上下而求索
文章目录
- 进程
-
- [Linux 内核 7 种进程状态](#Linux 内核 7 种进程状态)
- R状态
- sleep休眠状态
- 补充知识:前后台进程与+号的含义
- T状态
- t状态(追踪暂停):gdb调试时的断点
- D状态(深度睡眠):不可中断睡眠
- Z状态(僵尸)与X状态(死亡)
-
- 为什么需要僵尸状态?一个故事
- 类比到进程:退出信息保存在PCB中
- [`return` 值与退出信息](#
return值与退出信息) - 重新认识内存泄漏
- 僵尸进程的危害
- 演示僵尸进程
- 孤儿进程
进程
Linux 内核 7 种进程状态
Linux 里进程状态用字母表示:
R:Running / Runnable 运行 / 就绪
S:Sleeping 休眠(可中断)
D:Disk sleep 不可中断休眠
T:Stopped 停止
t:Tracing stop 调试暂停
Z:Zombie 僵尸
X:Dead 死亡
重点记住:
R = 运行队列中的进程(正在运行 or 就绪等待)
S/D/T = 阻塞状态
挂起 = 内核内部操作,不对外显示
R状态
经典面试坑:为什么死循环打印,状态却是 S?
写一个死循环打印:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("我是一个进程,我正在运行:%d?\n",getpid());
}
return 0;
}
运行gcc fs.c,./a.out
你用 ps 查看,发现状态是 S(休眠),不是 R!

原因:
printf 是IO 操作(显示器 / 网络终端)
IO 速度远慢于 CPU
进程大部分时间都在等待外设就绪
所以进程会进入休眠等待,状态显示 S
如果你把 printf 去掉,纯计算死循环:


再查看,状态一定是 R!
因为:
没有任何 IO 等待
进程永远在运行队列里
所以永远显示 R
六、单核 CPU 下的状态真相
单核 CPU 同一时刻只能运行一个进程。
当你用 ps 命令查看进程状态时,肯定执行的是 ps 进程
while死循环是没有在cpu上运行的,因为被ps占据cpu了,但它一直在运行队列里,所以状态依旧显示 R
这就是 Linux 中 R = 就绪 + 运行 的真正含义。
Linux 中 R = 运行队列里的进程(就绪 + 运行)
Linux 中 S/D/T = 阻塞等待
挂起是内核内部操作,用户看不到状态
带 IO 的程序容易进入 S 状态,纯 CPU 计算程序才是 R 状态
sleep休眠状态
可中段休眠状态!
c
#include<stdio.h>
#include<unistd.h>
int main()
{
int a = 0;
scanf("%d",&a);
return 0;
}
我们gcc编译下后运行,在ps查看下状态

之前讲过scanf在等待键盘输入时,如果键盘没有数据,这时候处于阻塞状态,在linux中怎么变成s了?
其实linux内核中的s状态他就属于操作系统学科里的阻塞状态
printf打印涉及外设,向显示器打印,大量的消息在等待外设所以状态为r
s休眠状态,可中断休眠状态
s后面为什么有+,+表示在前台
./myproc表示在前台
./myproc &表示在后台运行
补充知识:前后台进程与+号的含义
前台与后台的区分
在Linux命令行中,执行一个程序:
c
#include <stdio.h>
#include <unistd.h>
int main()
{
int a;
printf("please input a number: ");
scanf("%d", &a);
printf("you entered: %d\n", a);
return 0;
}
· ./myproc ------ 前台运行
[dzh@iZ7xve6aquwnm3zofunwsmZ work]$ ./procmy
please input a number:

· ./myproc & ------ 后台运行
用ps aux查看进程状态时,状态后面带+的表示前台进程,不带+的表示后台进程。

为什么要有前后台?
因为键盘只有一个。多个进程同时存在时,你敲键盘输入的数据到底应该给谁?必须明确。所以操作系统规定:有且只有一个前台进程,前台进程拥有键盘输入权。其他进程都是后台进程,不能读取键盘。后台存在的意义,比如下载东西时在后台运行
前台进程的运行效果
写一个简单的程序myproc.c:
c
#include <stdio.h>
#include<unistd.h>
int main() {
while(1)
{
printf("I am a process:pid:%d\n",getpd());
sleep(1);
}
return 0;
}
编译后运行./myproc,它会卡住等待输入。此时在终端输入ls、pwd等命令,没有任何反应------因为shell(bash)已经退出了前台,变成了后台进程,你的输入被丢给了前台进程(myproc),但myproc并没有执行命令,所以命令无效。


如果我们在执行时加&让它后台运行:
bash
./myproc &

这个时候ctrl + c按下去完全停止不了,但是输入ll和pwd会发现可以运行但是输出信息是shell输出和程序输出互相干扰,这个时候只能kill - 9了
T状态
c
#include <stdio.h>
#include <unistd.h>
int main()
{
int a;
scanf("%d", &a);
printf("you entered: %d\n", a);
return 0;
}
运行(前台):
./myproc
此时进程卡住,等待输入。另开一个终端,用ps aux | grep myproc查看,可以看到状态为S+。
如果我们在运行时加上&让它后台运行:
./myproc &
再用ps查看,状态为S(没有加号)。但是注意,这个程序里面调用了scanf,它会试图读取键盘。作为后台进程,它没有资格读键盘。操作系统会怎么处理?把它暂停,状态变为T(后面我们会讲T状态)。
jobs
这个时候通过jobs命令可以查看后台任务列表:
bash
jobs
输出:
[dzh@iZ7xve6aquwnm3zofunwsmZ ~]$ jobs [1]+ Stopped ./procmy
使用fg 1可以将这个后台任务切回前台,此时它重新获得键盘输入权,状态恢复为S(带+号)。然后就可以正常输入了。
3.5 总结前后台规则
· 前台进程:任何时刻在linux命令操作过程中有且只能有一个进程是前台进程,键盘输入的数据只能给前台进程,状态显示带+。拥有键盘
· 后台进程:多个,无键盘,若强行读键盘则被暂停(T状态)。
· Ctrl+C只能杀死前台进程。
· 杀后台进程需要用kill -9 [PID]。
发送信号暂停进程
除了后台读键盘自动暂停,我们还可以手动发送信号让进程暂停。
首先查看所有信号:
bash
kill -l

输出1~64号信号。其中:
kill -9 等价于 kill -sigkill
1-31 ~31目前只见过9号信号
34-64 实时信号
· 19号信号:SIGSTOP,暂停进程。
· 18号信号:SIGCONT,继续运行暂停的进程。
示例:写一个无限循环打印的程序loop.c:
c
#include <stdio.h>
#include <unistd.h>
int main() {
while(1) {
printf("hello, PID: %d\n", getpid());
sleep(1);
}
return 0;
}
编译运行,用ps查看状态,大部分时间是S(因为sleep),偶尔出现R。找到PID后:
bash
kill -19 12345 # 暂停
ps aux | grep loop # 状态变为 T
kill -18 12345 # 继续
ps aux | grep loop # 状态变回 S 或 R

被暂停的前台进程会自动转为后台(+号消失),因为键盘不能给一个暂停的进程。
T状态与阻塞的关系
T状态本质也是一种阻塞:进程在等待一个恢复运行的信号。它与S状态的区别在于触发原因:S是自愿等待资源,T是被动暂停(或非法操作)。
t状态(追踪暂停):gdb调试时的断点
当使用gdb调试程序时,在断点处停下,被调试的子进程会进入t状态(tracing stop)。
演示步骤:
- 编译带调试信息的程序:gcc -g myproc.c -o myproc
- 启动gdb:gdb ./myproc
- 在第8行打上断点:break 8
- 运行:run
- 程序停在断点处,此时另开一个终端,用ps查看进程状态:
· gdb进程是父进程,状态正常。
· 被调试的子进程状态显示为t(小写t)。 - 在gdb中执行next单步,进程状态会变化。
小t状态也是暂停,但它是被调试器通过ptrace系统调用控制的。本质上与大T没有太大区别。
D状态(深度睡眠):不可中断睡眠
6.1 银行数据丢失的故事
这是理解D状态最关键的一个故事。
假设有一个进程,它要向磁盘写入1GB的重要数据(比如银行一天的转账记录)。进程把数据交给磁盘驱动,磁盘说:"行,我去写,你等着,无论成功还是失败,我都会给你一个结果。"于是进程进入S状态(可中断睡眠),等待磁盘完成。
此时操作系统内存严重不足,它需要杀掉一些进程来释放内存。操作系统看到这个进程在S状态,心想"这家伙可以被中断",就把它杀掉了。
过了一会儿,磁盘写完了(或者因为磁盘空间不足写失败),回来找进程:"喂,你的数据我写完了/写失败了,结果在这儿呢!"------但进程已经没了。磁盘手里的结果无处可送,数据处于不确定状态。如果是银行交易记录,就可能造成几千万的损失。
银行行长震怒,找来操作系统、进程、磁盘三方对质:
· 进程说:"我是受害者,我老老实实在等结果,是你操作系统杀了我。"
· 操作系统说:"我内存不足,杀进程是我的职责,你不能怪我。"
· 磁盘说:"我是硬件牛马,让我干啥就干啥,写失败我通知了,但进程没了,怪我咯?"
行长无奈,找程序员解决。程序员于是增加了D状态(Disk Sleep),深度睡眠也叫不可中断睡眠。也属于一种阻塞态
D状态的规则
当一个进程进行同步磁盘I/O时,它会进入D状态。处于D状态的进程:
· 不能被任何信号中断(包括kill -9)。
· 操作系统无权杀死它。
· 只能等磁盘I/O完成后自行唤醒。
这是为了保护重要数据不丢失。如果进程长期处于D状态(比如连续多次ps都能看到),说明磁盘性能极差或即将损坏。
如何模拟D状态?
使用dd命令进行大量磁盘写操作。
bash
dd if=/dev/zero of=./log.txt bs=1M count=1000
dev/zero 字符类型文件,允许用户读,但是读出来的是大量随机值
dev/null 字符设备文件,往文件里写的内容被系统自动丢弃
· if=/dev/zero:从无限零设备读取数据。
· of=./largefile:写入当前目录下的largefile文件。
· bs=1M:每次读写1MB。
· count=1000:写1000次,共约1GB。

在dd执行过程中,另开终端用ps aux | grep dd查看,有一定概率看到D状态(尤其是当磁盘较慢或负载较高时)。D状态通常是瞬时的,很难捕捉,但理论上存在。

警告:不要直接向/dev/sda等块设备写入,否则会破坏分区表。
如果你的系统中一个进程持续几分钟显示D状态,那么基本上可以断定磁盘快挂了。
Z状态(僵尸)与X状态(死亡)
7.1 为什么要僵尸?
为什么需要僵尸状态?一个故事
很多概念如果直接讲会很抽象,我们先讲一个生活中的故事。
想象你早上在公园晨跑,突然一位大爷在你面前倒下,你走近一看,他已经没有呼吸了。这时你会怎么做?作为有责任感的青年,你立刻打了110和120。
警察和医生很快赶到。医生确认大爷已经死亡。警察没有立刻通知家属,而是先拉起警戒线,叫来法医。法医仔细检查大爷的血液、毛发、皮肤,采集各种信息。为什么要这么做?因为警察必须搞清楚:大爷是自然死亡、意外死亡,还是被人谋杀?只有得出明确结论,排除他杀可能后,警察才会通知家属来料理后事。
换句话说,一个人死亡后,尸体不能马上处理掉。必须先把死亡原因调查清楚,给社会一个交代。这个调查过程,就是保留尸体一段时间,从中提取关键信息。
类比到进程:退出信息保存在PCB中
同样道理,你创建一个进程,是让它帮你完成某项任务。当这个进程退出(死亡)时,作为它的"父亲"------父进程,需要知道:任务完成了吗?正常结束还是出错了?错误码是多少?
这些信息必须被记录下来,保存在一个地方。这个地方就是进程的 PCB(进程控制块) ,也就是内核中的 task_struct 结构体。
当一个进程执行完毕、主动退出时,它的退出信息会被写入自己的PCB中。父进程如果想了解子进程的死因,只需要读取子进程PCB里的退出信息就行了。
return 值与退出信息
大家写C语言程序时,main 函数最后总会写 return 0。这个 0 就是退出码。你也可以写 return 1、return 2 等,都是允许的。
这个 return 的值,最终就会被内核写入进程的退出信息中。通常,0 表示"正常完成任务",非零表示"出错了"或"异常退出"。
所以,退出信息就是通过 return 语句传递并保存在PCB里的 。
当一个进程退出时,它的代码和数据可以被操作系统立即释放。但是,它的PCB中保存着退出码(exit code)、资源使用统计等信息。这些信息需要父进程来读取------父进程需要知道子进程是正常结束还是出错了,以及返回值是多少。
因此,进程退出后不能立即彻底消失,它的PCB必须保留一段时间,等待父进程通过wait或waitpid系统调用来读取退出信息。在这个等待期间,进程处于Z状态(僵尸)。
一旦父进程读取完毕,进程就从Z状态转为X状态(死亡),随后PCB被释放。X状态是瞬时的,我们通常观察不到。
僵尸状态的定义
现在回到状态话题。
一个进程已经死亡(代码和数据都可以释放了),但它的"尸体"------也就是PCB------还不能被清理,因为父进程可能还没有来读取死亡原因。这个暂时保留PCB、等待父进程前来"验尸"的状态,就叫做 僵尸状态(Z状态)。
具体来说:
- 进程退出了,操作系统会立即释放它的代码和数据(这些不再需要)。
- 但PCB不能释放,因为里面存着退出码。
- 这个PCB会继续存在,直到父进程通过
wait或waitpid系统调用把它取走。 - 在父进程读取之前,这个进程就处于 Z(Zombie) 状态。
- 父进程读取之后,进程变为 X(死亡) 状态,PCB被彻底回收。X状态是瞬时的,我们几乎看不到。

重新认识内存泄漏
很多同学以为:用 malloc 申请了内存,如果不 free,进程退出后内存还占着。这是一个误区。
事实是:当进程退出时,操作系统会回收该进程的所有内存资源,包括堆空间(malloc申请的内存)。 所以,一个跑完就退出的程序,即使忘记 free,也不会造成长期的内存泄漏。
但是,如果进程是一个永不退出的常驻程序 (比如网易云音乐、浏览器、手机APP),它内部如果不断 malloc 而不 free,内存就会一直涨,这才是真正的内存泄漏。
我们日常使用的绝大多数软件都是死循环(常驻进程)。你打开一个APP不关闭,它就一直运行。所以,对于常驻服务,内存泄漏是致命的;对于一次性任务,影响不大。
僵尸进程的危害
如果父进程一直不读取子进程的退出信息,子进程就会永远停留在Z状态。虽然只占用一个很小的PCB结构体,但成千上万个僵尸就会消耗大量内存,导致内存泄漏。
演示僵尸进程
编写代码zombie.c:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// 子进程:打印5次后退出
int cnt = 5;
while (cnt--)
{
printf("我是子进程,我正在运行:%d,ppid: %d\n",getpid(),getppid());
sleep(1);
}
printf("我是子进程,我退出了:%d,ppid: %d\n",getpid(),getppid());
}
else if (pid > 0)
{
// 父进程:什么都不做,一直睡,不回收子进程
while (1)
{
sleep(1);
}
}
return 0;
}
编译并运行:
bash
gcc zombie.c
./a.out
在另一个终端执行 ps aux | grep zombie,你会看到:
前5秒,父子进程都在,状态一般是 S(睡眠)。
5秒后,子进程退出,状态变为 Z(僵尸),后面带有 [defunct] 字样。
父进程还在运行,一直不调用 wait,所以子进程会一直保持 Z 状态。
杀掉父进程后,子进程的僵尸会被1号进程回收,然后消失。

孤儿进程
定义与现象
如果父进程先于子进程退出,那么子进程就变成了孤儿进程。操作系统会自动将孤儿进程的父进程改为1号进程(通常是systemd或init)。1号进程会定期wait,回收孤儿进程的退出信息。
演示孤儿进程
修改上面的代码:让父进程先退出,子进程无限运行。
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:永远运行
while (1) {
printf("我是子进程,我正在运行:%d,ppid: %d\n",getpid(),getppid());
sleep(2);
}
} else {
// 父进程:5秒后退出
sleep(5);
printf("Parent exit\n");
}
return 0;
}

运行后,前5秒子进程的父进程是原父进程。5秒后父进程退出,子进程的父进程变成1号被领养(系统)(或别的init进程)。此时用ps查看,子进程状态没有+号,因为它变成了后台孤儿。
退出信息被系统回收,避免内存泄漏
孤儿进程的特征
当一个进程变成孤儿(父进程先退出),会有两个明显的特征:
-
状态变为无前台标志
使用
ps查看时,孤儿进程的状态只剩下S(或R等),后面没有加号+。这说明它已经变成了后台进程。 -
无法用
Ctrl+C杀掉因为孤儿进程运行在后台,
Ctrl+C只能影响前台进程。要杀掉它,必须用kill -9 [PID]。
为什么要有孤儿进程?它有什么用?
僵尸进程的存在是有意义的------它保留了退出信息供父进程读取。那孤儿进程有什么实际价值呢?
答案是:守护进程(Daemon)就是利用孤儿进程的特性实现的。
- 守护进程是一种在后台长期运行、提供服务的进程(例如 Web 服务器、数据库服务)。
- 实现方式:父进程先
fork()一个子进程,然后父进程立即退出,让子进程变成孤儿,被 1 号进程(systemd/init)领养。 - 这个孤儿进程就此脱离了原来的终端,可以在系统后台周而复始地运行 ,不受用户登录、注销或
Ctrl+C的影响。
孤儿进程是守护进程的基础。关于守护进程的详细内容,我们会在后面的网络编程部分深入讲解。