目录
- 一、进程状态
-
- [1.1 什么是运行状态?](#1.1 什么是运行状态?)
- [1.2 什么是阻塞状态?](#1.2 什么是阻塞状态?)
- [1.3 什么是挂起状态?](#1.3 什么是挂起状态?)
- 二、Linux具体的进程状态

个人主页:矢望
一、进程状态
1.1 什么是运行状态?
一个CPU对应一个调度队列 。

准备好运行并且需要CPU资源的任务,会在CPU的调度队列中排队。凡是在这个队列中的进程,它的状态都是运行状态。
1.2 什么是阻塞状态?
- 阻塞的现象:
我们都写过这样一份代码:
c
#include <stdio.h>
int main()
{
int a;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}
编译并运行 :

当你运行之后,没有输入数据,此时就卡在那里了,这就是进程堵塞。
OS角度
当我们运行这个程序,程序就会陷入等待,等待的是键盘上输入的数据,也就是硬件就绪。硬件一旦就绪,程序就可以运行了。
硬件的管理者是OS,所以操作系统可以第一时间知道硬件是否就绪。操作系统通过先描述,再组织管理硬件资源。

假设现在运行这个程序,这个程序就是运行状态,如果键盘上迟迟没有数据输入,此时进程就会从调度队列上剥离下来,从而进入键盘资源的等待队列,此时由运行状态改为堵塞状态,当有数据输入时,OS会再次将堵塞状态改为运行状态,并且这个进程会再次加入到调度队列中运行。
阻塞与运行的本质:你的PCB(task_struct)在哪个资源提供的队列中。
1.3 什么是挂起状态?
现在操作系统在正常执行进程,突然操作系统的内存空间不足了。此时操作系统发现有一部分进程一直在阻塞状态,在白白的浪费资源,此时OS就会把该进程对应的代码和数据交换到磁盘中的swap分区里,把该进程所占用的内存空间释放掉,这样内存就被节省出来了。等到该进程就绪之后,在将这个进程的PCB重新链入到调度队列里,当进程真正被调度时,再将swap分区中的代码和数据重新调回内存。

进程被挂起:进程的task_struct在内存里,而进程对应的代码和数据因为内存资源不足,被交换到磁盘的swap分区。
swap分区
swap分区存在的本质:用时间换空间。
swap分区不建议太大,这个分区一般和内存一样大,或者是内存空间的1.5 - 2倍,如果这个分区过大,会导致操作系统过度依赖swap分区,频繁的进行换入、换出操作。这种过度进行swap会导致系统变慢。
- 扩充
当操作系统将所有的阻塞进程的代码和数据全部换入到swap分区里,但此时内存空间还是不够时,操作系统会将没有被调度的进程对应的代码和数据换入到swap分区里,这叫做运行挂起 ,如果这样内存空间还是不足,LinuxOS会选择性的杀掉特定的进程。
二、Linux具体的进程状态
cpp
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
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 */
};
上面所罗列的就是具体的进程状态。
R(running): 运行状态,并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { while(1) { printf("我是一个进程,pid: %d\n", getpid()); } return 0; }如上,当我执行这个程序时,我们观察它的进程状态。
如上图,显示的是
S状态,这可是睡眠状态,可是进程一直在打印这句话呀,这是为什么? 我们接下来把这代码注释掉试试。
注释掉之后,再次执行程序进程状态就变成了运行状态,这是为什么?
原因 :当我们没有注释掉的时候,
printf的本质是向显示器做写入,而我们执行的程序在内存里,所以printf也就是从内存向显示器做写入 ,printf的每一次调用都要进行IO,而只要进行IO,程序的大部分时间都是在等的,因为死循环进行写入时,缓冲区一下就写满了,之后写入条件就不具备了,只有程序在向缓冲区做写入时,那一刻的状态才是运行状态R,绝大部分状态都是S状态。而当代码被注释之后,程序运行直接使用的是CPU资源所以查出的是R状态。
S(sleeping): 睡眠状态 ,意味着进程在等待事件完成,这里的睡眠有时候也叫做浅度睡眠 / 可中断睡眠interruptible sleep,也就是我们上面介绍的阻塞状态。
#include <stdio.h> int main() { int a; scanf("%d", &a); printf("%d\n", a); return 0; }运行该程序:
如上图,这就是睡眠状态。
扩充1 :使用信号杀掉进程kill -9 [进程pid],之前我们都是使用Ctrl + C终止进程。
扩充2:状态之后的+意味着什么 ?我们以一个循环程序为例,当我们执行它时,我们会发现此时我们输入其它的命令,
bash就没有反应了。
我们把这种进程叫做前台进程 ,在查前台进程的进程状态时,往往后面会带有加号
+,这种进程运行时,bash就在后台了。如果你不想让进程在前台运行,可以在程序的执行过程中加上
&,此时它就在后台运行了。
此时就不影响正常命令的执行。
如上图,这种进程就称之为后台进程 。另外
Ctrl + c无法终止后台进程,可以通过kill -9杀掉后台进程。
前后台进程的最大区别:前台进程可以获取键盘输入,而后台进程不行 。而Ctrl + c属于键盘输入,所以无法杀掉后台进程。
D(Disk sleep):Linux特有的状态,休眠状态,有时候也叫不可中断睡眠状态uninterruptible sleep,在这个状态的进程通常会等待IO的结束,通常IO完成后会自动恢复 。
处于该状态的进程往往是在磁盘进行写入,往往非常重要,因此D状态的进程不能被信号或者操作系统杀掉 。
D状态进程是Linux系统的"保护机制",但也是"单点故障"。少量短时D状态正常,但大量或长期的D状态需要立即处理,否则可能导致级联故障。最好的策略是预防和监控。如果系统已严重挂起,真到了万不得已时的最后手段就只有拔电源了。
T(stopped): 停止状态,可以通过发送SIGSTOP信号给进程来停止进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
kill -l:列出所有可用的信号。
kill -19如上图,该进程果然处于了暂停状态。接下来再将其恢复,
kill -18。
暂停状态出现的场景 :1.进程做出非法操作,操作系统暂停进程 。2.t (tracing stop):追踪暂停,追踪一个进程,在gdb调试的时候所处的状态就是t状态 。首先来看第一种场景,当一个需要获取键盘输入的进程被放在后台,此时就会出现
T状态,因为后台进程是无法获取键盘输入的,也就是进程做出了非法操作,OS将其暂停了。
#include <stdio.h> int main() { int a; scanf("%d", &a); printf("%d\n", a); return 0; }如上图,有两次执行,第一次是前台进程,进程状态是睡眠状态。第二次是后台进程由于进行了非法操作,被
OS暂停了。接下来,我们在来看第二种场景,我们编译时加上
-g选项,之后进行用gdb进行代码调试。
刚开始
gdb code时,我们只看到gdb一个进程,在打完断点,执行r命令时,我们看到gdb是父进程,而code是子进程,所以gdb运行的本质是gdb作为父进程,启动了一个子进程,然后这个子进程遇到断点就停下来了,这个子进程此时处于t状态 ,可以理解为gdb遇到断点时,给子进程发送了19号信号,把子进程暂停了。当你向继续运行时,可以输入n,此时代码就会继续运行,相当于父进程给子进程发送了18号信号,执行完后接收到19号信号又会停下来。所以gdb可以通过不断给子进程进行发送18、19号信号,从而使子进程暂停、运行,这样就可以达到逐语句,逐过程的目的。
X(dead):死亡状态 ,这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态。
Z (zombie):僵尸状态 ,进程已终止但父进程尚未读取其退出状态。一个进程死亡时,它不能直接是
X状态,它必须先处于Z状态,为什么? 一个进程运行是有它自己要执行的任务的, 所以在它结束前,我们需要获取它退出时的相关信息,所以当一个进程结束时,会将它的PCB设置为Z状态,那么此时操作系统不能释放处于Z状态的进程,OS会通过一定方式获取进程的退出信息,之后再将这个进程设置为X状态,此时才能杀掉该进程。观察
Z状态:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf("我是一个进程,pid: %d\n", getpid()); sleep(3); pid_t id = fork(); if(id == 0) { while(1) { printf("我是一个子进程,pid:%d, ppid: %d\n", getpid(), getppid()); sleep(1); } } else { while(1) { printf("我是一个父进程,pid: %d\n", getpid()); sleep(1); } } return 0; }接下来,我们编译运行这个程序,并在执行过程中将子进程杀掉,观察子进程的状态。
如上图,因为父进程没有对子进程进行回收,所以会看到子进程处于Z状态。
- 几个子问题:
1.进程退出了,退出信息是什么?
是main函数的返回值,或者是收到的信号值 。
2.进程退出了,退出信息保存在哪里?
一般保存在进程自己的task_struct结构体中 。
3.检测、回收Z状态进程本质是在做什么?
本质是在检查、获取task_struct内部的退出信息数据,获取完成之后就可以将进程状态改成X,接下来释放它 。
所以Z僵尸状态指的是只保留进程的task_struct,未来让父进程或者OS获取到子进程的退出数据 。Z状态的核心:最小化保留的task_struct。
4.怎么回收,谁来回收?
一般是父进程通过系统调用回收子进程 。
如果就是不回收子进程会发生什么呢?此时子进程就会永远处于Z状态,也就是task_struct不会被释放了,它可是占据着内存空间的,这就是内存泄漏 。
所以最佳实践:子进程必须回收。
扩充 :如果对应的进程结束了,内存泄露问题还存在吗? 答:不存在了 ,用户态进程退出时,操作系统会自动回收它的所有内存 。我们生活中的所有软件你会发现它都是死循环,你会发现它一般不退出(你看似把软件退出了,实则它在后台运行),它也叫常驻进程,所以才会在意内存泄漏。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~













