目录

进程状态的分类

CPU亲和性掩码:

进程运行:

进程阻塞:

S状态:

D状态:

进程挂起:

磁盘上的swap分区:

内核的 OOM killer:

进程中断:

进程死亡:

僵尸进程:

孤儿进程:

​编辑

孤儿托管:

前后台进程:


进程状态的分类

   我们先来看一下Linux关于进程种类的源码:

/*
*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 */
};

   在代码中我们可以看到进程在Linux操作系统下至少包含这几种状态:

   运行状态(R)、休眠状态(S)、不可中断休眠状态(D)、中断状态(T)、追踪暂停状态(t)、死亡状态(X)、僵尸状态(Z)

CPU亲和性掩码:

   我们先不谈论那么多的进程状态,先来介绍一下各状态后面的数字--CPU亲和性掩码

   首先我们要明白CPU并不是一个绝对的单独个体,CPU中还会分0各种核心以处理不同的进程提高效率。各种状态后面的十进制数字(假设R--0、S--1、D--2……)转化成二进制(那么R--0、S--01、D--10……)其中1表示可以调度。而这个1所处的位置就表示调用第几号核心,如在上面假设中:R为0没有被分配CPU核心无法进行;S为第零位为1,分配到CPU 0号核心;D为第一位为1,分配到CPU1号核心……

   CPU亲和性掩码作用是限制进程只能在指定 CPU 核心运行,机制基于二进制位映射与内核调度规则。

   上面R的亲和性掩码是0就表明无法调度啊,可是实际上一个进程正常情况下在机器上是能够正常进行的(R运行状态)。这是因为大部分进程状态的亲和性掩码没有固定值(R可能为4、S可能为3……),但存在两种特殊情况(掩码 0、系统强制配置)。无论进程显示何种状态(除临时异常 R),掩码 0 都是固定无效配置,进程无法获得 CPU 核心。

   R的亲和掩码为0通常意味着:

  1. 临时过渡状态:进程刚被修改亲和性(如从其他掩码改为 0),状态还未同步更新,很快会转为非 R 状态(如 Z 僵尸态、T 停止态)。
  2. 系统异常 / 配置错误:手动通过taskset等命令强制设置掩码为 0,或内核 bug 导致状态显示异常,属于无效配置。
  3. 工具显示偏差:部分进程查看工具(如简化版ps)可能误判状态,实际进程已无法调度,需用ps -eo pid,stat,cpuid,cmdtaskset -p <pid>确认真实掩码。

进程运行:

   CPU也符合先描述,再组织的哲理,其内部专门有一个数据结构--CPU调度队列来管理控制待进入CPU的各进程。

   在目前主流的看法中处于运行(R)状态的进程就是CPU等待队列中的进程 + CPU中正在执行的进程,这稍微有些反直觉但记住就好。

进程阻塞:

S状态:

   当我们启动这个进程并且就是不从键盘输入数据时:

   我们可以看到此时./text进程状态为休眠(S)状态。进程触发sacnf后,会主动请求键盘输入资源。但键盘资源未就绪,内核会将进程从 “CPU 就绪队列” 移出,放入 “键盘设备等待队列”。此时 CPU 调度队列中无该进程,进程不参与 CPU 调度,处于 S 状态,不占用 CPU 资源。

   这个时候虽然./text进程I/O资源未就绪,但由于其被移到键盘硬件的等待队列中并不会阻碍CPU处理其它进程。

   另外,S状态是可以被打断的,故S状态也可以称为为浅睡眠状态(例如Ctrl+c)。

   阻塞相比于运行,其核心不同就是进程是否处于CPU等待队列中

D状态:

   S状态属于浅睡眠状态能够被系统杀死,但有时候我们需要保证某个进程不会被系统杀死就会标记为D--不可中断休眠状态。

   D 状态无法被常规信号(包括kill -9)杀死,仅当 I/O 操作完成、硬件出错或系统重启时,D 状态进程才会退出。

   在一般情况下我们是难以见到D状态的,但一旦见到了除了目前存在大量I/O流程以外还可能暗示这台机器年久失修,命不久矣。

进程挂起:

磁盘上的swap分区:

   磁盘上存在一个独立的swap分区用于处理物理内存不足的情况:当内存不足时内核会将内存中闲置的进程数据移至 swap(挂起),释放物理内存给活跃进程。但由于磁盘的读取速度远不如物理内存,一旦存在过多的进程进入swap分区就会使机器产生明显卡顿。其本质就是时间换空间。

内核的 OOM killer:

   在极端情况下磁盘上的swap分区也使用完的时候,操作系统就不得不进行杀掉一些进程并释放其空间,OOM就是处理这种情况(不能杀死D状态下的进程)。

   OOM会根据优先级有选择性的杀掉那些对操作系统影响不大的进程释放其空间,这也是机器卡死时部分软件会崩溃原因。

进程中断:

   中断一个进程通常有两种情况:1、中断信号引发 2、启动调试( t 状态)

   相比于阻塞,产生中断的原因通常而言更加严重--一般是进程出现某种错误而被中断;而阻塞则不是进程出错的缘故导致的,阻塞是正常进程等待进行。

   cgdb调试也是一个进程,目前休眠(S)等待用户调试;而./text进程处于调试追踪场景为t状态(ts是一个组合状态)。

   关于信号这一部分后面再说,目前暂时不用太多于关注。

进程死亡:

   X 状态表示进程已死亡(终止),正在被内核清理资源持续时间极短,几乎不会在ps命令中稳定观测到。

僵尸进程:

   举个不恰当的例子,现实生活中一个人突然倒在马路上死去时并不会立刻就被救护车拉走而是会被警察封锁现场进行处理,确认正常死亡后还要通知家属安排后续。

   这一过类比说明当一个进程死亡时不应该立刻进行资源清理工作而是应该先进行进程退出信息的处理、暂时保存后再释放。这释放资源之前的过渡状态就是僵尸状态(Z)。

   进程的退出值通常为main函数返回值或者是收到的信号,这些退出值保存在进程的PCB(task_struct)中并且不会被销毁,直到未来父进程或者操作系统来获取到子进程的task_struct中的退出数据才彻底释放资源。

   一旦没有”家长“处理”后事“,进程就会一直处于僵尸(Z)状态其task_struct也不会被销毁,一直占用内存空间导致内存泄漏!

孤儿进程:

   这里明明进程正在进行为什么状态却是S状态呢?这是因为源代码中采用了sleep函数导致进程会每次打印后休眠(S)一秒钟,又因为休眠时长远大于I/O流的速度导致难以观察到R状态,实际上R状态是存在的不过持续时间很短,大部分还是会观测到处于S状态。

   当我们使用 kill -9 杀掉父进程时:

   我们可以观察两个现象:

1、父进程已经被杀死但是子进程并没有退出,子进程的父进程变为1

2、子进程的状态由S+变为S

解释:

孤儿托管:

   当一个进程的父进程被杀死后该进程就没有了父亲成为了“孤儿”,这时候就会导致没有父进程管理释放子进程的内存空间导致内存泄漏,这是很严重的问题。

   为了防止这种问题操作系统会将那些没了父进程的孤儿进程领养,孤儿进程的内存释放等问题就有操作系统处理解决。

   在 Linux 系统中,PID 为 1 的进程是系统的 “初始化进程”,承担着核心的系统管理职责。

前后台进程:

   linux下,之前我们所看到状态后面跟着一个 + 表示该进程处于前台进程,如果没有则表示为一个后台进程。

类型 含义 终端交互 状态标识(ps命令) 典型场景
前台进程 与用户当前终端直接交互的进程,占据终端的输入输出流。 进程状态后带+(如S+ 命令行交互程序(如vimtop
后台进程 在终端后台运行,不占用终端输入输出,用户可同时在终端执行其他命令。 进程状态后不带+(如S 耗时任务(如编译、文件下载、后台服务)

   后台进程无法进行I/O操作,一旦强制将带有I/O操作的进程设为后台进程就会导致进程被暂停(进入 T 状态)

   并且处于后台的进程是无法直接被暂停的(如Ctrl+c)

   如果需要暂停则需要通过信号来处理,如:

kill -STOP 进程PID  # 发送暂停信号,进程进入T状态

   区分一个进程是否为前后端的关键是看其能否进行I/O操作。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐