【Linux】进程状态(二)
目录
前言:
一、进程状态:
1.运行状态(时间片)
2.阻塞状态
3.阻塞挂起状态
二、Linux进程状态:
1.运行状态(R)和阻塞状态(S)
2.深度睡眠状态(D)
3.停止状态(T)
3.1使进程在后台运行
4.追踪暂停状态(t)
5.死亡状态(X)和僵尸状态(Z)
5.1进程退出信息
三、孤儿进程:
四、命令总结:
总结:
前言:
我们已经知道了进程的一些属性,和如何创建子进程,那么接下来我们需要了解更多关于进程的概念。
一、进程状态:
我们主要讲述Linux的进程状态。
我们先来了解什么是并行和并发。
并发是指多个任务或者事件在一段时间内交替执行。它强调的是在宏观上看起来这些任务是同时在进行,但在微观层面,实际上在某个瞬间可能只有一个任务在执行。还是用交通来类比,在一个单车道的道路上,有多辆车(任务)需要通过。由于只有一个车道,车辆不能同时通过,但是通过合理的调度,让每辆车都有机会前进,在一段时间内,看起来所有的车辆都在前进。例如,在一个单核处理器的计算机系统中,同时运行多个程序。由于只有一个核心,这些程序不能真正同时执行。操作系统会在这些程序之间快速切换,在一段时间内,每个程序都能得到执行时间,就好像它们在同时运行一样。
并行是指多个任务或者多个事件在同一时刻同时执行。这就好比有多个车道的高速公路,不同的车辆(任务)可以在不同的车道上同时前进,它们在物理时间上是重叠的。例如,在一个拥有多核处理器的计算机系统中,不同的核心可以同时处理不同的计算任务。假设我们有一个四核处理器,当执行四个独立的计算任务(如对四个不同的数据集进行数学运算)时,这四个任务可以同时在四个核心上运行,它们真正地在同一时间点都在执行,这就是并行处理。
Linux/Windows民用级别的操作系统,都是分时操作系统(调度追求公平)。
实时操作系统,实时操作系统一般要将一个特定的进程彻底执行完(在特定的领域会使用),不追求调度公平。
操作系统内会提供一个runqueue的运行队列:
而我们一般就是并发,也就是这样:
1.运行状态(时间片)
所以我们在理解运行状态时,就是当进程在运行队列中,该进程就处于运行状态。
此时CPU调度进程时,直接在runqueue队列中拿到一个进程的PCB即可。之后在CPU上运行完以后(也就是该进程的时间片到达以后),直接根据FIFO算法(先进先出)将时间片到达的进程再尾插到进程链表最后即可。
2.阻塞状态
操作系统有时如何管理硬件的呢?——先描述,在组织!
struct device
int type;
//比如1代表键盘
int status;//状态
//管理时间
//其他属性
}
struct device
{
int type;
//比如1代表键盘
int status;//状态
//管理时间
//其他属性
}
此时如果有代码执行到了scanf时,CPU不可能一直让其在CPU上等待。其实struct device结构体中还有一个属性就是task struct* wait_queue:
所以当代码运行到scanf时,用户没有输入时,就会把该进程放入该设备的wait_queue中等待。当用户输入数据后,会再把该进程链入到runqueue中。
当进程在键盘的等待队列中,也被称为阻塞状态。所以进程都是在队列中的,状态只是看在那些队列中而已。
运行和阻塞的本质就是让不同的进程处在不同的队列中。等待的本质:链入目标外部设备,CPU不调度。
3.阻塞挂起状态
因为即使在等待队列中,也会占据内存,可此时内存已经严重不足了。此时操作系统为了自身的安全,会把该进程代码和数据换出到磁盘中,当然PCB还保留着。对于阻塞的进程,此时该用户又输入了内容,但是阻塞状态都是运行状态给定的,所以会直接把该进程再链入到运行状态的队列中,此过程为换入。
磁盘中有一个专门为此存在的分区——swap分区。当有很多这样处于阻塞的进程,操作系统内存严重不足时,可能会把所有处于阻塞状态的进程都换出到swap分区上。
以上是阻塞挂起状态。当然还有运行挂起,也就是正在运行时的进程加载到swap分区中。但是这个风险较大,一般操作系统不会开启。
这个过程就会变慢,也就是时间换空间。swap分区一般设计为内存的等量大小,根据工程师分配。
一般云服务器swap功能会被禁用,系统一般对时间要求更高。如果此时操作系统内存快要占满,就会杀死正在运行的进程以保证自身安全,这也就是我们有时可能会遇到的闪退。
二、Linux进程状态:
我们之前说的都是进程的状态,但是接下来我们要说的是Linux进程的状态:
1.运行状态(R)和阻塞状态(S)
我们写一个代码:
#include<stdio.h>
int main()
{
int cnt = 0;
while(1)
{
printf("hello world, cnt: %d\n", cnt++);
}
return 0;
}
运行该进程,在另外一个窗口中查看该进程的运行状态:
此时可以看到我们有时能查到S状态,有时能查到R状态(后面的+是指的在前台下跑的)。这是为什么?
其实S状态就是阻塞状态。你可能不相信,此时我们把代码更改一下:
#include<stdio.h>
int main()
{
int cnt = 0;
while(1)
{
scanf("%d", &cnt);
printf("hello world, cnt: %d\n", cnt);
}
return 0;
}
此时我们再修改代码,此时我们先把打印语句给注释掉再次观察状态:
此时为运行状态。 这是为什么?
一个进程的时间片是非常短的,而print是往显示器中写的,也就是IO,所以大部分时间都是阻塞状态,之后偶尔是R状态。
加入你使用的是云服务器,所以还要从你服务器所在地传入到你所在的地方,也就是网络IO,所以你可能会很难查到R状态。
在阻塞状态的进程,可以被kill发送信号给杀掉。这种可中断睡眠也称为浅睡眠。
2.深度睡眠状态(D)
阻塞等待状态的一种,不可中断睡眠,深度睡眠。
此时有10w条银行数据,需要往磁盘中写入,进程A会处于阻塞状态,知道在磁盘中写完这些数据。此时内存资源严重不足了,操作系统把这个进程A给干掉了。此时磁盘在写入第8w条数据时出现了错误,于是向进程A汇报错误,结果进程A已经挂掉了,于是磁盘就把这些数据清空了。
此时就出现了问题,因为这10w条数据很重要,于是操作系统就把向磁盘中写入数据进程状态的阻塞又分出了一种D状态,也就是不可终止的深度睡眠状态。
这里不做演示。当在公司中看到该状态,有两种可能:
- 磁盘可能要挂掉了,磁盘空间不足或者磁盘老化了
- 操作系统可能要挂掉了
3.停止状态(T)
我们再次修改code.c代码,让其死循环打印:
这里相当于发送暂停信号。 之后再对其发送-18SIGCONT继续信号:
可以看到刚才停止的程序又继续运行了。这时我们在程序运行的Xshell窗口上Ctrl+C发现无法终止程序,你可能也发现了,当我们查看code这个运行程序时,发现状态栏后面的+消失了,之前说过,这里的+代表在前台运行,此时已经转到后台运行了,而Ctrl+C只能杀死前台运行的程序,所以此时只能通过信号终止进程。
发送-9信号杀死code程序。
3.1使进程在后台运行
我们当然也可以让进程在后台进行。先将code.c代码修改为每隔一秒打印一次:
在调用程序时在后面加上&即可让程序在后台运行, 也就是Windows上的最小化。但是此时还是在前台打印了,因为我们并没用和终端文件做脱离,如果脱离就看不到了。
T状态一般是进程做了非法但不致命的操作,被OS暂停了。
4.追踪暂停状态(t)
我们需要使用gdb来观察t状态,我们更改makefile,生成一个debug版本的可执行程序。
为了方便动态查询,我们编写一个简单的shell脚本,这里死循环每隔一秒打印关于code进程的信息。
while :; do ps ajx | head -1 && ps ajx | grep code | grep -v grep;sleep 1; done
此时运行代码,会在断点位置停下:
所以t(tracing stop追踪的暂停)状态的本质就是当前进程被暂停了。此时我们n执行一步程序。
可以看到由t状态变为S+状态。 当进程被追踪的时候,断点停下,进程状态就是t。
5.死亡状态(X)和僵尸状态(Z)
dead状态和Z(zombie)状态。
我们首先要知道,进程为什么要被创建出来?进程创建出来是为了完成用户任务的。但是进程结束,任务到底完成没有?这是通过进程执行的结果告知父进程/操作系统的。
5.1进程退出信息
我们这里来了解一个命令:$?
代表上一个进程退出时的退出信息,一般0代表程序正常退出,也就是完成任务;而其他就是任务就是出错的。
我们平时写的.c文件,在结尾都会有return,我们修改code.c代码,让其正常执行完并使用echo $?来查看退出结果(更加具体的细节会在环境变量和进程替换中讲解)。
关于X(dead)和Z(zombie)状态,我们先举一个例子:
有一个大爷,跑的很快,忽然倒下了。你在旁边经过,作为新时代的三好少年,你不会袖手旁观,于是你拨打了110,之后警察封锁了现场,法医进行了鉴定,之后对外宣布大爷是如何死亡的。这里我们就可以把大爷理解为进程,其状态如下:
对于Linux为什么有Z状态,因为我们要维持退出信息,方便父进程和操作系统来进行查询。
当一个进程退出的时候:
1.代码不会执行---首先可以立即释放进程对应的程序信息数据
2.进程退出,要有退出信息(进程的退出码)保存在tastk_struct(int exit_code)内部
3.管理结构task_struct必须被OS维护起来,方便用户未进行获取进程退出的信息。
所以一个进程在创建的时候第一步是先创建内核数据结构(struct_task),之后加载代码和结构。在销毁的时候先释放代码和结构,之后OS维护内核数据结构,最后根据情况释放。
接下来就用代码证明,我们先创建子进程,此时父子同时存在,之后让子进程退出,父进程什么都不做。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("父进程运行,pid: %d, ppid: %d\n\n",getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程,我的pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(2);
cnt--;
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,我的pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
我们再次开启一个窗口,持续观察进程和进程状态,还是使用这个代码进行观察:
while :; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; sleep 1; done
defunct : 失灵的,不再使用的,死的
当然我们还可以使用另外一种方法观察僵尸状态,我们修改myprocess.c代码,将子进程代码修改为死循环:
我们再次新开一个窗口,发送信号将子进程杀掉:
僵尸状态的进程,也称僵尸进程,维护自己的task_struct,方便父进程读取状态。也就是说,没人管它,它就是一直僵尸,task_struct也就一直占用内存资源,即使对应的代码和数据已经释放。此时存在的问题就是内存泄漏。
我们以前一般写的程序存在内存泄露我们一般发不现问题。是因为我们很快就把程序执行完了,但是在一个大项目中,不会很快就执行完程序,所以我们要避免内存泄漏。
最后,我们要回收这个进程,需要父进程读取子进程信息,子进程才会自动退出,此时task_struct也就会释放。
此时我们已经将子进程杀掉了,再次发送信号是无法杀掉的,因为你无法杀掉一个在概念上已经死掉的进程。
如何回收?我们后面再讲,不过这里先抛砖引玉一下,wait方法回收(在2号系统调用手册中)。
X状态是瞬时状态,我们捕捉不到,但是确实存在。
三、孤儿进程:
我们已经知道了僵尸进程,也就是父在子退;而还有另一种情况,就是父退子在。我们还用刚才的代码进行演示,这次我们将父进程杀死。
我们可以看到子进程成为了后端进程,所以先发送信号将其杀死。
这就奇怪了,在之前我们把子进程杀掉,其会变成僵尸状态;而我们把父进程杀掉他却没有变成僵尸状态,这是为什么?因为父进程的父进程是bash,当我们杀死父进程时,bash会自动回收。而子进程没有了父亲,PID为1的进程会将其领养。
接下来我们看看PID为1的进程时谁。执行top命令,我们可以先将其理解为任务资源管理器。
因为操作体统必须对所有的进程进行管理,当一个子进程没有父亲时,系统进程就会将其领养。
四、命令总结:
top命令:相当于任务资源管理器。
可执行程序 + &:该进程在后台运行。
echo $?:查看上次进程退出信息。
死循环查看进程信息脚本:
while :; do ps ajx | head -1 && ps ajx | grep code | grep -v grep;sleep 1; done
总结:
我们知道了进程的状态并且知道如何观察,但是这只是冰山一角,我们目前只是初窥门径,欲知后事如何,且听下回分解(记得追剧啊!)。