【Linux】进程状态
- [1. 为什么要有状态](#1. 为什么要有状态)
-
- [1.1 基于时间片轮询](#1.1 基于时间片轮询)
- [1.2 简单描述进程排队](#1.2 简单描述进程排队)
- [1.3 运行状态](#1.3 运行状态)
- [1.4 阻塞状态](#1.4 阻塞状态)
- [1.5 挂起状态](#1.5 挂起状态)
- [2. Linux下的状态信息](#2. Linux下的状态信息)
- [3. Linux下具体的进程状态](#3. Linux下具体的进程状态)
-
- [3.1 运行状态R](#3.1 运行状态R)
- [3.2 可中断睡眠-浅度睡眠-S](#3.2 可中断睡眠-浅度睡眠-S)
- [3.3 可不中断睡眠-深度睡眠-D](#3.3 可不中断睡眠-深度睡眠-D)
- [3.4 停止状态-T](#3.4 停止状态-T)
- [3.5 停止状态-t](#3.5 停止状态-t)
- [3.6 死亡状态-X](#3.6 死亡状态-X)
- [3.7 僵尸状态-Z](#3.7 僵尸状态-Z)
-
- [3.7.1 僵尸进程的危害](#3.7.1 僵尸进程的危害)
- [4. 孤儿进程](#4. 孤儿进程)
1. 为什么要有状态
1.1 基于时间片轮询
首先我们大部分的电脑都只有一个CPU,但是我们的电脑去可以同时执行多个应用程序,比如我们同时登录了QQ和微信,也可能同时在看抖音。所以说虽然我们的CPU只有一个但是可以同时运行多个应用程序(进程)。也就是说我们启动的一个进程并不是一直在运行的,虽然进程需要在CPU中运行,但是并不是一直在CPU上一直进行运行的。而是进行轮询的方式占用CPU的资源,是基于一种时间片的方式进行,就是给每个进程占用CPU资源的时间,一旦时间过了就需要从CPU中下来让另个一进程进行占用,保证其他进程可以占用CPU资源,这也是为什么只有一个CPU,却可以同时有多个进程在运行。
1.2 简单描述进程排队
前面我们也已经说过了其实进程=内核数据(task_struct) + 可执行程序。所以我们让进程去排队是让可执行程序去排队还是让task_struct去排队呢?就好比我们去面试,我们肯定是先让我们的简历去进行排队。所以让一个进程去排队其实是让task_struct去排队。但是这里又有一个问题,前面我们不是讲过了,task_struct是通过链表进行连接的,到这里怎么就又是队列了呢?这里我们需要补充一个知识点就是一个task_struct是可以被连入多个数据结构中的。
在我们以前学习双联报表的时候是这样实现的:
但是在Linux下并不是这样实现的,而是下面这样实现的:
那么到这里有小伙伴就可能有疑问了,如果我们只是拿着listnode去进行连接,那怎么找到task_struct的首地址呢?所以这个时候就需要用到偏移量了,是不是只要知道n在task_struct里的偏移量,在使用n的地址减去偏移量就找到了t的地址了。具体做法:(task_struct*)(&n - &((task_struct*)0->n);
而是用这样的方式我们就可以不止定义一个listnode结构体,在task_struct中我们可以定义多个这样的listnode,就可以实现使用多种数据结构将task_struct进行管理起来,并且这样做的话,我们想要在添加新的数据结构,并不需要将原来的数据接机构进行清除,而是在添加listnode结构体,进行新从数据结构连接。
1.3 运行状态
所谓状态就是决定了后序的动作,Linux下可能存在多个进程都要根据它的状态执行后序的动作。比如一个进程不可能一直都处于运行状态,他可能在等待软硬件资源,而在等待的过程中是不需要进行消耗CPU资源的,也就可以不用加载到CPU中占用CPU资源。而一个CPU会有一个运行队列,被加载到队列中的进程都可以认为是运行状态的,这个队列会基于时间片的方式进行轮询是使每个被加载到队列中的task_struct都可以享受到CPU的资源。那么就是根据什么,CPU知道需要将进程加载到运行队列中呢?答案就是进程状态。在一些教材中常见的状态有运行,阻塞,挂起,只有状态处于运行状态才可以被加载到运行队列中。
1.4 阻塞状态
比如我们写了一个scnaf()
函数从键盘中获取数据,当我们运行这个程序的时候,首先是需要加载到CPU中的,但是一旦执行到scnaf()这个函数,此时进程在等待硬件的资源,可是CPU(操作系统)并不知道你要等待多久,那么如果一直在CPU中进行等待的话就会浪费CPU的资源,所以这个时候操作系统就会将该进程的状态设置为阻塞状态,并把该进程放到键盘设备的等待队列(你没有听错,不只是CPU有运行队列,其他设备也有自己的队列,CPU也是设备)设置该进程为阻塞状态。只有当该进程从键盘中拿到了数据,此时操作系统才会将该进程更新状态为运行状态并,重新放到CPU运行队列中。
所以当我们的进程在等待软硬件资源的时候,资源如果没有就绪,我们的进程task_struct只能第一将自己设置为阻塞状态,第二将自己的PCB连入等待的资源提供的的等待队列。也就是说状态的变迁,引起的是PCB会被操作系统变迁到不同的队列中。
1.5 挂起状态
而挂起这里我们只谈阻塞挂起,有些像运行挂起之类的我们这里不进行讲解。当计算机资源比较吃紧的时候,内存占用情况比较严重,这个时候。如果我们还将在内存中加载进程的话将要面临的问题第一要么进程直接挂掉,第二就是将内存进行站桩腾挪挤出空间供进程使用。所以操作系统会将处于阻塞状态的进程中的代码和数据交换到磁盘上这操作叫做换入=出操作 ,将内存腾出空间来,磁盘上有固定的一块区域,专门用来当内存资源紧张的时候,将内存中的数据进行换入换成,这个部分叫做swap分区 。一旦阻塞状态被更新为运,行状态,在从磁盘上重新换入操作。但是这里的全部前提都是进程的PCB是不会别换出的,这想到不用想,如果PCB也别换出去了,谁知道这进程到底要不要换入啊!所以这里我们其实也可以推测出一个进程的加载过程,一个进程的加载时先创建的PCB并加载了PCB,再是将代码和数据进行加载的,也就是说一个进程其实只要PCB有了,后序的数据和代码都是可以进行慢慢的加载过来的。
那么是不是我们的swap分区越大越好呢?首先我们要分析一下换出其实本质是把数据拷贝到外设,换入的本质其实就是将数据拷贝会内存。我们之前在将冯诺依曼的时候,数据从外设到内存,从内存到外设,本质在内存访问的时,我们访问外设的动作一定是比较慢的,所以在换入和换出的操作本质就是用效率换取系统的可用性。一旦我们将swap分区设置的很大,那么操作系统就会很依赖swap分区,带来的结果就是内存和外设进行IO的次数的频率就会非常的高,结果就是低效的操作的比重就高了,那么整体的效率也就下降了。所以一般是将swap分区设置为内存大小的一半。
2. Linux下的状态信息
其实所谓状态,本质上其实就是一个整形变量,是在task_struct属性中的一个整形变量。
c
/*
* 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) | 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。 |
S睡眠状态(sleeping) | 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。 |
D磁盘休眠状态(Disk sleep) | 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。 |
T停止状态(stopped) | 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。 |
X死亡状态(dead) | 这个状态只是一个返回状态,你不会在任务列表里看到这个状态 |
Z(zombie)-僵尸进程 | 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 |
3. Linux下具体的进程状态
3.1 运行状态R
R运行状态(running)并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
c
int main()
{
while (1)
{}
return 0;
}
3.2 可中断睡眠-浅度睡眠-S
S睡眠状态(sleeping)意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
其实就等同于上面个我们讲的进程阻塞状态。
c
int main()
{
int a = 0;
scanf("%d", &a);
printf("a=%d\n", a);
return 0;
}
S+后面这个加号表示前台运行,没有加号表示后台运行。
"+"代表是前台运行,无"+"代表后台运行,后台运行时可在命令行继续输入指令并执行,但无法用ctrl+c结束,需要用kill -9 pid杀死。想要后台运行某个程序就在后面加"&",如:./test &
3.3 可不中断睡眠-深度睡眠-D
在这里D状态我们无法使用代码进行演示。我们只能简单的说明它的原理。
前面我们说过了,如果内存资源比较吃紧的话,操作系统是会让一些处于阻塞状态的进程进行换入,把数据和代码拷贝到磁盘上,进而把空间进行暂时的腾挪出来。但是如果内存资源非常的吃紧,操作系统是有权利直接把进程给杀掉的。所以这就会导致一个问题,一旦一个进程正处于阻塞状态,它在通知磁盘设备将数据拷贝到磁盘上,并等待磁盘回复拷贝情况,这个时候内存资源非常的吃紧,而操作系统有正好的检测到了这个进程,并将其杀掉了,而这个时候磁盘拷贝失败了并将结果告诉进程,可是发现找不到这个进程了,于是就将数据给丢弃了。从这个过程中我们发现,如果这个数据是非常重要的话,引发的结果不敢想想,所以这里就需要有一个状态,类似于免死金牌,到操作系统检测到进程时,进程出示免死金牌,操作系统就不会将其杀死。
当然一般都不会出现上面的状况,因为一旦出现D状态就说明当前的系统资源已经非常的吃紧了,上面的状况也可以说明为什么有时候我们打开了很多的引用程序后,有时候有些程序就强制推出了的原因。而D状态其实本质上也是阻塞状态的一种
3.4 停止状态-T
前面我们使用了kill -9 杀死一个进程,这个其实是一个信号,后面我们会讲信号,这里我们就先看一下有哪些信号。
这里会发现一个19号信号SIGSTOP,就是一个暂停信号。
如果要让他继续运行可以使用18号信号SIGCONT
其实上上面我们也发现了,一点我们暂停了,就自动变成了后端运行了,当我们重新启动的时候也是变成了后端运行了,这其实也符合厂里,既然是你主动给我暂停了,那么就不因该让前台继续运行了。如果要将其停止,只能使用kill -9将其杀掉。
而至于上面的不是R运行状态,而是S睡眠状态,主要原因是我们打印数据其实是显示到显示屏上的,而显示屏其实也算是外设,而我们的CPU执行printf这个指令其实是非常的快速的,而至于打印出来其实不关CPU的事情了,所以者时候进程其实一直处于等待显示器显示打印结果的过程,所以才显示为S状态。
3.5 停止状态-t
t也是一种暂停状态。
而暂停状态其实也是一种阻塞状态。
3.6 死亡状态-X
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态,死亡状态是一个瞬时过程,我们很难查看到。
3.7 僵尸状态-Z
一个进程被创建出来就是用来帮忙干事情的,一点这个进程死了,我们要不要知道这个进程为什么死掉了,交给这个进程干的事情干的怎么样子了。这些都需要提供给创建它的那个。也就是说需要获取这个进程的推出数据,所以一个进程退出了,它的代码和数据可以直接释放,因为不会再进行执行了,但是它所对应的PCB不因该立马被释放,这样是为了让系统或者创建它的进程获取它的数据。
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 3;
while (cnt)
{
printf("child[%d] is begin Z...\n", getpid());
sleep(1);
cnt--;
}
exit(0); // 直接让子进程退出
}
while (1)
{
// 父进程一直运行,但是不读取子进程的状态信息
printf("parent[%d] is sleeping...\n", getpid());
sleep(1);
}
return 0;
}
3.7.1 僵尸进程的危害
为什么要有僵尸状态呢?
创建一个进程的目的时希望这个进程可以帮我们完成工作,所以子进程必须有结果数据,而结果数据时保存在PCB中的。所以进程的代码和数据可以释放,但是PCB不能立即释放,必须等到PCB中的数据被父进程拿走才可以释放。所以子进程就必须维持当前现状,供上层读取。
如果父进程一直不读取呢?
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护,而PCB是要占居内存的。所以如果一个进程创建了很多的子进程,但是就是一直不回收子进程的退出信息,那么PCB就会一直存在,就会导致内存泄漏。
为什么我们在命令行启动的进程不需要读取进程退出信息
那是因为我们在命令行创建的进程都是是bash的子进程,而bash会自动的读取进程的退出信息。
4. 孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为"孤儿进程"孤儿进程被1号init进程领养我们认为就是操作系统领养的,当然要有init进程回收喽。
并且一个进程变成孤儿后,他还会变成后台进程。