【Linux系统编程】进程概念(三)进程状态
【Linux系统编程】进程概念(三)进程状态
- 1. 操作系统教材中的进程状态
- 1.1 运行状态
- 1.2 阻塞状态
- 1.3 挂起状态
- 2. Linux操作系统的进程状态
- 2.1 R运行状态
- 2.2 S睡眠状态
- 2.3 磁盘休眠状态
- 2.4 T停止状态
- 2.5 t追踪停止状态
- 2.6 进程状态查看
- 2.7 前台和后台进程
- 2.8 X死亡状态
- 3 僵尸进程(Z僵尸状态)
- 3.1 僵尸进程的危害
- 4. 孤儿进程
1. 操作系统教材中的进程状态

别看图中写了很多状态,其实主要就是三种状态:运行状态、阻塞状态、挂起状态。
下面具体说说这三种状态。
1.1 运行状态

- 一个CPU对应一个运行队列,在该运行队列中的进程,遵循FIFO(先进先出)的原则,排在前面的进程先被运行。
- 凡是在这个队列中的进程,状态都是运行状态。
1.2 阻塞状态
当我们运行一个有scanf的C程序的,程序会卡在scanf那里,它需要键盘上面的数据才能运行,否则就会一直卡在这里,键盘是硬件,而操作系统是硬件的管理者,操作系统肯定不能一直等这个进程,它还有别的进程需要调度,所以操作系统就会把该进程先托管到该硬件上,等该硬件就绪,这里就是键盘数据输入完成,再把该进程重新加入到运行队列中。
int main()
{int a = 0;scanf("%d", &a);printf("a = %d\n", a);return 0;
}

- 阻塞与运行的本质:看你的task_struct在谁提供的队列中。
1.3 挂起状态
如果操作系统(OS)发现自己的内存空间不足了,那OS就会把一些进程的代码和数据交换到swap分区(磁盘),即swap out(换出),此时该进程处于阻塞挂起状态,如果这样做还是不行,LinuxOS就会选择性的杀掉特定的进程,等到OS发先自己的内存足够了就会把swap分区中该进程的代码和数据拿回来,即swap in(换入)。

- 磁盘中的swap分区存在的本质是用时间换空间。
- swap分区的大小一般是内存的1.5~2倍,不建议swap分区太大,这样会导致OS会过度依赖swap分区,只要有进程就把它的代码和数据交换到swap分区,而过度的swap out/in,会导致系统变慢。
2. Linux操作系统的进程状态
static const char *const task_state_array[] = {"R (running)", // 0,运行"S (sleeping)", // 1,睡眠"D (disk sleep)", // 2,磁盘休眠状态"T (stopped)", // 4,停止"t (tracing stop)", // 8,跟踪停止"X (dead)", // 16,死亡"Z (zombie)", // 32,僵死};
2.1 R运行状态
R运行状态:并不意味着进程一定在运行中,它表明进程要么在CPU运行中,要么在CUP对应的运行队列里。
我们以下面的代码为例,讲解一下R运行状态。
#include <stdio.h>int main()
{while(1){}return 0;
}
运行结果:

我们看到该进程的STAT是R+,R就是运行状态,+表示该进程在前台,什么也没有表示该进程在后台,后面会讲前台和后台。

结合我们最开始讲的运行状态,思考一下为什么该进程处于运行状态?
因为该进程一直在死循环重复执行代码,占用CPU资源,并且它不满足访问外设,例如显示屏、键盘等,进入阻塞状态。
2.2 S睡眠状态
S睡眠状态(sleeping):意味着进程在等待事件完成,同时也叫做可中断睡眠,浅度睡眠,即睡眠期间可以被唤醒,对应操作系统学科中的阻塞状态。
我们以下面的代码为例,讲解一下S睡眠状态。
#include <stdio.h> int main()
{ while(1) { printf("I am process\n"); } return 0;
}
运行结果:

我们看到该进程的STAT是S+,此时该进程就处于睡眠状态。

那么我们思考一下为什么该进程会处于睡眠状态?
由于我们一直用printf打印,要访问外设显示器,在写入显示器完成的一瞬间将其加入到运行队列,等待运行,此时该程序处于运行状态,只不过时间太短,我们很难查看到,之后再托管到显示器中进行写入,所以我们查看的时候一般都会显示该进程处于休眠状态。
但是如果是下面的程序,只要我们不在键盘中输入数据,该进程就一直处于睡眠状态。
int main()
{int a = 0;scanf("%d", &a);printf("a = %d\n", a);return 0;
}


2.3 磁盘休眠状态
D磁盘休眠状态(disk sleep):也叫做深度睡眠,不可中断睡眠状态,Linux特有的状态,处于这个状态的进程通常会等待IO的结束,不响应任何请求,同时其也对应操作系统学科上的阻塞状态的一种特殊情况,处于深度睡眠的进程不可被信号,或者OS杀掉。
想象这样一个场景:一个进程正准备向磁盘写入一批重要数据。
- 危机潜伏:磁盘的剩余空间可能不足以容纳这批数据,但进程并不知情,它发起了写入请求后,便进入等待状态,期盼着磁盘的“操作成功”回复。
- 系统发难:恰在此时,操作系统发现内存资源告急,整个系统濒临卡顿。为了自救,它开始清理“占用资源却不干活”的进程。它一眼就看到了这个正在“悠闲”等待磁盘回复的进程,心想:“内存都快耗尽了,你居然还在空等?” 于是,操作系统手起刀落,强制终止了这个进程,并回收了其占用的所有内存,其中自然也包括那批等待写入的数据。
- 磁盘的抉择:几乎在同一时间,磁盘正在处理写入请求。它发现空间确实不够,无法完成这个任务。磁盘心想:“这批数据太大了,我写不进去。但我剩余的空间还可以服务其他进程的小数据请求,不能让它堵在这里。” 于是,磁盘丢弃了这批无法写入的数据,转而处理其他任务。当它回过头来,准备给刚才的进程回复一个“写入失败”时,却发现——那个进程已经消失了。
结果:数据彻底丢失了。进程的数据在内存中被系统释放,在磁盘端又被丢弃。用户的重要资料就此不翼而飞。
责任在谁?
- 操作系统:为了保障系统整体稳定而清理资源,看似无可厚非。
- 磁盘:为了不阻塞其他任务而丢弃无法处理的数据,似乎也情有可原。
- 进程:只是一个等待结果的“受害者”。
这成了一笔糊涂账。但用户绝不接受这种结果,频繁的数据丢失将是灾难性的。
解决方案:引入“深度睡眠”状态
为了解决这一致命问题,深度睡眠状态应运而生。当进程在执行诸如关键数据写入这类不容中断的任务时,它会被标记为此状态。
一旦进入深度睡眠,进程便获得了一把“尚方宝剑”:
- 对操作系统的清理指令置若罔闻,即使是强大的
kill -9信号也无法将其终止。 - 它仿佛进入了一个受保护的“结界”,其代码和数据会一直被保留在内存中,纹丝不动。
进程会一直维持这种“不死”的沉睡,直到它等待的事件发生——也就是磁盘确实给出了最终回复(无论成功与否)——它才会被唤醒并做出响应。
这样一来,在上述场景中,即使内存紧张,操作系统也无法杀死这个进程。磁盘在丢弃数据后发出的“失败”回应,最终也能顺利送达这个仍在等待的进程。进程收到回应后,便可以由内核将其正常唤醒和终止,或者尝试其他错误处理策略,从而从根本上避免了数据的无声丢失。
2.4 T停止状态
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{ while(1) { printf("I am process, pid:%d,ppid: %d\n", getpid(), getppid()); sleep(1); } return 0;
}



2.5 t追踪停止状态
t追踪停止状态(tracing stop):在调试过程中的进程停止。


2.6 进程状态查看
ps aux / ps axj 命令
- a: 显示一个终端所有的进程,包括其他用户的进程。
- x: 显示没有控制终端的进程,例如后台运行的守护进程。
- j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
- u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。
2.7 前台和后台进程
区分前后台进程:
谁能从键盘获取数据输入,谁就是前台。
键盘只有一个,所以在获取输入的时候,只能有一个进程在获取键盘数据。
前台进程任何时刻只能有一个,后台进程可以有很多个。
例如你自己的手机打开很多app,谁在你的屏幕上谁就是前台,其他的都在后台。
2.8 X死亡状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
3 僵尸进程(Z僵尸状态)
僵尸进程(zombies):子进程退出的时候,如果父进程没有主动回收子进程的信息,那么子进程会让自己一直处于Z僵尸状态,即对应子进程相关资源尤其是task_struct结构体不能释放。
我们以下面的代码演示一下僵尸进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{ printf("我是父进程:%d\n", getpid()); sleep(3); pid_t id = fork(); if(id == 0) { //child while(1) { printf("我是子进程:%d,我的父进程是:%d\n", getpid(), getppid()); sleep(3); } } else { while(1) { printf("我是父进程:%d\n", getpid()); sleep(3); } } return 0;
}
输出结果:

每3秒打印一次状态。

杀死子进程,此时父进程还在运行,无法收回子进程中的信息,导致子进程处于Z僵尸状态。

3.1 僵尸进程的危害
1. 进程退出了,退出信息是什么?
main函数的返回值or收到的信号值,该信号值在该进程的task_struct结构体中。
2. 进程退出了,退出信息保存在哪里?
进程自己的task_struct结构体中。
w
3. 检测Z状态进程,回收Z状态进程,本质是在做什么?
获取子进程的退出数据。
4. 具体怎么回收?谁来回收?
父进程系统调用(OS)wait命令来回收子进程的信息。

5. 子进程必须回收!!!
假如有一个常驻进程一直在malloc空间,如果我一直不回收该进程,它就会一直处于Z状态,它就会占据大量内存空间,task_struck也不会被释放,从而导致内存泄漏。
如果对应的进程结束了,内存泄漏还在吗?
进程结束,系统会自动回收该进程,不会存在内存泄漏。但在实践中,进程一般都是死循环,即不会结束的,所以内存泄漏问题很重要。
4. 孤儿进程
孤儿进程:在父子进程中,父进程提前退出,子进程的父进程会被改为1号进程(操作系统),我们称该子进程被操作系统领养了,此时这个子进程(孤儿进程)就变成了后台进程,无法从键盘中获取数据,那么Ctrl + C就无法退出,只能用信号kill -9 PID将子进程杀死。
我们以下面的代码为例,讲解孤儿进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{ // 创建子进程 pid_t id = fork(); if(id == 0) { //child while(1) { printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(3); } } else { //parent int cnt = 5; while(cnt) { cnt--; printf("我是父进程, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); sleep(3); } } return 0;
}

父进程退出,子进程被1号进程领养,并变成后台进程。

后台进程无法获取键盘中的数据,所以Ctrl + C不能结束该进程。

使用信号kill -9 PID 杀死该进程。


为什么要被系统领养?
给孤儿进程退出进行善后,系统自动回收。
