本篇详细的讲解了 Linux 中进程会出现的各种状态,以及出现这些状态的原因,其中进程的阻塞、挂起和运行就是进程状态的体现。接着科普了一下进程的切换,然后讲解了进程的优先级,以及如何调整进程的优先级。最后对进程的特点进行了总结。
目录如下:
目录
1. 进程状态
1.1 R 状态与 S 状态
1.2 T 状态与 t 状态
1.3 D 状态
1.4 Z 状态与 X 状态
1.5 孤儿进程
1.6 进程的阻塞、挂起和运行
2. 进程切换
3. 进程的优先级
3.1 优先级存在的意义
3.2 优先级的特点
4. 进程的特点的总结
1. 进程状态
对于 Linux 中的进程,他们存在各种各样的状态,比如正在运行 Run,在休眠状态 Sleep,或者说是已经停止 Stop,其实这样的一些状态也就只是 task_struct 内部属性中的一个变量,用该变量的值来表示当前进程的状态。在进程中存在如下的状态:
static const char* task_state_array[] = { "R (runing)", "S (sleeping)", "D (disk sleep)", "T (stopped)", "t (tracing stop)", "X (dead)", "Z (zombie)" };
如上的进程状态中,我们只有 D、X 状态无法使用操作来验证,但是其余的操作都可以使用操作来验证。
1.1 R 状态与 S 状态
R 状态与 S 状态分别标识运行状态与休眠状态,我们将使用如下代码来对我们这两种状态进行区分,如下动图:
(运行状态就是一直在执行进程,而休眠状态就是CPU什么都不干,没有执行进程)
如上图所示,当我们写了一个无限循环的打印代码,在左框图中查询相关的状态,状态表示:S+,当我们在将打印代码给注释掉了之后,发现状态变为了 R+ 状态。
关于如上的现象,为什么屏幕一直在打印信息,状态会显示为 S+ (休眠)状态呢?这是因为当我们需要打印信息的时候,是向屏幕上打印出信息,而屏幕上打印信息也就需要屏幕与内存相交互,需要CPU执行过后将数据刷新到内存中,然后屏幕在从内存中拿出数据打印,但是其中存在很大的速度间隔,比如CPU执行完这条数据只需要 1ns,而屏幕从内存中拿出数据却需要 1ms 的实践,所以很多的时候都是CPU在等待屏幕将内存中的数据取完在执行,所以我们在查询状态的时候,有 99% 的可能都是查询到 S 状态,所以我们可以得出关于 S 状态其实是 CPU 一直在等待执行进程的状态,同时也是进程在等待“资源”就绪的状态,因为只有进程资源就绪,才会继续运行。同时这也对应了为什么我们将打印代码给注释掉之后,查询到的进程状态为 R 状态,因为CPU一直在运行这个进程,期间并没有等待。
那么为什么我们的 S 状态后还跟着一个 “ + ” 呢?这是因为当前进程是在前台运行,当当前进程在后台运行的时候,这个 “ + ” 号也就消失了,想让程序在后台执行,我们只需要在执行文件的指令后面加上一个 & 符号即可,如下:
如上操作,当前进程的状态就从 S+ 变成了 S 状态,关于前台后台运行状态的区别,将会在后面给出。(后台运行的进程不能用 ctrl + c 终止进程,只能用 kill -9 pid)
1.2 T 状态与 t 状态
现在将进入 T 状态,关于 T 状态就是将当前运行的进程停止运行,进程变为等待运行状态,如下:
当我们使用 kill -19 pid 的时候,我们就将该进程给暂停掉了,当我们使用 kill -18 pid 的时候,又将进程给再次启动了。当程序进入暂停状态的时候,程序就不会在继续运行了,只有当我们使用指令将其唤醒,才会继续运行。
那么当我们在对程序进行调试的时候,进程又是处于什么状态呢?,如下所示:
当我们使用 gdb 对程序进行调试的时候,状态从 S 状态变为了 t 状态,t 状态叫做追踪暂停,也就是当我们进行调试打断点的时候,就会出现 t 状态,运行到断点处暂停下来,就是 t 状态。
1.3 D 状态
D 状态是 Linux 系统中特有的一个状态,是一种特殊的休眠状态。由于这个状态我们并不能掩饰,所以用例子来给出:
当我们的进程想要调度磁盘给我们写入大量数据的时候,磁盘接收到了这个消息,然后磁盘便开始向内写入数据,提出申请的进程便进入了 S 状态。但当前的内存中的空间十分的紧张,内存出现严重不足的情况,Linux 操作系统此时有权杀掉内存中的部分进程来腾出空间。假设这时刚好将等待磁盘给出回应的进程给删除了,当磁盘写入数据失败的时候,会给进程一个响应,是再一次写入还是直接丢弃。但此时进程都已经被杀掉了,磁盘根本找不到进程,然而其他进程又需要使用磁盘,此时磁盘就将刚刚写入的数据给丢弃了,这份数据就丢失了,也就造成了数据丢失的事故。
所以为了防止这样数据丢失的情况,也就出现了 D 这种状态,这种状态被叫做深度睡眠(不可中断睡眠)。结束这种状态只有两种方法:自己醒来或者重启。
1.4 Z 状态与 X 状态
Z 状态下的进程我们也叫做僵尸进程,出现这种的情况的原因是,当我们的子进程运行完自己的程序后,按道理应该将子进程的数据代码以及在操作系统中的 task_struct 给删除的,但是此时父进程并没有将子进程给回收,没有被回收也就意味着进程的数据代码从内存中删除了,但是内核数据结构 task_struct 还在操作系统中,并没有被释放掉。如下:
如上所示,当我们的子进程结束后,而父进程还未结束,这就会导致子进程进入僵尸进程。关于僵尸进程,并不是会一直处于僵尸进程,也是会被回收的,task_struct 进程中记录着自己退出的信息,未来让父进程读取。但是也会存在一些情况,父进程异常结束,那么子进程的僵尸进程会一直存在。
当我们的僵尸进程未来被 waitpid 接口读取了,这个进程就会由 Z 状态变成 X 状态,就会被回收了。
1.5 孤儿进程
孤儿进程,就是当父进程已经退出,但是子进程还并没有退出,这时候的子进程就是孤儿进程。如下所示:
如上图所示,当我们的父进程运行结束后,子进程的 ppid 变为了 1,也就是说子进程的父进程改变了,这是为什么呢?这时因为,当父进程先退出,而子进程并未退出,也就意味着子进程未来不会被回收,会变成僵尸进程,但是操作系统为了防止这种情况发生,就自己将孤儿进程领养了,用来保证子进程被正常的回收。
通过以上也可以得出,只有在父进程结束的时候,才会回收子进程,但是我们平时在命令行运行的程序,为什么不会出现僵尸进程的情况呢?这是因为命令行启动的进程的父进程是 bash,bash 会自动的回收新进程的僵尸进程。
1.6 进程的阻塞、挂起和运行
首先我们先介绍进程的运行状态,也就是 R 状态(上面所讲的为在 task_struct 中的出现的状态),现在我们将从操作系统层面讲解运行状态。如下图:
在操作系统中,每个CPU都有着一个对应的运行队列,当我们需要执行某个进程的时候,就是将该进程对应的 task_struct 进入运行队列,然后将对应的代码和数据传入到CPU中进程运行,此时只要进程在运行队列中,那么该进程就是处于运行状态。
关于进程一旦持有CPU,CPU不会直到将该进程运行结束才运行下一个进程,这是因为在内核数据结构 task_struct 中有着一个时间片,记录着该进程最多被CPU运行多少时间,当时间到了的时候,就会将该进程给列入到队尾,运行下一个进程,循环往返。这样让多个进程以切换的方式进行调度,在一个时间段内得以同时推进代码的方式,叫做并发。(这是调度算法的一种, Linux 中不是这样调度的)
关于我们进程的阻塞状态,应该对应于以上进程的哪种状态呢?就比如当我们需要从键盘中读入数据的时候,只有读完数据之后,才能继续运行我们的进程,所以等待键盘读入是一种阻塞,如下:
如上所示,当我们需要从键盘中读入一个数据的时候,左边的检测窗口检测出进程的状态为 S 状态,所以 S 状态属于一种阻塞态。(其实 D 状态也是阻塞态,但是我们不能演示出来)
关于阻塞状态的底层实现:其实对于各种设备的管理,也是使用数据结构将其描述组织起来的,该数据结构中包括当前设备的类型、状态、已经对应的设备结构体指针(用于将各个设备给链接起来)等等,还有一个等待队列,这个等待的队列的作用是,当某些进程需要从设备中读入数据的时候(比如使用 sancf 函数从键盘读取设备,在磁盘中给进程读入数据),操作系统就会将正在运行队列中的 task_struct 从运行队列中剥离,然后将其放入到设备的等待队列中,等到从设备中读取数据成功之后,才将 task_struct 放回到运行队列中。所以对于以上这种情况的状态,就叫做阻塞状态。
最后是我们的挂起状态,如下图:
在磁盘中存在一个 swap分区,当我们的内存特别吃紧的时候,操作系统会将处于 S 状态,也就是处于阻塞状态的进程的代码和数据放入到swap分区中去,然后腾出空间给其他需要的进程,但是这样并不是将一个进程给删除了,因为该进程的 task_struct 还在操作系统中,只是将他的数据放入到了磁盘中去,这个操作叫做换出,当阻塞状态结束的时候,在将进程的代码和数据换入回内存中。这种状态叫做阻塞挂起态。其实关于以上这个将暂时不需要的数据放入到磁盘中,需要的时候在将数据从磁盘中换入的状态就叫做挂起态。
关于这样的挂起态,其实会导致一定的效率问题,因为这样不断的换入换出,会花部分时间在 IO 写入和写出中,所以对于我们的 swap分区来说,一般并不会有太大的空间,因为太大空间会导致 OS 依赖该空间,频繁的用该空间挂其进程,导致效率变低。
2. 进程切换
关于我们进程的切换,CPU 正是通过频繁地对不同的进程进行运行,来达到在用户层面上很多进程同时进行的现象,所以说在 CPU 内进程会频繁的进行切换,当在进程切换的时候,有可能是因为当前进程已经运行结束,也有可能是当前进程的时间片已经结束,该下一个进程的运行了。
当一个进程的时间片结束运行下一个进程的时候,我们需要将进程进行切换,但当前这个进程并没有运行结束,而是运行了部分,所以我们需要将未运行结束的数据给保存起来,然后当下一次运行到这个进程的时候,我们将之前保存的数据在调出来,然后继续的运行。在 CPU 中用于存储数据的元件叫做寄存器,每个CPU只有一套的寄存器,这些寄存器中的临时数据叫做上下文数据,当我们需要切换的时候,将换走进程的数据放入的 task_struct 保存起来,将换入进程的数据从 tast_struct 中读入。
3. 进程的优先级
上文中所讲的进程分别进入到 CPU 的运行队列中,运行队列按照先进先出的方式,分别按顺序地运行不同的进程,不同进程在运行队列前的排队,其本质就是确定获取 CPU 资源的顺序,所以进程之间的优先级就是获取某种资源的先后顺序。
在内核数据结构 task_struct 中存在一个内部字段 --> int priority = ?? ,表示当前进程的优先级,其中 priority 数值越小,优先级越高。
3.1 优先级存在的意义
对于大多的普通的电脑设备而言,CPU 只有一个,也就是一段时间内只能运行一个进程,所以说进程访问资源(CPU)始终都是有限的。所以说需要对进程之间创建一个优先级,决定哪些进程先运行,提高运行队列。
操作系统中关于调度和优先级的原则:分时操作系统,分时间片对不同的进程进行运行,这样可以维持基本的公平。若进程因为长时间不被 CPU 调度,就造成了进程饥饿的问题。
3.2 优先级的特点
在 Linux 中,我们使用指令:ps -al,查看当前所有运行的进程的优先级,如下:
PRI:进程优先级;
NI:进程优先级的修正值,nice值;
进程优先级 = 优先级 + nice值,只要修改 nice 值就可以达到对进程优先级修改的过程。那么我们使用什么指令对进程的优先级做调整呢?
我们在命令行中输入 top 指令,然后输入 r 指令,输入我们要调整优先级的 pid,然后在输入我们要调整的 nice 值,如下:
如上所示,当我们进行如上步骤时,先将 nice 值调整为100,我们查看时,也只是发现调整的 nice 值为19,优先级从80变成了99,当我们将 nice 值调整为-100的时候,也只是发现nice值变成了-20,从99变成了60,所以对于我们nice值的范围是-20~19,最多只能将 niec 调整为19,最低调整为-20,默认进程的优先级从80开始调整。
4. 进程的特点的总结
进程还存在一些其他的特点,如下:
1. 并发与并行:关于并发,就是多个进程在一个CPU下采用进程切换的方式,在一段时间内(也就是基于时间片来运行程序),让多个进程都得以推进。对于并行,就是一个电脑中存在多个CPU,多个进程在多个CPU下分别,同时的运行。
2. 进程的独立性:多个进程的运行,每个进程都是相互独立的,都独享着自己的各种资源,运行期间不会相互干扰。(某个进程运行出现异常,也不会影响到其他进程)
3. 竞争性:系统的进程数目较多,而CPU资源只有少量(通常为1个),所以进程之间是具有竞争属性的,为了高效的完成任务,更合理竞争相关资源,就出现了进程的优先级。