环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:【 Linux 】 🌹undefined 相关文章推荐: 【Linux】冯.诺依曼体系结构与操作系统 【Linux】进程理解与学习(Ⅰ) 浅谈Linux下的shell--BASH
在前文中我们已经了解了进程的相关概念,明白了 OS管理进程实际上就是对进程对应的task_struct/pcb做相关操作,但是实际上系统中存在的进程有很多,我们可以输入指令 ps -lA 来 查看当前系统下的所有进程。而OS为了更高效的对进程进行管理,不会对每个进程都面面俱到,而是会将进程分为 不同的状态( 这就好像OS为了更好的管理内存,会将内存划分为不同的分区,每个分区存放各自对应的数据类型,比如栈区存放一些局部变量、静态区存放全局变量等)从而来进行 更好的管理。
本次章节目标就是对进程的不同状态做相关介绍与深入了解。
ps -lA查看系统下的所有进程(部分)
阻塞
在了解进程状态之前,我们先来谈一谈阻塞与挂起的两个概念。所谓阻塞,就是指进程因为等待某种资源就绪,而导致的一种不推进状态。也就是我们常说的卡住了。
🔺这里简单举个例子
分析图
当然,为了更直观的看到这种现象,我们可以看下面这张图
分析图2
挂起
我们可以查看内核中的源码,来看看进程的各种状态
/*
* 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表示该进程处于一种运行状态,不过这里需要注意的是,R状态表示的运行是指只要进程的pcb在运行队列下,都是R,并不是说一定是指在cpu上运行。
我们看一下以下代码:
#include<stdio.h>
int main()
{
while(1)
{
printf("hello world,pid:%d\n",getpid());
}
return 0;
}
运行结果与疑惑
S表示的是可中断休眠状态,为什么不是R状态呢?实际上确实如我们所说,我们的程序一直在运行,但是,cpu调度的速度实在是太快了,我们很难捕捉到,并且pcb相对于被cpu的调度时间,其余99%的时间都用在了在外设(显示器)的等待队列中排队了。所以,我们可以看到程序在不断打印数据在显示器,但是却捕捉不到cpu调度pcb并执行的那一刻。
不过我们假如将外设这个干扰给屏蔽了,如下:
#include<stdio.h>
int main()
{
while(1)
{}
return 0;
}
小总结一下
正如上面的图片所示,我们看到S状态是因为程序的pcb99%的时间都在外设的等待队列下排队,而被cpu调度的那一刻我们捕捉不到。这里大家有没有发现,休眠状态与我们的上面讲的阻塞状态一致,阻塞就是指pcb不被cpu调度,去某个资源的队列下等待资源就绪。
事实上确实如此,S状态的本质就是阻塞。(pcb去某种资源的等待队列下排队,等待资源就绪)。至于说它是可中断休眠,是因为我们可以通过ctrl c或者kill命令来结束该进程。
可中断休眠
所谓不可中断休眠状态,说白了就是我们不可以使用kill、ctrl c等命令将进程中断,我们只可以将电源关闭,以此来结束进程,但是这样做可能会造成数据的丢失等问题。(这种状态一般很难看到)
暂停状态顾名思义就是让该进程暂停,我们可以通过指令kill -19 pid的指令来暂停该进程。输入指令kill -18 pid可以使该进程继续运行。
(不过需要注意的是,当恢复运行时,此时的进程就处于后台进程,我们用ctrl c结束不了,用kill指令才可以中止进程,关于前后台进程我会在后面的章节讲解。)
如下:
我们每一个进程结束时都会有一个退出码,就好像我们写一个main函数时,最后都会加上return 0,return 0就表示该进程正常返回(事实上就算我们不写return 0,系统也会默认return 0),这里的0就是该进程的返回代码。
(Linux下可以通过echo $?指令来查看该进程的返回代码)
vs下编译后的返回代码
僵尸状态是指一个进程结束时,它的返回代码没有被父进程读取,那么该进程会一直处于一种僵尸状态,等待父进程读取,直到父进程读取返回结果后,才彻底结束子进程。
保持僵尸状态是为了让父进程读取该进程的返回代码,而我们平常写的程序为什么结束后没有变成僵尸呢?
不过我们可以写以下代码,用fork创建子进程,来观察僵尸状态:
#include<stdio.h>
#include<unistd.h>
int main()
{
//fork创建子进程
pid_t id=fork();
if(id == 0)
{
//child
while(1)
{
printf("子进程:pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//father
while(1)
{
printf("父进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
此时我们kill -9 进程pid结束子进程,观察子进程是否立马结束,还是如我们所说维持僵尸状态,等待父进程读取推出结果。通过以下运行结果可以发现,子进程并不是直接退出。
运行结果
如果一个进程处于Z状态,假如它的父进程一直不读取该进程的退出码,那么该进程会一直维持僵尸状态。而维护这个状态,实际上就是维护这个数据,该数据也属于进程基本信息,保存在task_struct(PCB)中,不仅 占用了内存空间(因为task_struct本身是一个结构体,结构体都会有自己的大小),并且 维护pcb也是有一定的代价的。
我们上面讲了子进程退出时不会立马退出,而是维持僵尸状态等待父进程读取退出结果。(如果父进程是bash,则会被bash的回收机制回收,不会出现僵尸)
那么假如子进程正常运行,父进程结束呢?如下:
杀掉父进程,发现子进程的父进程变成了1号
这里我们来分析一下,为什么父进程结束后不会出现僵尸呢?
为什么该父进程的子进程被1号进程领养了呢?1号进程是什么?
1号进程实际上就是操作系统
生活原本沉闷,但跑起来就会有风!🌹