一、前言
Hello,大家好,本文我们所要介绍的是有关Linux下的进程状态
- 在上一文中,我们重点介绍了有关 Linux下进程的基本概念,了解了什么是进程、怎么去描述并组织进程、创建一个进程。
- 在本文中,我们将先通过了解操作系统学科下的进程状态,对进程的状态有一个基本的概念,然后呢再去学习Linux下的7种进程状态,学习这PCB结构体中的第二个成员变量
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- ==状态: 任务状态,退出代码,退出信号等。==
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
二、操作系统学科下的进程状态
- 对于进程而言呢,它是操作系统中的概念,如果有学习过《操作系统》这门学科的话应该可以很清楚对于进程而言的话是存在着许许多多的状态,如果一开始刚刚接触的小伙伴一定会感觉这么多状态要怎么区分呀😵
其实那么多的状态,真正主要的也就那么几个,所以接下去我会中重点讲解以下几种进程的状态
1、运行状态
首先我们要谈到的是【运行状态】,这个状态是最普遍的
- 首先对于一个进程而言,我们知道它是由 内核数据结构 + 所对应的代码和数据 所组成的,所以当系统中存在多个进程的时候就势必会存在多个结构体;当然我们需要将这些进程给链接组织起来
- 那么这些进程就相当于是在处在一个运行队列中,我们如果要找到这个队列中的某个进程的话,只需要找到这个进程的头部即可,那我们就可以对应地去调度某个进程,把这个进程所对应的代码和数据放到CPU上去执行
💬 因为每个进程是需要去竞争CPU资源的,但是呢CPU不可能同时给这么多进程分配资源
- 所以每一个CPU都会去维护一个运行队列,里面的队头指针
head
所指向就是第一个进程所对应的【task_struct】,队尾指针tail
所指向就是最后一个所对应的【task_struct】。所以我们要运行某一个进程只需要将 head 所指向的那个进程放到CPU上去运行即可
- 但是CPU无法直接去找到某个需要调度的进程来运行,此时呢就需要一种东西叫做【调度器】,CPU通过找到运行队列,运行队列找到调度器,然后调度器再去一一调度所需要运行的进程,那么此时所被调度的、处于运行队列里的这些进程所处的状态我们称之为 ==运行状态R==
提问:一个进程只要把自己放到CPU上开始运行了,是不是一直要到执行完毕,才把自己放下来?
- 不是,每一个进程都有一个叫做:时间片的概念! 其时间大概是在10 ms左右。所以并不是一个进程一直在执行,而是这多个进程在一个时间段内所有代码都会被执行 —— 这就叫做【并发执行】
- 所以呢这就一定会存在大量的进程被CPU放上去、拿下来的动作 —— 这就叫做【进程切换】
💬 所以呢我们不要拿自己的时间感受去衡量CPU,其运行一遍速度是非常快的,你根本感受不到这种进程切换的效果
2、阻塞状态
在介绍完【运行状态】后,我们再来讲讲【阻塞状态】
- 对于阻塞状态而言,其实就是 ==进程等待某种资源就绪的过程==,例如你当前的这个进程向操作系统发起了IO请求,那么当前这个进程就会处于暂停状态,无法继续运行,但是这样看还是太比较难理解,举两个例子来说明一下
- 如果你的网络突然断了,进程无法继续进行下载的任务,此时它只能靠边,CPU便去跑其他进程了,等这个进程就绪了才继续来跑这个进程
- 去银行的柜台办存钱,但到了你的时候突然发现单子还没有打印好,于是只能在一旁进行等待
💬 上面的这两种状态都可以算作是进程处于阻塞状态
- 之前我们在讲 操作系统基本概念 的时候,有说到过操作系统要去管理底层的硬件的话需要进行【先描述,再组织】的动作,那就是将这一个个生冷的硬件抽象成为结构体类型,内部的
type
代表这是一个什么类型的硬件、status
代表这个硬件当前在操作系统中所处的状态是什么样的,wait_queue
代表的则是等待队列的指针
- 那我们继续去谈起操作系统中的【进程】,现在呢有一个进程想要去读取键盘中的数据,那么它就需要与我们刚才所描述的结构体相互勾连起来,因为操作系统根据这个结构体顺着体系结构去访问到最底层的硬件需要实现,所以这个进程便需要一直处于等待状态,我们可以将其链入【等待队列】中,即我们刚才所讲的
wait_queue
💬 那有同学可能会疑惑这为什么叫做【等待队列】呢?明显只有一个进程鸭🦆
- 一个进程当然不能算是队列,若此时又有一个进程也要来读取键盘中数据的话,我们就需要将其链入到这个等待队列中。此时这两个进程所处的状态即为 ==阻塞状态==
- 那么当键盘里一旦有数据了,我们只需要把这个进程放到我们在上面所讲的【运行队列】中即可,那么CPU在调度的时候就可以自动地到底层设备里去读取了;如果当键盘里没有输入的时候,最终我们的进程就只能在每一个各自的等待队列里去等待,把这种处于等待队列的进程称之为 ==阻塞状态==
3、挂起状态
最后我们再来讲讲一种状态叫做【挂起状态】
- 对于操作系统而言,我们知道它要为当前的所有进程分配内存资源,在上面我只画了一个等待队列,而且只有两个进程,若是一个等待队列中存在多个进程,并且通过有多个等待队列在排队的话,操作系统中的内存资源就会出现【消耗殆尽】的问题
- 那么操作系统就得保证在正常情况下省出来内存资源
要怎么去省呢?
- 只要一个进程没有被调度(运行),那么当前这个进程的 代码和数据 就是空闲的、没有被使用的。于是操作系统就想办法把这个进程内核的PCB给保留,然后将其 代码和数据 重新交换到外设当中【换出】,那么此时只有一个PCB在这里排队,我们把上面这种状态就称之为 ==挂起状态==
- 当这个进程就绪了,把这个进程放到运行队列时,此时再考虑把 代码和数据 重新放回进程中【换入】
💬 那有的同学说:为什么要这样去做呢?这样做有什么意义?
- 答:当一个进程所对应的代码和数据交换到外设上时,操作系统内部就腾出了相应的空间,此时OS就可以将这块多出来的空间重新给到其他需要的进程使用,从而达到了循环利用系统资源的目的,节省了系统开销。
看了上面的三种基本的进程状态后我们可以来总结一下,如果要看进程是什么状态的话一般看这个 进程在哪里排队
- 位于【运行队列】的话它就是处于
运行状态
;- 位于【阻塞队列】的话它就是处于
阻塞状态
;
三、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 */ };
1、运行状态R
首先我们要来聊的是【运行状态R】
- 来看下下面的这段代码,是一个死循环去
printf
打印循环语句
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 while(1); 7 { 8 printf("hello bit\n"); 9 } 10 11 return 0; 12 }
- 然后我们将下面的代码给运行起来,观察这个进程的状态时,看到其显示为
S+
,不过呢读者想看到的应该是R
才对
- 接下去呢,我们把代码修改一下再来看看,不使用
printf
打印语句了,而且直接使用一个while(1)
去做循环
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 while(1); 7 //{ 8 // printf("hello bit\n"); 9 //} 10 11 return 0; 12 }
- 然后我们看到,此时再运行起来时这个进程的状态就改变了,变成了
R+
,这才是我们想要的【进程状态】
💬 那有读者就要问了:为什么把printf
打印语句给去掉之后就变成这样了呢?
- 原因就在于
printf
打印语句它是属于IO流的一种,第一次因为是循环的缘故,它一直在等IO设备就绪,所以其进程状态就一直为S+
,对应的即是在操作系统中的【阻塞状态】;但是当我们去掉printf
这种IO流之后呢,它就是在纯纯运行,没有IO,那也就变成了R
状态
这里再补充说明一下这个
S
和R
后面的+
- 这里的
R+
代表的就是这个进程是在前台运行的,所以我们在输入任何指令后不会对其造成 任何的影响
- 那若是我们不以正常的方式去启动这个进程的话,其进程的状态就会不一样了,可以看到我在
./myproc
的后面加上了一个&
;那么其状态变成了R
,此代表的意思就是这个进程它是运行在了【后台】的
不过呢,R状态并不代表这个进程就在运行,而代表其在运行队列中排队而已
2、浅度睡眠状态S
接下去我们再来介绍一下Linux下的睡眠状态S
- 下面我们要运行的代码是每隔一秒打印一句内容
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 while(1) 7 { 8 sleep(1); 9 printf("hello bit\n"); 10 } 11 12 return 0; 13 }
- 运行起来后可以看到这个进程所处的状态即为
S+
睡眠状态
💬 那有同学就很疑惑,这个进程不是在运行吗?为什么不是R
状态呢?
- 我们可以通过动图来观察看看,每一次去查看这个进程的状态时,都是处于
S+
这个睡眠状态。其实呢,这和外设与CPU的速度之差是有关系的,CPU那个运行速度是极其快速的 - 虽然我们肉眼可见右侧的进程是一直在每隔一秒运行,但是呢在我们查进程状态的时候看到的却只能是
S+
;因为有99.99%
的时间都在进行等待,而只有0.01%
的时间在运行,它们之间简直可以说是数量级别的差别
- 不过呢,上面这样还无法看出一个进程是否真正地处于睡眠状态,我们再通过一段代码来看看
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 int a = 0; 7 printf("Enter# "); 8 scanf("%d", &a); 9 10 printf("echo : %d\n", a); 11 return 0; 12 }
- 将该进程运行起来我们可以看到其是出于
S+
的状态,因为【shell】此时正在等待用户的输入,这个就对应到了我们上面所讲到的 ==阻塞状态==
- 不仅如此,像我们一直在使用的
bash
,也可以算是一种【阻塞状态】,一直等待着我们去输入命令行,一旦有了的话它就进行读取