humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


Linux系统 2023 年 3 月 3 日

[Linux] 进程状态相关概念、Linux实际进程状态、进程优先级

生成进程之后, 操作系统会给进程分配其所需要的资源。而为了整个操作系统的稳定和效率, 操作系统会对进程进行调度、管理, 在调度管理的过程中进程会存在不同的状态, 这些不同的状态就被称为进程状态。

进程状态的概念

当程序运行时, 操作系统会生成相应的PCB与程序的代码、数据一起加载到内存中 生成进程.
生成进程之后, 操作系统会给进程分配其所需要的资源。而为了整个操作系统的稳定和效率, 操作系统会对进程进行调度、管理, 在调度管理的过程中进程会存在不同的状态, 这些不同的状态就被称为进程状态。
那么从宏观角度, 而不从实际角度来说, 进程一般存在多少种状态呢?

在描述进程在操作系统中的各种状态之前, 先了解一些模糊的不具体的概念:
  1. 进程 = PCB + 程序代码、数据

  2. 操作系统会对进程生成PCB这样描述进程所有属性的结构体, 而为了方便管理, 操作系统也对所有硬件, 例如:CPU、磁盘、显卡、声卡、网卡……描述了他们的所有属性, 生成类似PCB这样的结构体 用于对硬件设施的管理。即, PCB是描述进程所有属性的结构体, 而操作系统中也存在 描述CPU、磁盘、显卡等硬件的所有属性的结构体

  3. 在描述硬件属性的结构体中, 一般都会存在一个用于给进程分配资源所存在的队列, 当进程需要某种资源时, 如需要在显示器上显示内容, 那就需要显卡资源, 此时操作系统就会将此进程的PCB移动到系统描述的显卡结构体的等待队列中。

    也就是说, 在操作系统这里, CPU、磁盘、显卡等硬件都存在一个描述其属性的数据结构, 且此数据结构中都存在一个给进程分配资源的队列, 当进程需要某种硬件资源时, 操作系统就会将进程PCB从某队列中移出转移到另一种进程所需资源的队列中

  4. 硬件分配资源的队列, 此队列遵循队列的先进先出的规则, 进程在此队列中可以看作是在排队, 只有排到的进程才能被硬件赋予资源

  5. 硬件处理速度是非常快的, 虽然总说磁盘之类的硬件速度很慢, 但也是相对CPU的处理速度说的, 即使是磁盘的处理速度也可能是快到看不出的, 所以进程状态的变换是非常快的, 是肉眼不可见的, 除非其不变换状态

这些模糊的概念是为了方便对于进程状态的理解所写

1. 运行态

看到运行态, 许多人可能第一时间想到的是:只要程序运行起来了, 那其进程就在运行态. 其实不是的, 程序被运行起来成为了进程, 程序只要没有被关闭可以说是一直在运行的, 但进程并不能如此。
上面的概念中提到, 操作系统给硬件描述了一个其分配资源用的队列。也就是说, 在操作系统中, CPU也是存在一个给进程分配资源的队列的, 一般被称为运行队列(runqueue).
那么运行态就表示, 只要进程在这个CPU的运行队列中, 就成这个进程在运行态。无论这个进程是在排队, 还是在接受CPU的资源。

2. 进程终止

进程终止的理解更为简单
进程终止并不是指 程序运行结束, 进程已经从内存中释放, 而是指 进程依旧在内存中, 但已经不在接受任何资源, 也不再接受调度, 永远不运行了, 随时可以被释放
进程从内存中释放之后, 就已经不能称为进程了, 更不用说此时的状态

进程明明已经不运行了, 为什么不直接将其释放, 而是要存在一个终止态?

这个问题的原因之一就是:
释放进程资源也是需要消耗成本、资源的!假如计算机内存充足, 但此刻操作系统已经非常繁忙了, 难道操作系统要停下现在正在忙的事情, 而转过头来消耗资源释放一个已经不再运行的进程吗?显然是不太合理的

3. 进程阻塞

进程阻塞, 也就是进程的阻塞态。听起来可能像是进程堵住了, 其实实际上也类似堵住了这种情况
那么究竟什么是进程阻塞?
一般来说, 一个进程是不会仅仅只需要一种资源的, 一个进程可能需要申请多种资源, 比如:看视频除了需要CPU资源, 还需要显卡资源、网卡资源、声卡资源等。
那么如果一个进程正在申请CPU资源 在运行队列中排队, 但是CPU此时非常的忙, 一时半会没办法给这个进程资源。刚好这个进程也需要网卡或磁盘资源, 而此进程又不能再短时间内获得CPU资源, 那么操作系统就会将此进程的PCB从运行队列中拿出来, 然后将其放到需要的其他硬件资源的队列中。但是尴尬的事情又出现了, 这个队列也需要排队一时半会也不能获取相应的资源。
也就是说 此时此进程想获取CPU资源时需要长时间排队, 操作系统将它放到别的硬件资源队列中了, 发现也需要长时间排队 那么此进程就进入了阻塞状态
即, 当进程此时没有获取CPU资源的同时, 也正在等待非CPU资源, 该进程就进入了阻塞状态, 此时进程代码就不在运行了, 进程就卡住了
那么简单点来说, 当进程满足两个条件时, 此进程阻塞:
  1. 此进程PCB不在系统描述CPU结构体的运行队列里
  2. 此进程PCB在其他系统描述硬件结构体的等待队列里, 还没有获取此硬件的资源
当进程满足这两个条件时, 进程就没有获取任何资源, 在等待着获取资源, 进程代码不再运行, 此时进程就卡住了

4. 进程挂起

进程挂起 其实是一种节省内存资源的状态。它类似于阻塞, 但又不同。
进程 = PCB + 程序代码、数据, 一般来说一个进程的程序代码和数据要比PCB的大小大得多, 所以进程中更占用内存空间的一部分就是程序的代码和数据
当一个进程短时间内不会被调度 或 处于阻塞状态时, 此进程的代码可能短时间内无法运行起来, 但是此进程仍然是完全被加载到内存中的。这就导致了可能存在一些 进程代码实际上并没有运行的进程 却依旧占用着很大一部分内存空间, 如果此时内存空间并不充足, 那么操作系统就会考虑将此进程挂起
操作系统 会将进程的程序代码和数据部分 从内存中拿出来, 放到指定的磁盘空间中, 而将此进程的PCB留在内存中。这样的操作 就称为进程挂起
等到此进程代码即将运行或内存充足的时候, 操作系统会再将程序代码和数据加载到相应的内存中

因为存在进程挂起, 所以当操作系统内存不足的时候, 往往伴随着磁盘被高速访问着

进程挂起会导致磁盘中存在多份的相同程序代码和数据吗?

并不会, 实际上操作系统再将进程挂起的时候, 并不会将多一份的程序代码和数据存入磁盘中, 结束挂起时, 也不会再将存入的程序代码和数据再加载到内存中。而是直接用原程序的代码和数据进行交换。

Linux中实际的进程状态

上一部分内容, 将宏观方面的四种不具体的进程状态介绍了一下。而实际上, Linux的进程状态并不是按部就班的只是运行、终止、阻塞和挂起四种
再Linux内核源码中, 有关进程状态分类的部分是这样的:
Linux将进程的状态分为了:R(running)、S(sleeping)、D(disk sleep)、T(stopped)、t(tracing stop)、Z(zombie) 和 X(dead) 7种
其中, R 和 X 对应了运行态和终止态。而 S D T t Z 又都是什么状态呢?

1. S (sleeping) 睡眠状态

在介绍正式介绍S状态之前, 先来观察一个现象:
使用运行这段c++程序代码:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using std::cout;
using std::endl;

int main() {
	while(1) {
		cout << "I am a proess, pid = " << getpid() << endl;
	}

	return 0;
}
SleepStat
SleepStat
可以看到, 进程是运行着的, 并且右边一直在屏幕上刷着一句话"I am a process, pid = 17590"
而使用 ps 查询17590时 也可以查询的到, 但是此时系统显示 进程的状态却是 S(忽略‘+’号)
上面介绍了, 只要一个进程在CPU的运行队列中, 那么这个进程就在运行态, 在Linux中就应该处于 R 状态。而且 程序不停地在运行时可以观察得到的。那为什么系统显示当前进程的状态是 S 呢?S 究竟是个什么状态?
在上面的代码中, 确实使用了While(1)死循环, 保证了程序可以一直运行, 但是 我们还在循环内使用了cout 这个可以在电脑屏幕上输出内容的对象
向电脑屏幕输出内容需要什么资源?一定是显卡和显示器资源。以CPU的运算速度的来对比, 此进程运行的绝大部分时间一定是在等待显卡和显示器资源的, 显示器的刷新速度与CPU的运算速度根本不是一个量级的。以人的速度, 也是捕捉不到CPU运算这个进程的时刻的, 所以STAT不是 R
那既然 进程其实是在等待显卡和显示器的资源, 那其实也就说明进程在等带非CPU的资源, 那也就是指此进程实际上正处于阻塞状态
结论出来了, S (sleeping) 睡眠状态, 其实就是阻塞状态

怎么在Linux观察到R状态呢?

其实很简单, 只需要将上面代码中 cout 语句删除, 让此进程不需要使用其他硬件资源, 那就可以看到此进程在R (running)状态了:

S 状态是睡眠状态, 也可以被称为浅度睡眠。既然有浅度睡眠, 那与之对应的深度睡眠也不会缺少。

2. D (disk sleeping) 深度睡眠

D(disk sleeping) 深度睡眠状态, 其实也是阻塞状态, 不过是 特指进程在等待磁盘资源时的阻塞状态
与 S 状态不同的是, D 状态是不可中断的睡眠状态, 而 S 状态是可中断的睡眠状态
这也是为什么 S 被称为浅度睡眠, 而 D 被称为深度睡眠
S 状态是可中断的, 即可以被 kill 掉:
但 D 状态是不能被中断的, 也没有办法演示

因为 D 状态的出现, 一般都是在文件传输占用磁盘资源时, 而以现在磁盘的速度传输小的文件也不能观察到 D 状态, 更别说测试能否被中断了。如果是大文件, 博主的垃圾服务器也承受不了太大的文件

一般情况下, D 状态是不容易被看到的

只需要知道, D 是特指进程在等待磁盘资源时的阻塞状态 就可以了

为什么要有一个 D 状态?

虽然不能演示和测试 D 状态, 但是可以举个例子解释一下, 为什么要单独将 进程等待磁盘资源时的阻塞状态 特定为 D状态?
或者可以换一个问法:进程在等待磁盘资源时, 为什么不能中断? 毕竟 D状态 区别于 S状态 的地方就只是 D状态 无法中断
这个问题很好解释, 只需要举个拟人化例子就可以了:
假如, 某个银行的某台服务器在运行着某个程序, 此程序需要将程序的几个G的账单数据文件传输到磁盘上(就假设磁盘速度不快, 需要传输一段时间), 所以此程序正在以进程的形式在内存中加载着, 磁盘正在稳定的传输这届数据。
进程对磁盘说:这写数据你拿走用吧, 你传输完了把结果告诉我。然后进程就占着内存空间在这干等磁盘传输结束
这时候操作系统看了一下发现这个进程处于简单的阻塞状态其实啥都没干, 还占着这么多的内存空间, 明明操作系统的内存空间已经快没有了。这个时候操作系统就把这个进程给释放掉了。
进程被释放掉了数据还没拿回来, 传输结果也不知道。磁盘还在那传输, 等磁盘传输完成或者传输失败之后, 想找进程把结果告诉它、数据还给它, 但是找不到。进程不见了, 被释放掉了。磁盘为了不影响自己工作, 就把这数据文件丢弃了。
这时候 程序数据没有了, 磁盘也没有保留数据。这几个G的文件还都是账单数据, 那银行怎么办?如果几个G的账单数据丢失, 那银行直接玩完!
为了防止这种传输文件时突然中断, 导致数据文件丢失或损坏, Linux就将 进程等待磁盘资源时的阻塞状态单独设置成了 D状态, D状态的特点就是无法中断

3. Z (zombie) 僵尸状态

Linux系统中的 X状态, 就是概念中的终止状态。当进程不会再被调度、永远不会再运行时, 会进入终止状态, 处于此状态的进程就随时等着被操作系统释放了
但是Linux系统中, 当进程不会再被调度、不会再运行时, 不会立马进入 X 终止状态。而是先进入 Z(zombie) 僵尸状态, 此进程被称为僵尸进程

为什么要进入僵尸状态?

首先提一个问题:进程为什么被创建?
答, 一定是程序有一定的功能, 需要执行一定的任务, 所以会被运行, 进程才被创建了。既然有功能有任务, 那在退出之前 肯定是需要将任务的执行进度告诉我们的。而一般, 进程在退出之前都会将执行结果、执行进度告知给其父进程或者操作系统
这些执行结果, 在task_struct中存储着, 称为退出信息。这些信息可以被父进程或者操作系统读取。
而当 进程不会再被调度、也不会再运行时, 就会进入 Z状态 表明自己不会再运行了, 但是退出信息还未被父进程或操作系统读取, 不能直接释放
只有在此进程的退出信息被父进程或操作系统读取了之后, 进程才会最后进入 X状态, 随时等待着被释放。所以, 其实 Z状态是维护进程退出信息的状态

task_struct 中部分退出信息

模拟僵尸进程

当进程已经运行结束已经退出但还未被释放, 而且退出信息没有被父进程读取。此时 子进程就会处于 Z状态, 变成僵尸进程
也就是说, 我们编写一个程序创建一个可以结束的子进程, 不给父进程写 读取子进程退出信息的代码, 让父进程一直运行下去, 此时的子进程就是一个僵尸进程:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using std::cout;
using std::endl;

int main() {
	pid_t id = fork();
	
	if(id == 0) {
		cout << "我是子进程, 我的pid是" << getpid() << ", 我运行结束了" << endl;
	}
	else {
		cout << "我是父进程, 我的pid是" << getpid() << ", 我运行没有结束" << endl;
		while(1) {}
	}
	
	return 0;
}
此时, 子进程运行已经结束, 而父进程也不读取退出信息, 就会导致子进程一直处于僵尸状态。
而且, 如果僵尸进程的退出信息一直不被读取不被接收的话, 僵尸进程就一直存在, 进程就会一直在内存中加载, 这其实就是内存泄漏
所以, 编写创建子进程的程序时, 要求一定要编写子进程的回收代码

4. T(stopped) 和 t(stracing stop) 暂停状态

这两个状态都是指 进程被暂停了, 没有本质上的区别。也不像 S 和 D一样 存在不同的特点。
只不过 t 特指 进程被调试时, 遇到断点时所处的状态
没错, 当进程被调试时, 遇到断点整个进程会被暂停, 此时所处的状态就是 t(stracing stop)状态

暂停进程

那么, 怎么让进程暂停呢?
其实很简单, 随便运行一个死循环程序:
#include <iostream>
#include <unistd.h>
using std::cout;
using std::endl;

int main() {
	while(1) {
		cout << "I am a Process, PID = " << getpid() << endl;
	}
	
	return 0;
}
然后使用kill -19 进程标识符Ctrl+Z, 将进程暂停:
此时显示进程的状态为 T状态
那么 t该怎么显示呢?
既然 t特指进程调试时遇到断点时所处的状态, 那就用gdb调试的方式查看:
可以看到, 使用gdb调试进程的时候, 当进程遇到断点, 此时进程的状态就会进入 t(stracing stop)状态

* 孤儿进程

Linux中, 存在一种特殊的进程——孤儿进程
只看名字其实就已经能想象到是什么意思了, 孤儿进程是指 子进程运行没有结束, 但是父进程的运行结束了, 父进程先被回收了, 那么此时的子进程就成了孤儿, 被称为孤儿进程
并且, 此时子进程一直在命令行中运行, 但是并不影响其他指令的执行:
并且, 此时的子进程无法被Ctrl+C终止掉, 只能使用kill -9 PID的方式kill

因为此时的子进程已经成为了后台进程, 在进程状态一栏中, 之前的进程的状态后都会有一个+号, 此 +号表示此程序是前台进程

而孤儿进程的状态栏中没有 +号, 表示此进程是一个后台进程

为什么孤儿进程的PPID变成了 1

因为当子进程的父进程提前结束的时候, 此子进程就会被系统接管, 此子进程也变成了孤儿进程, PID为1的进程就是系统:

Linux进程的优先级

进程的优先级也是进程的属性, 既然是进程的属性, 那么在Linux中就也在task_struct中被维护
关于优先级, 现提出一个问题:进程为什么存在优先级?
要清楚这个问题, 那就要知道 进程在什么情况下会涉及到使用到优先级?
答案只有一个, 那就是在等待获取资源的时候
进程在等待获取资源的时候, 其实就是在各种硬件的等待队列中时, 就像排队一样, 这个排队过程的本质其实就是在确定优先级
那么进程获取资源为什么需要排队呢?就像在日常生活中排队一样, 一定是想要获取的资源有许多人都想要获取, 但是获取速度太慢就会排队。其实进程与资源的关系也是一样的:操作系统中永远都是, 进程是多数的, 而资源是少数的
资源既然是少数的, 那么进程之间对资源的竞争一定会发生。这个时候, 如果操作系统不去确定每个进程的优先级是多少, 而是让所有的进程一锅粥的凭本事去竞争资源, 那么一定会造成部分进程"饿死"的情况。这是操作系统不应该也不允许发生的, 所以进程就需要确定一个优先级, 这样进程在获取资源的时候才不会乱套
这就是为什么进程需要存在优先级

priority 和 nice

在Linux操作系统中, 进程关于优先级的属性存在两个指标:priority 和 nice
  1. priority:在Linux系统中, 指当前进程的优先级, 此数值越小即表明进程的优先级越高
  2. nice:在Linux系统中, 可以理解为 当前进程优先级被调整的数值

PRI即为priority, NI即为nice

这两个指标中, PRI是不能被设置的, 只能通过修改NI值来间接修改PRI值。即, 如果想要调整进程的优先级, 只能调整NI的数值或者去修改系统源代码
举个例子:
可以修改自己进程的NI值, 演示一下进程优先级的简单修改:

关于修改进程优先级的指令, 可以查看一下man手册中的nice和renice

下面演示使用 top 指令

知道进程的PID时, 进入top, 再按r, 再输入PID, 再输入需要的NI值, 就可以做到NI值的修改(必须为root用户)

运行了一个死循环进程, PID 为 27495
进入top界面
输入进程PID:
在输入需要修改到的NI值:
然后可以发现, top界面中, NI和PR值都改变了:
使用ps -la查看此进程的优先级, 发现同样发生了改变:

PS:不同程序界面的PRI值不同, 可能是因为基准不同

这就通过top修改进程的NI值, 进而修改了进程的优先级

但是 NI值也不是随意修改的, 它有自己的限度:
当我设置NI值 -100 和 100 时:
设置NI -100
设置NI -100
设置NI 100
设置NI 100
可以发现, 其实 NI最低只能设置到-20, 最高只能设置到19。
所以 NI是存在设置范围的, 设置范围是 [-20, 19]

通过上面的例子, 其实可以发现一个细节——无论是ps指令中的PRI值, 还是top界面的PR值, 展示的都是进程的当前的优先级, 也就是已经加上了NI值的优先级。并且, NI = -20时, top界面的 PR = 0, ps展示的PRI = 60; NI = 19时, top界面的 PR = 39, ps展示的PRI = 99. 也就是说 当前的优先级值 = 最原始的优先级值 + NI值。
即 Pri(new) = Pri(old) + nice. Pri(old) 指的是今晨最原始的 Pri, 也是 nice 为 0时的值