在多任务操作系统中,进程的优先级是决定其执行顺序的重要依据。Linux 作为一个支持多用户、多任务的操作系统,通过优先级机制来确保系统资源能够合理分配,从而满足不同类型任务的需求。理解进程优先级不仅能帮助我们优化程序性能,还能为定位系统问题提供有效的思路。
1 基本概念
- 优先级也是一个整数,值越低,表明优先级越高
UID(user id)
2. PRI and NI
在 Linux 系统中,进程优先级由两个关键指标共同作用PRI(Priority) 和 NI(Nice Value)。
2.1 PRI(Priority)— 动态优先级
PRI 是内核使用的动态优先级值,用于决定进程的调度顺序。它是实际调度的依据,影响进程在 CPU 调度中的优先级高低。
- 范围:
PRI 的范围通常是 0-139,分为两部分:
- 实时优先级:0-99(用于实时进程)。
- 普通优先级:100-139(用于普通进程)。
普通进程的优先级范围是 100-139,但一些文档中提到的 60-99 是调度器内部映射的范围,并非实际的 PRI 值。内核将 `100-139` 映射为更小的范围(如 `60-99`),以便优化调度器的权重计算和处理性能。因此,这只是内核实现中的优化,而非用户可见的 PRI 值变化。此时PRI为默认80。
2.2 NI(Nice Value)— 静态优先级
NI 是用户空间可见的优先级值,用户可以通过它调整进程的初始优先级,进而间接影响 PRI 值。可以说NI是进程优先级的修正数据。
- 范围:
NI 的取值范围是 -20 到 19,默认值为 0。
- -20 表示最高优先级。
- 19 表示最低优先级。
进程真实的优先级 = PRI(默认) + NI
2.3 修改进程优先级
修改进程优先级:top -> r -> pid -> 修改数据
值得注意的是,进程优先级永远是默认的PRI 加上修正值
修改为10
再修改-10,这是得到的值是80(默认PR)-10,而不是90-10
优先级的极值
将修正值设为-100和100
由上面结果可以看到,优先级区间为[60, 99],nice值为[-20,19]
优先级设置不合理,会导致优先级低的进程长时间得不到CPU资源,进而导致进程饥饿
3. 查看进程优先级的命令
3.1 nice
`nice` 用于以指定的nice值启动一个新进程。
语法:
nice [OPTION] [COMMAND [ARG]...]
参数:
- `-n, --adjustment`:设置 `nice` 值的调整量,范围为 `-20` 到 `19`,默认为 `10`。
- `-20`:最高优先级(最低 `nice` 值)。
- `19`:最低优先级(最高 `nice` 值)。
使用示例
1. 启动进程并降低优先级
启动 `myprocess 并设置 `nice` 值为 `10`:
nice -n 10 ./myprocess
2. 提高优先级
设置 `nice` 值为 `-5`(需要超级用户权限):
sudo nice -n -5 ./myprocess
3.2. renice
`renice` 用于调整已运行进程的 `nice` 值。
语法:
renice [OPTION] priority [IDENTIFIERS...]
参数:
- `priority`:新的 `nice` 值,范围为 `-20` 到 `19`。
- `IDENTIFIERS`:指定的进程标识符,可以是以下之一:
- PID(进程号)。
- PGRP(进程组号,需加 `-g` 选项)。
- 用户(需加 `-u` 选项)。
选项:
- `-p`:默认,表示通过PID调整。
- `-g`:通过PPID调整。
- `-u`:通过用户调整。
使用示例
1. 调整单个进程的优先级
将进程优先级设置为 `5`:
renice -n 5 -p 1234
2. 调整进程组的优先级
将进程组号为 `5678` 的所有进程设置为 `10`:
renice -n 10 -g 5678
3. 调整某用户所有进程的优先级
将用户 `user1` 的所有进程优先级设置为 `15`:
renice -n 15 -u user1
权限要求:
- 普通用户:
- 只能增加 `nice` 值(降低优先级)。
- 无法将 `nice` 值降低到负值。
- 超级用户(root):
- 可以任意调整 `nice` 值,包括设置负值。
nice 值越小,`PRI` 越低,优先级越高。
nice 值越大,`PRI` 越高,优先级越低。
3.3 top
`top` 是 Linux 中常用的实时性能监控工具,显示系统的资源使用情况和活动进程。通过它,可以查看 CPU、内存、任务、负载等信息,并对进程进行交互式管理。
1. 命令基础
语法
top [选项]
常用选项
| 选项 | 说明 |
|---------------------|----------------------------------------------------------------------|
| `-d <秒>` | 指定刷新间隔时间,默认是 3 秒。 |
| `-p <PID>` | 仅显示指定 PID 的进程。 |
| `-u <用户名>` | 仅显示指定用户的进程。 |
| `-n <次数>` | 运行指定次数后退出。 |
| `-b` | 批处理模式,用于脚本或文件保存。 |
| `-i` | 忽略闲置和僵尸进程,仅显示活动进程。 |
| `-o <字段>` | 按指定字段排序(如 `%CPU`、`MEM` 等)。 |
示例
1. 显示某用户的进程:
top -u alice
2. 每秒刷新一次并忽略闲置进程:
top -d 1 -i
2. `top` 界面解析
全局状态信息
在 `top` 的输出顶部,会显示系统的全局状态,包括系统性能、任务和资源使用情况。
| 行序 | 字段 | 说明 |
|------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | 系统时间 | 当前时间、运行时间、在线用户数、系统平均负载 (`load average`)。 |
| 2 | 任务信息 | 任务总数、运行、睡眠、停止和僵尸任务的数量。 |
| 3 | CPU 使用率 | 按用户、系统、中断、空闲等分类显示 CPU 的使用率。 |
| 4 | 内存使用信息 | 显示物理内存的总量、已用、空闲和缓存/缓冲区占用情况。 |
| 5 | 交换分区信息 | 显示交换分区的总量、已用、空闲及缓存到交换分区的内存量。 |
进程详细信息
下方是活动进程的详细列表,显示系统中每个进程的实时状态。
| 字段 | 说明 |
|----------|-------------------------------------------------------------------------------------------|
| PID | 进程 ID。 |
| USER | 启动进程的用户。 |
| PR | 进程的优先级。 |
| NI | 进程的 `nice` 值。 |
| VIRT | 进程占用的虚拟内存总量,包括物理内存和交换空间。 |
| RES | 进程占用的物理内存大小,不包括交换空间。 |
| SHR | 共享内存的大小(进程之间共享的内存)。 |
| S | 进程状态:`R`(运行),`S`(睡眠),`D`(不可中断睡眠),`Z`(僵尸),`T`(停止)。 |
| %CPU | 进程的 CPU 使用百分比。 |
| %MEM | 进程的内存使用百分比。 |
| TIME+ | 进程启动以来占用的总 CPU 时间(用户模式和内核模式)。 |
| COMMAND | 运行进程的命令名或路径。 |
3. 交互式操作
在 `top` 界面中,可以使用以下快捷键对显示内容和进程进行交互操作。
常用快捷键
| 快捷键 | 功能 |
|--------|----------------------------------------------------|
| `h` | 显示帮助菜单,列出所有快捷键。 |
| `q` | 退出 `top`。 |
| `z` | 切换颜色高亮显示。 |
| `l` | 切换显示平均负载和系统运行时间的第一行信息。 |
| `t` | 切换任务和 CPU 信息显示。 |
| `m` | 切换内存和交换分区信息显示。 |
| `1` | 显示每个 CPU 的单独使用情况(多核系统)。 |
| `c` | 显示或隐藏完整命令行(默认只显示命令名)。 |
| `n` | 更改显示的进程数。 |
| `u` | 按用户过滤进程。 |
| `k` | 杀死指定进程(需输入 PID)。 |
| `r` | 更改指定进程的优先级(需输入 PID 和新 `nice` 值)。 |
| `P` | 按 CPU 使用率排序。 |
| `M` | 按内存使用率排序。 |
| `T` | 按运行时间排序。 |
4. 竞争、独立、并行、并发
5. 进程切换
进程切换是指操作系统暂停当前运行的进程,并将 CPU 资源分配给另一个进程的过程。这种切换由内核负责,是实现多任务操作系统的重要机制。
进程切换最核心的,就是保存和恢复当前进程硬件上下文数据,即CPU内寄存器的内容。这些内容会被保存到task_struct中。
1. 为什么需要进程切换?
多任务操作系统需要支持多个进程同时运行,而 CPU 在任意时刻只能执行一个任务。进程切换通过时间分片和资源调度,让用户感觉多个任务同时进行,从而实现**并发**或**并行**。
2. 进程切换的触发条件
进程切换可以在以下情况下触发:
1) 时间片到期
- 使用时间片轮转调度算法时,当前进程的时间片耗尽,操作系统会切换到下一个进程。
2) I/O 或阻塞操作
- 当前进程请求 I/O 设备(如读写文件、网络操作),导致进程进入阻塞状态,操作系统会调度其他进程运行。
3) 优先级调度
- 高优先级的进程就绪时,低优先级进程会被暂停,CPU 切换到高优先级进程。
4) 主动让出 CPU
- 进程调用系统调用(如 `sched_yield()`)主动放弃 CPU,切换给其他进程。
5) 硬件中断
- 当外部设备产生中断(如键盘输入、网络数据到达)时,当前进程会被暂停,切换到处理该中断的中断处理程序。
6) 系统调用
- 进程发起某些系统调用(如文件操作、内存分配)时,可能会切换到内核态或其他进程。
3. 进程切换的步骤
进程切换涉及保存当前进程的状态,并加载另一个进程的状态。
1) 保存当前进程的状态
- **保存 CPU 寄存器内容**:包括程序计数器(PC)、栈指针(SP)和通用寄存器。
- **保存内存管理信息**:如页表指针。
- **更新进程控制块 (PCB)**:将当前进程的状态更新为“就绪”或“等待”。
2) 切换上下文
- 调度器选择下一个要运行的进程。
- 加载该进程的 PCB 和内存上下文。
3) 恢复新进程的状态
- 恢复目标进程的 CPU 寄存器、栈指针等信息。
- 将目标进程的状态设置为“运行”。
总结
- 进程切换是多任务操作系统中实现并发的重要机制。
- 切换由操作系统内核管理,包括保存和恢复进程上下文。
- 虽然进程切换有开销,但通过高效调度算法可以降低影响。
- 深入理解进程切换有助于优化系统性能和任务调度。
6. O(1)调度算法
Linux 中的 O(1) 调度算法是一种高效的进程调度方式,其设计目的是在常数时间内完成进程调度决策,不论系统中运行的进程数目有多少。这是通过合理组织数据结构(如 `runqueue` 和优先级队列)来实现的。
6.1 关键数据结构
1) `runqueue`(运行队列)
- 每个 CPU 上都有一个 `runqueue`,它包含了所有处于就绪状态的进程。
- `runqueue` 中的进程按照优先级进行组织,进程根据其优先级分配到不同的队列。
2) 活动队列(Active Queue)
- 活动队列存储的是当前时间片内应该运行的进程。
- 这些进程是高优先级的进程,通常会被安排在 CPU 上执行,直到它们的时间片用完或有 I/O 操作。
3) 过期队列(Expired Queue)
- 过期队列存储的是已经耗尽时间片的进程,这些进程暂时不能继续执行,需要经过优先级调整后,才能重新回到活动队列中。
- 过期队列中的进程等待下一个周期的调度。
4) `active` 和 `expired` 指针
- `active` 指针指向活动队列中的队列头(最优先的进程)。
- `expired` 指针指向过期队列中的队列头。
- 通过这两个指针,调度器可以快速地从活动队列或过期队列中选择一个进程进行调度。
5) 优先级
- Linux 中的进程优先级决定了进程的执行顺序。O(1) 调度算法采用优先级队列来区分不同优先级的进程。
- 进程有不同的优先级级别,在 Linux 的 O(1) 调度中,优先级被划分为 **多个优先级队列**,每个队列对应一个优先级范围。
6.2 O(1) 调度算法的工作原理
O(1) 调度算法的核心思想是 保持每个优先级的活动队列 和 过期队列,并通过一个常数时间复杂度的操作来完成进程调度。它的实现方式是通过两个主要的队列管理进程的优先级和运行状态:
1) 当 CPU 空闲时
- 查找活动队列中的第一个进程,即 `active` 队列指针指向的进程。
- 如果活动队列中有可运行的进程,则将该进程加载到 CPU 并开始执行。
2) 进程时间片耗尽
- 如果进程的时间片耗尽,它会被移到过期队列 `expired` 中,并且会降低优先级。
- 然后,调度器会将过期队列中的进程迁移到活动队列,并重新排序。
3) 从活动队列和过期队列中调度进程
- 活动队列:首先检查 `active` 队列。`active` 队列中保存着当前时间片内可以运行的高优先级进程。如果有进程准备好运行,则直接调度该进程。
- 过期队列:如果活动队列为空,或者没有更高优先级的进程,则调度器检查 `expired` 队列中的进程。
- `expired` 队列中的进程需要重新调整优先级后,再放回到 `active` 队列中。
4) 切换活跃队列与过期队列
- 进程时间片耗尽后,调度器会将所有活跃队列中的进程移到过期队列。
- 当一个队列为空时,调度器会交换 `active` 和 `expired` 指针。
- 比如,如果活动队列为空,`active` 指针指向过期队列,`expired` 指针指向活动队列。
5) O(1) 时间复杂度
- 由于每个队列中的操作(如插入、删除、查找)都是常数时间操作(O(1)),因此无论进程的数量是多少,调度决策都可以在 **常数时间** 内完成。
总结
O(1) 调度算法通过使用 活动队列(Active Queue)和 过期队列(Expired Queue),结合 active 和 expired 指针,实现了高效的进程调度。该算法确保了调度操作在 常数时间复杂度 下完成,无论进程的数量如何变化,都能保持较低的开销。通过这种方式,Linux 能够在保证高效调度的同时,平衡系统资源,确保任务公平性和响应性。