Linux 七大进程状态
一.前言
进程只有被操作系统管理好了,才能发挥它的全部作用 ,而系统中存在多个进程,操作系统无法做到面面俱到,因此为了更好的管理进程,操作系统把进程分成了几种状态:运行 阻塞 挂起 休眠等等。至于每种状态的 应用场景是什么?有什么用?本文将会带大家认识各种进程状态
在谈进程状态之前,我们需要了解什么是进程、怎么去描述并组织进程、如何创建一个进程。
- 操作系统(OS)的本质是:先描述,在组织
- 操作系统并非直接管理 --进程,而是管理进程 的 PCB(task_struct)
- PCB中有着进程的各种信息,包括:PID、PPID 、进程状态等
- 我们可以通过函数getpid()获取当前进程的PID
- 进程间存在父子关系,可以通过fork()主动创建子进程
- 父子进程相互独立,共享一份代码时,具有写诗拷贝机制
二、什么是进程状态?
- 所谓的进程状态其实就是PCB(task_struct)内部的一个整形变量-----int status
- 操作系统(OS)要更改一个进程状态,只要更改进程的PCB(task_struct)中的状态属性,它只是一个标志位用来表明进程的状态,仅此而已。
那么状态决定了什么呢?
- 进程后续的动作!---- 而操作系统(OS)中可能会存在多个进程都要根据它的状态执行后续动作,所以说明 --状态-- 对于操作系统后续如何管理 进程十分重要!!
三、操作系统(OS)下的 --- 进程状态
- 对于进程而言呢,它是操作系统中的概念,如果有学习过《操作系统》这门学科的话应该可以很清楚对于进程而言的话是存在着许许多多的状态,如果一开始刚刚接触的小伙伴一定会感觉这么多状态要怎么区分呀😵
- 其实那么多的状态,真正主要的也就那么几个,所以接下去我会中重点讲解以下几种进程的状态
运行状态
首先我们要谈到的是【运行状态】,这个状态是最普遍的:进程 PCB 被调度到 CPU 运行队列中且已被分配 CPU 资源,就叫做 ------ 运行状态
一个CPU,一个运行队列,也就是说这些进程都需要在CPU这里进行排队,逐个执行
而排队的是进程么?
其实不是,排队的是进程的PCB(面试时不是你在排队,而是你的简历信息在排队)
- 首先对于一个进程而言,我们知道它是由内核 数据结构 + 所对应的代码和数据所组成的,所以当系统中存在多个进程的时候就势必会存在多个结构体;当然我们需要将这些进程给链接组织起来(双向链表链接)
- 那么这些进程就相当于是在处在一个运行队列中,我们如果要找到这个队列中的某个进程的话,只需要找到这个进程的头部即可,那我们就可以对应地去调度某个进程,把这个进程所对应的代码和数据放到CPU上去执行
因为每个进程是需要去竞争CPU资源的,但是呢CPU不可能同时给这么多进程分配资源
- 所以每一个CPU都会去维护一个运行队列,里面的队头指针
head所指向就是第一个进程所对应的【PCB】,队尾指针tail所指向就是最后一个所对应的【PCB】。所以我们要运行某一个进程只需要将 head 所指向的那个进程放到 CPU上去运行即可
提问:一个进程只要把自己放到 CPU 上开始运行了,是不是一直要到执行完毕,才把自己放下来?
- 不是,每一个进程都有一个叫做:时间片的概念!其时间大概是在10 ms左右。所以并不是一个进程一直在执行,而是这多个进程在一个时间段内所有代码都会被执行—— 这就叫做【并发执行】
- 所以呢这就一定会存在大量的进程被CPU放上去、拿下来的动作—— 这就叫做【进程切换】
所以呢我们不要拿自己的时间感受去衡量CPU,其运行一遍速度是非常快的,你根本感受不到这种进程切换的效果
阻塞状态
- 阻塞就是进程因等待某种条件就绪,而导致的一种不推进状态(比如等待 键盘输入)。
- 通俗的来说阻塞就是进程卡住了,原因就是缺少资源
比如在我们日常生活中,常常发生堵车,原因就是道路资源不够用了,车辆这个进程就需要原地等待
那么进程需要什么资源呢?
- 比如
磁盘、网卡、显卡等各种外设 - 假设你现在想在
steam上下载游戏,当你点击下载按钮后提示磁盘空间不足,此时是无法运行steam下载这个进程的,因为此进程需要等待足够大的磁盘资源 - 此时我们就称此
进程为阻塞状态
总结:进程 阻塞 就是不被调度
- 此时PCB(task_struct)就会被设置为 阻塞状态,并链入等待的资源提供的等待队列。
- 没错,这里的等待队列 类似于 CPU 运行队列
挂起状态
挂起(阻塞挂起):
- 当
CPU资源紧张时,将进程的数据和代码交换至磁盘中挂起,此时内存中只有PCB- 挂起可以看作一种特殊的 --阻塞状态
详解:
- 当计算机资源比较吃紧时,操作系统一定要确保自身不会因为资源的紧张而崩溃,所以就会将一些等待资源(阻塞)的进程的代码和 数据交换到磁盘的swap分区中,这个过程称为唤出。
- 当需要调度此进程时,就会将磁盘的swap分区中保存的内容换回到内存中,这个过程称为唤入。
注意:交换的是进程的代码和数据,不是PCB!!如果PCB被交换出内存了,那操作系统如何管理呢?
所以当某个进程的代码和数据不在内存中,而被换出到磁盘上时,该进程就为挂起状态。
思考:swap分区是越大越好么?
磁盘本质是输入输出设备,每次唤入唤出其实都是非常低效的操作,如果swap分区设置的过大,那么操作系统就会十分依赖它,导致出现更多低效IO,这本身就是一种牺牲效率来确保操作系统能够正常运行的行为。
所以swap分区不宜设置的过大,一般为内存大小或内存大小的一半,具体要看不同的操作系统
举个--挂起-- 例子 :
在我们生活中,一边走路一边玩手机很危险,所以此时我们会将玩手机这个进程挂起,即把手机揣进兜里,然后专心执行走路这个进程
总结:
看了上面的三种基本的进程状态后我们可以来总结一下,如果要看进程是什么状态的话一般看这个进程在哪里排队
- 位于【运行队列】的话它就是处于
运行状态;- 位于【阻塞队列】的话它就是处于
阻塞状态;
四、Linux下的7种进程状态
在介绍完操作系统学科下的三种最主要进程状态后,我们对进程的状态有了基本的概念,接下去就让我们正式地来学习一下Linux 系统下7种进程状态
先来小结并回顾一下上面所学:
- 如果当前是【运行状态】,那么接下来就需要被调度运行
- 如果当前是【阻塞状态】,那就等条件就绪,等设备准备好就把当前进程投递到运行队列里,然后再被CPU调度运行
- 如果当前是【挂起状态】,要做的就是把当时换出的代码和数据重新换入,然后再把所对应的进程列入到运行队列中
以下就是关于进程的所有状态
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】
- 来看下下面的这段代码,是一个死循环去
printf打印循环语句
#include <stdio.h> #include <unistd.h> int main(void) { while(1); { printf("Hello process, pid: %d\n",getpid()); sleep(1); } return 0; }- 然后我们将下面的代码给运行起来,观察这个进程的状态时,看到其显示为
S+,不过呢读者想看到的应该是R才对
- 接下去呢,我们把代码修改一下再来看看,不使用
printf打印语句了,而且直接使用一个while(1)去做循环
#include <stdio.h> #include <unistd.h> int main(void) { while(1); { //printf("Hello process, pid: %d\n",getpid()); //sleep(1); } return 0; }- 然后我们看到,此时再运行起来时这个进程的状态就改变了,变成了
R+,这才是我们想要的【进程状态】
那有读者就要问了:为什么把
printf打印语句给去掉之后就变成这样了呢?
原因就在于printf打印语句它是属于IO流的一种,第一次因为是循环的缘故,它一直在等IO设备就绪,所以其进程状态就一直为S+,对应的即是在操作系统中的【阻塞状态】;但是当我们去掉printf这种IO流之后呢,它就是在纯纯运行,没有IO,那也就变成了R状态
这里再补充说明一下这个
S和R后面的+
- 这里的
R+代表的就是这个进程是在前台运行的,所以我们在输入任何指令后不会对其造成 任何的影响
- 那若是我们不以正常的方式去启动这个进程的话,其进程的状态就会不一样了,可以看到我在
./mytest的后面加上了一个&;那么其状态变成了R,此代表的意思就是这个进程它是运行在了【后台】的
总结:
"+"代表是前台运行,无"+"代表后台运行,后台运行时可在命令行继续输入指令并执行,但无法用ctrl+c结束,需要用kill -9 pid杀死。想要后台运行某个程序就在后面加"&",如:./test &
浅度睡眠状态 -- S
- 我们再通过一段代码来看看
#include <stdio.h> #include <unistd.h> int main(void) { int a = 0; printf("Enter# "); scanf("%d", &a); printf("echo : %d\n", a); return 0; }- 将该进程运行起来我们可以看到其是出于
S+的状态,因为【shell】此时正在等待用户的输入,这个就对应到了我们上面所讲到的阻塞状态
深度睡眠状态 -- D
除了【浅度睡眠】之外呢,还有一种叫做【深度睡眠】,它们俩呢,都是阻塞状态
- 对于浅度睡眠来说,之所以称为“浅度”,是有原因的:也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程,我们使用
kill -9 PID向这个进程发送【9号信号】,那么这个进程就被杀死了,你也可以认为被唤醒了
- 深度睡眠状态/不可中断睡眠状态/磁盘休眠状态,顾名思义,在这个状态的进程不会被杀掉,哪怕是操作系统也不行,通常会等待IO的结束。
- 例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
- 如果在这个过程中,操作系统能够杀死该进程,那么就有可能丢失 数据
停止状态 -- T
好,接下去呢我们来讲讲【停止状态T】
好,接下去呢我们来讲讲【停止状态T】
kill -l- 此处我们要使用到的是18、19号信号
- 接下去我们就来试一试如何让这个进程暂停之后又重新启动会是怎样的
#include <stdio.h> #include <unistd.h> int main() { while(1) { printf("hello my process: %d\n",getpid()); sleep(1); } return 0; }所以我们来总结一下
- 暂停进程
kill -19 PID- 启动进程
kill -18 PID所以呢,如果我们要将一个进程给终止的话,发送19号信号即可,要让其继续启动起来,则发起18号信号
那我现在要问了,这个T停止状态和S睡眠状态有什么不同呢?
- stopped状态 进程完全暂停了, 其不会再接收任何信号了
- 一个进程通过 stopped 状态可以控制另一个
- S 和 D 一定是在等待某种资源,而T状态可能在等待某种资源,也可能被其他进程控制
进程跟踪状态 -- t
接下去呢,我们再来说说进程的跟踪状态t,还记得我们在基础篇中所学习的 GDB 调试 吗?
- 首先我们在正常状态下查看这个进程的状态,其为
S睡眠状态。接下去呢我进行了【gdb】调试,在行内打上断点后,输入r运行起来后,我们再去查看这个进程的状态时,就发现其状态变成了t,原因就在于这个进程在调试的时候停了下来
死亡状态 -- X
对于【死亡状态X】来说呢,这个状态只是一个返回状态,你不会在任务列表里看到这个状态
- 第一种方法就是向这个进程发送9号信号,就可以杀掉这个进程
kill -9 PID- 第二种方法就是通过这个进程的名称来杀掉它
killall 进程名僵死状态Z —— 两个特殊进程
接下去我们来介绍一种状态叫做【僵死状态Z】,对于这个状态,我们要涉及到两个特殊的进程叫做僵尸进程与孤儿进程
① 僵尸进程
首先我们要来介绍的是僵尸进程,这里呢通过一个故事来进行引入
你呢,很喜欢晨跑🏃,这一天早晨6点又起来跑步了,当你路过一个公园的时候,遇到了一个晨练的【程序员】,边跑边掉头发😀 但是呢,跑着跑着,此时突然他就“啪叽”倒了下来。那你此时就非常担惊受怕了,马上拨打了110和120的电话,然后守在他的身边等待救援来到,周边的人看到也都纷纷围了过来,其中不免有一些人会紧急救援。不过等了一会这个人就没了呼吸🖤
过了十几分钟后,救护车和警车都来了,警察先封锁了,让法医过来检验一下其状况,就说:“这个人已经没救了,赶紧通知家属准备后事吧~。
好,我们回归正题,来说说这个【僵尸进程】
- 因为在救护车来之前这个人其实就已经死亡了,但是其状态还没有被检测,当前并不知道它的死因,所以我们操作系统就可能会维护当前的这个状态,这个状态即为
Z状态,即[僵死状态]
那我现在想问了:有一个进程暂时退出了,它要将它的状态暂时维持一段时间,问题是它维持给谁看呢?
答:父进程!当一个进程退出的时候,那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程fork出来,此时呢它突然挂掉了,那么此时父进程就必须去关心一下对应的子进程退出时的原因
就上面这样生冷的文字来叙述还不太行,接下去我们通过实际的案例来观察一下
#include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 6 int Test1() 7 { 8 pid_t id = fork(); 9 if(id < 0) 10 { 11 //创建失败 12 printf("创建fork失败!\n"); 13 return 1; 14 } 15 else if(id == 0) 16 { 17 //子进程暂停3s后终止 18 printf("I'm child process, "); 19 printf("pid:%d ", getpid()); 20 printf("ppid:%d \n", getppid()); 21 sleep(3); 22 return 0; 23 } 24 else{ 25 //父进程死循环 26 while(1) 27 { 28 printf("I'am parent process, "); 29 printf("pid: %d ", getpid()); 30 printf("ppid: %d\n", getppid()); 31 sleep(1); 32 } 33 return 0; 34 } 35 } 36 37 38 39 40 int main() 41 { 42 Test1(); 43 return 0; 44 }- 运行起来可以看到,在一开始的 3s 内,父子进程还是同时在跑的,但是当子进程在循环结束后退出了。不过此时呢父进程还在运行并没有退出。所以我们在右侧查看这两个进程的状态时间就发生了一定的变化
- 仔细地来看一下这个状态的变化,其中【PID】为2815,其父进程为2814,一开始它们均为
S 睡眠状态,但是呢到了后面子进程的状态就变成了Z僵死状态。也就意味着此时子进程已经死亡了,但是呢父进程还不知道 - 这里还可以通过右侧的这个
<defunct>这个来看,它表示【失效的、不再存在的】
所以我们总结一下:
💬 进程一般退出的时候,一般其不会立即彻底退出。如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,这也是为了方便后续父进程读取子进程的相关退出结果
那对于上面的这种子进程,我们就将其称作为是【僵尸进程】,不过呢这种进程是存在一定危害的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为 数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
- 那么对于上面的这种危害我们就称作为是【内存泄漏】,要如何去进行避免呢,之后的文章会做讲解~
② 孤儿进程
- 上面我们讲到,当一个子进程突然退出但是父进程并没有去主动回收的话,那么此时这个子进程就会变成【僵尸进程】
- 那看到下面我们将这个进程突然终止之后,父子进程都不见了
因为当前这个进程是bash的子进程,当其一退出之后,bash就将其回收了。可是这个子进程为什么也没了呢?这个的话我们就要聊聊【孤儿进程】了
接下去我们来将上面测试僵尸进程的代码做个修改,让父进程先于子进程退出
#include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 6 int Test1() 7 { 8 pid_t id = fork(); 9 if(id < 0) 10 { 11 //创建失败 12 printf("创建fork失败!\n"); 13 return 1; 14 } 15 else if(id == 0) 16 { 17 //子进程进入死循环 18 while(1) 19 { 20 printf("I'm child process, "); 21 printf("pid:%d ", getpid()); 22 printf("ppid:%d \n", getppid()); 23 sleep(1); 24 } 25 return 0; 26 } 27 else{ 28 //父进程暂停3s后终止 29 printf("I'am parent process, "); 30 printf("pid: %d ", getpid()); 31 printf("ppid: %d\n", getppid()); 32 sleep(3); 33 return 0; 34 } 35 } 36 37 38 39 40 int main() 41 { 42 Test1(); 43 return 0; 44 }- 然后通过进程状态追踪来看看,我们观察到一开始两个父进程和子进程是同步在走的,但是呢过了一会父进程终止了,只有子进程还在跑,此时我们需要留意的不是其进程状态,而是这个子进程的
PPID即父进程的PID,它由原先的【22677】变成了【1】,这是为什么呢?
- 这里的话就要给读者普及一下了,对于
PID的值为1的进程,我们一般将其称作为是【系统进程】。我们可以使用下面这句指令去查找一下
ps ajx | head -1 && ps ajx | grep systemd- 然后我们看到这个【系统进程】的
PID即为1
那为何会出现上面这种现象呢?是这个子进程突然换父进程了吗?
对于父子进程来说,如果父进程先于子进程结束的话,那么这个子进程就会被称作为是【孤儿进程】,对于孤儿进程来说呢,它会有1号进程即【系统进程】所领养,因为此时这个进程没有了父进程了,所以需要有人去带领它
但是这个孤儿进程为什么要被领养呢?
- 其实很简单,既然它是一个进程的话,最终都是要退出的,但是没了父亲的话就只能由系统进程来进行维护了
五、总结与提炼
在本文中,我们主要讲解了两个大点,一个是在操作系统中所常见的一些进程状态,它们分别为:运行状态、阻塞状态、挂起状态。
- 所被调度的、处于运行队列里的这些进程所处的状态我们称之为
运行状态R - 处于等待队列中,同时在等待外设相应的进程所处的状态我们称之为
阻塞状态R - 当把一个进程的 数据和代码都重新交换到外设当中,进程所处的状态我们称之为
挂起状态R
接下去我们又介绍了在Linux系统下的七种进程状态,分别是:运行状态R、浅度睡眠状态S、深度睡眠状态D、停止状态T、进程跟踪状态t、死亡状态X、僵死状态Z
- 当一个进程没有在等待IO流的时候,其就会处于运行状态R
- 使用sleep()函数可以很好地使一个进程处于浅度睡眠状态S
- 如果不想让一个进程在等待磁盘操作的时候被操作系统杀掉,则可让其处于深度睡眠状态D
- 可以向一个进程发送【19】号信号使其暂停,处于停止状态T继续发送【18】号信号的话则可以使其重新启动
- 在【gdb】的环境下去运行一个断点的话则可以使其处于进程跟踪状态t
- 使用kill -9 PID就可以杀掉一个进程,使其处于死亡状态X
- 如果让一个子进程在父进程不知晓的时候退出,那么其就会处于僵死状态Z,变为【僵尸进程】;若是在父子进程中父进程先于子进程退出的话,那么这个子进程就会变成【孤儿进程】
