012 进程状态和优先级

进程状态和优先级

[!TIP]

相关推荐视频 | B站

一、进程状态分类

Linux 中的进程状态可以通过 ps 命令或者 top 命令来查看,常见的状态码有以下几种:

状态码 名称 含义说明
R 运行(Running) 进程正在运行或处于可运行状态(等待 CPU 调度)
S 可中断睡眠(Sleeping) 进程正在等待某个事件(如 I/O、信号等),可以被信号或外部事件唤醒
D 不可中断睡眠(Uninterruptible Sleep) 进程正等待 无法被信号唤醒 的事件(如磁盘 I/O),一般出现在设备驱动程序中,例如正在等待硬件操作
T 停止(Stopped/Traced) 进程已被暂停执行,例如收到了 SIGSTOP 信号,或者在被调试时被暂停。
Z 僵尸(Zombie) 子进程已结束/终止,但父进程未回收它的资源(PID 和退出状态仍占用系统资源),导致进程表里留有“尸体”
X 死亡(Dead) 进程已彻底终止,且不会再存在于进程表中(非常短暂极少见,用户通常看不到)

特殊状态说明

  1. 僵尸进程 (Z)(一种比较特殊的状态)
    • 产生原因:当进程退出并且父进程(使用 wait() 系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态。
    • 危害:占用 PID,可能导致系统 PID 耗尽。
    • 解决:杀死父进程(僵尸进程会由 init 进程接管并回收)。
  2. 不可中断睡眠 (D)
    • 常见场景:进程在执行关键系统调用(如写入磁盘)。
    • 处理:通常需等待操作完成,强制终止可能导致数据损坏。

二、如何查看进程状态(进程的状态显示在 STAT 字段下)

image-20250317193133559

1. 使用 ps 命令

ps 可以显示一次性快照信息:

1
ps aux

查看部分关键字段:

1
ps -eo pid,ppid,stat,comm
  • PID:进程 ID。
  • PPID:父进程 ID。
  • STAT:进程状态(如 SRZ 等)。
  • COMMAND:执行该进程的命令。

输出示例:

1
2
3
4
5
6
7
8
PID  PPID STAT COMMAND
1 0 Ss systemd
2 0 S kthreadd
123 1 R bash
456 123 S vim
789 456 Z python
945 1 Ssl hostguard
1254 1 Ss+ agetty

解释:

  • Sssystemd 是会话首领(s),当前处于可中断睡眠状态(S)。
  • Sslhostguard 是会话首领(s),处于可中断睡眠状态(S),且是多线程进程(l)。
  • Ss+agetty 是会话首领(s),处于可中断睡眠状态(S),且是前台进程(+)。
  • Zpython 进程已变成僵尸进程(Z),等待父进程回收。

STAT 字段可能含有多个字符组合,如 SsR+额外属性(从第二列字符开始):

字符 属性名称 含义说明
s 会话首领(Session Leader) 该进程是会话的领导者,通常是终端启动的第一个进程。
+ 前台进程组(Foreground) 进程属于前台进程组,能接受来自终端的输入信号。
l 多线程(Multithreaded) 进程是多线程的(用 Linux 的线程实现)。
L 内存锁定(Locked in Memory) 进程有部分内存被锁定,无法被换出。
N 低优先级(Low Priority) 进程运行在低优先级(nice 值大于 0)。
< 高优先级(High Priority) 进程运行在实时优先级(nice 值小于 0)。
s 会话首领(Session Leader) 该进程是会话的领导者(通常是终端启动的第一个进程)。
n 优先级降低(Reduced Priority) 进程的优先级被降低(通过 nice 命令调整)。
** 进程被克隆(Cloned Process) 进程是通过 clone() 系统调用创建的,通常用于线程实现。

2. 使用 top 命令

top 命令动态刷新进程状态:

1
top
  • S 列表示进程状态,通常会看到 RSZ 等状态。
  • q 键退出。

3. 使用 proc 文件系统

每个进程在 /proc 下有一个专属目录(以 PID 命名),可以直接查看其状态:

1
cat /proc/<PID>/status | grep State

示例:

1
2
cat /proc/1234/status | grep State
State: S (sleeping)

三、进程状态的生命周期

一个典型的进程生命周期大致如下:

  1. 创建(Created):fork() 创建进程,父进程复制自身的内存空间。

  2. 就绪(Ready): 进程已准备好运行,等待 CPU 调度。

  3. 运行(Running): 进程正在被 CPU 执行。

  4. 阻塞/睡眠(Blocked/Sleeping): 进程等待外部事件(如 I/O)完成 (可中断睡眠和不可中断睡眠)

    • (挂起(Suspended):通常由用户或调试器主动暂停进程(如 CTRL + z))
  5. 终止(Terminated): 进程执行完成或被强制终止。

  6. 僵尸(Zombie): 子进程结束后,父进程未回收其退出信息,导致子进程残留在进程表。

  7. 销毁(Dead): 僵尸进程被父进程回收后,彻底消失。

四、进程状态转换示意图

这部分比较抽象,了解部分即可。 典型的转换流程:新建 (New) → 就绪 (R) → 运行 (R) ↔ 阻塞 (S/D) → 终止 (Z/X),还有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+---------------------+         +-----------------------+
| 创建 (New) | | 不可中断睡眠 (D) |
+---------------------+ +-----------------------+
↓ ↑
+---------------------+ 等待事件完成 |
| 可运行/运行中 (R) | ←--------------→ | 可中断睡眠 (S) |
+---------------------+ 调度器分配 CPU +---------------------+
| ↑ ↑ |
| | | 等待硬件操作 | 挂起信号
↓ | ↓ ↓
+---------------------+ +---------------------+
| 停止 (T) | ← SIGSTOP/SIGTSTP | 终止/僵尸 (Z) |
+---------------------+ +---------------------+
| |
| SIGCONT ↓
↓ +---------------------+
+---------------------+ | 回收 (X) |
| 可运行/运行中 (R) | → 父进程回收 → +---------------------+
+---------------------+

进程的状态模型并没有一个单一的官方标准定义,而是根据不同的操作系统和理论模型有不同的实现。 不过,通常讨论的 三态、五态和七态模型 是基于操作系统的进程管理理论中的常见模型。

[!WARNING]

下面是从网络上找的相关图片,只是 偏向正确,因为没有具体标准定义!

image-20250317165239824

image-20250317212904093


实验 1:僵尸进程的创建与监控

1. 实验步骤

  1. 编写僵尸进程代码:父进程创建子进程后,子进程立即退出,但父进程不调用 wait() 去回收子进程的资源,从而让子进程变成僵尸进程。
  2. 编译与运行:运行代码后,使用 ps 命令在另一个终端监控进程状态。
  3. 观察现象:子进程退出后,父进程没有回收它,它的状态会变成 Z(Zombie)。

2. 僵尸进程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
pid_t pid = fork(); // 创建子进程

if (pid < 0)
{
perror("fork失败");
exit(1);
}
else if (pid == 0) // 子进程
{
printf("子进程(PID:%d)正在运行...\n", getpid());
sleep(5); // 子进程运行5秒后退出
printf("子进程(PID:%d)即将退出...\n", getpid());
exit(0);
}
else // 父进程
{
printf("父进程(PID:%d)正在运行...\n", getpid());
sleep(30); // 父进程等待30秒(不调用wait(),导致子进程成为僵尸进程)
printf("父进程结束。\n");
}

return 0;
}

3. 运行步骤:

  1. 编译代码:
1
gcc corpse.c -o corpse
  1. 运行程序:
1
./corpse
  1. 在另一个终端用 ps 命令观察:
1
ps -eo pid,ppid,stat,cmd | grep 'Z'
  1. 现象:你会发现子进程的 STAT 显示为 Z,说明它已变成僵尸进程。

    image-20250317202057393

  2. 危害

    • PID 资源耗尽:僵尸进程本身不消耗资源,但它的 PID (进程标识符)不会被释放。如果系统产生大量僵尸进程,PID 会被耗尽,导致新进程无法创建!
    • 内存泄漏:内核保留僵尸进程的退出状态和资源描述符,直到父进程回收。
  3. 解决办法:通过 kill 杀掉父进程,僵尸进程会被 init 进程回收。

1
kill -9 父进程PID		# 终止父进程,子进程由 init 进程回收

实验 2:孤儿进程的创建与监控

1. 实验步骤:

  1. 编写孤儿进程代码:父进程创建子进程后,父进程主动退出,子进程被 init 进程接管。
  2. 编译与运行:运行代码后,用 ps 命令监控子进程的 PPID(父进程 ID)。
  3. 观察现象:父进程退出后,子进程的 PPID 变成 1(即 init 进程)。

2. 孤儿进程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid = fork(); // 创建子进程

if (pid < 0)
{
perror("fork 失败");
exit(1);
}
else if (pid == 0) // 子进程逻辑
{
sleep(1); // 确保父进程先退出
printf("子进程 PID: %d, PPID: %d\n", getpid(), getppid());
while (1)
{
sleep(10); // 保持子进程存活,方便观察
}
}
else // 父进程逻辑
{
printf("父进程 PID: %d 创建了子进程 PID: %d,然后退出\n", getpid(), pid);
exit(0); // 父进程主动退出,产生孤儿进程
}

return 0;
}

3. 运行步骤:

  1. 编译代码:
1
gcc orphan.c -o orphan
  1. 运行程序:
1
./orphan
  1. 在另一个终端用 ps 命令观察:
1
ps -eo pid,ppid,stat,cmd | grep 子进程PID
  1. 现象:你会发现子进程的 PPID 变成 1,说明它被 init 进程接管,变成了孤儿进程。

    image-20250317203906101

  2. 危害孤儿进程一般不会直接危害系统,主要分下面几种情况(这就像一个“扫地机器人”:只有当进程“倒下”(退出)时,init 才来打扫;如果进程一直乱跑或者不断“生孩子”,init 也束手无策。):

    • 自动回收机制:

      • 当父进程退出后,孤儿进程会被 init 进程(PID = 1)收养。
      • init 进程会定期调用 wait() 来回收那些已经结束的子进程,避免出现僵尸进程。
    • 陷入死循环的情况:

      • 如果孤儿进程本身陷入死循环(比如 while (1) { sleep(1); }),它不会退出,自然也不会被 init 回收。
      • init 只能回收已经 退出的子进程。如果孤儿进程一直在运行,init 什么也做不了。
    • 大量创建子进程的情况:

      • 孤儿进程本身如果大量 fork() 创建新子进程,这些子进程同样会被它的父进程(即原孤儿进程)管理。

      • 如果原孤儿进程随后退出,那么它创建的子进程就会变成新的孤儿进程,init 会接管它们。

      • 如果这种行为持续发生,系统的进程表(PID 资源)可能会被快速耗尽,导致系统无法创建新进程,从而影响稳定性。

  3. 解决办法:手动杀掉孤儿进程:

1
kill -9 子进程PID

4. 对比总结

[!NOTE]

  1. 孤儿进程的资源会被回收吗?
    • 是的,init 进程会主动回收孤儿进程。
  2. 僵尸进程和孤儿进程哪个更危险?
    • 僵尸进程,因为长期占用系统资源。
特性 僵尸进程 孤儿进程
产生条件 子进程终止,父进程未调用 wait() 父进程终止,子进程仍在运行
危害 占用 PID 和内核资源 一般无危害(主要由 init 进程自动回收)
状态符号 Z (Zombie) 无特殊状态,PPID 变为 1
处理方式 终止父进程或修复父进程代码调用 wait() 通常无需处理
系统影响 可能导致 PID 耗尽 无负面影响

五、进程的优先级

在 Linux 系统中,进程的优先级(Priority)决定了调度器(Scheduler)选择哪个进程优先运行。这里有两个关键概念:PRI(优先级)NI(静态优先级/Nice 值)

名称 全称 范围 作用
PRI Priority 0~139 进程的实际优先级,值越小优先级越高
NI Nice Value -20~19 用户可调整的优先级修正值,影响 PRI,NI 是用户可调节的值,用来“建议”系统优先级,但最终还是由内核决定。

1. PRI(Priority,优先级):

  • 定义:表示内核调度时分配给进程的优先级。数值越小,优先级越高。
  • 范围:
    • 内核视角PRI 范围 0 ~ 139 是进程的 真实优先级,值越小优先级越高。
    • 用户视角实时进程的优先级是 0 ~ 99(适合对实时性要求高的任务,如工业控制、音视频处理等),普通进程的优先级是 100 ~ 139(普通用户任务,如文本编辑器、浏览器等)。
  • 决定因素PRI = 20 + NI(普通进程),nice 值影响普通进程的优先级,但 不会直接影响实时进程的优先级
  • 查看命令
1
ps -eo pid,pri,ni,comm

输出示例:

1
2
3
4
 PID PRI NI COMMAND
1 80 0 systemd
1234 90 10 python
5678 70 -5 nginx

或者:top 命令,在 top 界面里:

  • f 键进入字段选择界面,找到 PRINI,按空格键选中后回车返回主界面,即可看到优先级信息。
  • PRI 和 NI 默认就会显示在列表中。

2. NI(Nice 值):

  • 公式:PRI (新) = PRI (默认) + NI

    • 默认 PRI 通常为 80(不同系统可能不同),因此实际 PRI 范围是:80 + (-20) = 60(最高优先级) 到 80 + 19 = 99(最低优先级)。
  • 定义:影响普通进程的优先级(PRI),表示“进程对 CPU 资源的友好程度”。数值越高,优先级越低,越“谦让”。当 nice 值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值。

  • 范围-20(最重要)到 19(最不重要),一共 40 个级别。默认值为 0

  • 调整规则:NI 值越低,PRI 越小,进程优先级越高

    • 普通用户只能 降低优先级(NI 值 ≥ 0)
    • Root 用户可 提升优先级(NI 值 < 0)
  • 关系:$PRI = 20 + NI$

    • NI = -20 → PRI = 0 + 20 = 20(最高优先级)
    • NI = 0 → PRI = 0 + 20 = 20(默认值)
    • NI = 19 → PRI = 19 + 20 = 39(最低优先级)
  • 调整 NI 值

1
2
3
4
5
# 启动时设置 nice 值
nice -n 10 ./可执行程序

# 运行中调整 nice 值
renice -5 1234 # 把 PID 为 1234 的进程 NI 调整到 -5

3. 注意事项

  1. 避免滥用高优先级:过多高优先级进程可能导致系统不稳定(如 GUI 无响应)。
  2. NI 值继承:子进程会继承父进程的 NI 值。

六、其他概念补充

[!IMPORTANT]

  • 竞争性:进程争抢 CPU、内存等资源。
  • 独立性:每个进程互不干扰,资源独立。
  • 并行:多核 CPU 下的真正“同时运行”。
  • 并发:单核 CPU 下的“快速切换”,让多个进程看似“同时进行”。

1. 竞争性(Competitiveness):

  • 定义:由于系统中的进程数量众多,而 CPU 资源有限(甚至可能只有 1 个),所以各个进程需要竞争 CPU 使用权。为了高效完成任务,更合理竞争相关资源,便有了优先级。
  • 引申含义:
    • 系统通过调度算法来决定哪个进程先使用 CPU。
    • 为了更合理地分配资源,引入了 优先级(Priority) 概念,优先级高的进程更有可能被调度运行。
    • 竞争不局限于 CPU,进程还可能竞争内存、I/O 设备等资源。

2. 独立性(Independence):

  • 定义:多进程运行时,每个进程都有自己独立的内存空间和资源,彼此不会直接影响。
  • 特点:
    • 每个进程的执行逻辑、变量、文件描述符等都是独立的。
    • 若需要通信,通常使用 进程间通信 机制,例如:管道(Pipe)、共享内存、消息队列等。
  • 意义:独立性保证了系统稳定性,即使某个进程崩溃,其他进程也能正常运行

3. 并行(Parallelism):

  • 定义:多个进程在多个 CPU 核心上同时运行,互不干扰,真正实现“同时进行”。
  • 条件:需要多核 CPU 或多台机器(分布式系统)支持。
  • 举例:在 4 核 CPU 上,4 个进程可以在每个核心上独立运行,互不干扰,实现真正的“并行”。

4. 并发(Concurrency):

  • 定义:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。(在单核 CPU 上,多个进程通过频繁的“时间片切换”,让每个进程在宏观上看起来同时执行
  • 原理:CPU 每隔一段时间(时间片)切换到下一个进程执行,切换速度极快,人眼无法分辨,以为多个进程“同时”运行。
  • 举例:在单核 CPU 上运行多个下载任务,CPU 不断在不同任务之间切换,让所有任务都能逐步完成。

[!NOTE]

进程切换

  • 概念:在单 CPU 系统中,多个进程通过轮流使用 CPU 资源实现“并发”,这依赖于操作系统的“进程切换”。
  • 实现方式:采用调度算法(如时间片轮转的调度算法),每个进程被分配固定的时间片。时间片用完后,系统暂停该进程,保存状态,并切换到下一个进程执行。

上下文保存

  • 意义:确保进程被切换时,其运行状态(即“上下文”)被妥善保存,待下次恢复执行。上下文是指进程执行时的环境状态,包括寄存器的值、程序计数器的值等。
  • 保存内容
    • 通用寄存器:如eax、ebx、ecx、edx等,用于存储操作数和计算结果。
    • 栈指针和基指针:如ebp、esp,用于管理函数调用和局部变量的存储。
    • 程序计数器(eip):记录当前进程正在执行指令的下一条指令的地址,决定了进程执行的流程。
    • 状态寄存器(status):包含条件码等信息,用于判断指令执行后的状态。
  • 保存时机:在进程被切换时,需要先保存当前进程的上下文,然后恢复下一个要执行进程的上下文。

寄存器的作用

  • 通用寄存器:用于存储操作数和中间结果,提高数据访问速度,如在算术运算和数据处理指令中使用。
  • 程序计数器(eip):指向进程下次执行的指令地址,是进程执行流程的关键。
  • 栈指针和基指针:用于管理函数调用时的参数传递和局部变量存储,维护函数调用栈的结构。
  • 状态寄存器:保存CPU的状态信息,如进位标志、零标志等,用于条件判断和跳转指令。

进程切换的具体步骤

  1. 保存当前进程上下文:暂停当前进程,将寄存器、程序计数器等保存到该进程的控制块(如 task_struct)中。
  2. 更新进程状态:将当前进程状态改为“就绪”或“等待”,并加入对应队列。
  3. 选择新进程:根据调度算法(如优先级调度)选择下一个要执行的进程。
  4. 恢复新进程上下文:从新进程的控制块恢复寄存器和程序计数器等信息。
  5. 切换至新进程:CPU 开始执行新进程的指令,完成切换。