Linux 内核块层 I/O 优先级(ioprio)机制详解:ionice 工具、系统调用与 bfq/mq-deadline 调度实现
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文以内核文档 Documentation/block/ioprio.rst 为核心,系统讲解 Linux 块层 I/O 优先级体系:三大调度类(RT / BE / IDLE)的语义、ionice工具的实际用法与完整示例源码、ioprio_set/ioprio_get系统调用的内核实现与权限约束,以及优先级值如何从任务一路传递到 mq-deadline 等 I/O 调度器中生效。读完本文,你可以为任意进程/进程组精确设定磁盘 I/O 优先级,并理解内核在哪些环节校验和使用这些优先级。
1. 概述:I/O nice 是什么
块层 I/O 优先级功能允许用户像设置 CPU nice 值一样为进程或进程组"调好"磁盘 I/O(io nice)。正如原文档所述,I/O 优先级的支持依赖于具体的 I/O 调度器,目前由bfq和mq-deadline两个调度器支持。这意味着:即使你成功通过系统调用设置了优先级,若当前块设备使用的是 multiqueue-deadline(mq-deadline)之外的调度器(如 kyber、none),该优先级值仍会被记录,但不会改变实际的请求排序行为。
从源码结构看,优先级的"存储—使用"链路是:
- 用户态通过
ioprio_set()系统调用把优先级写入任务的io_context(实现见 block/ioprio.c); - 任务发起 I/O 时,内核把当前任务的 I/O 优先级填入
bio->bi_ioprio(见 block/blk-core.c 与 block/blk-ioprio.c); - I/O 调度器(如 mq-deadline)读取请求携带的优先级类,映射到调度器内部的优先级桶(见 block/mq-deadline.c)。
2. 三个调度类:RT、BE、IDLE
内核为 I/O 优先级实现了三个通用调度类,决定进程如何获得磁盘服务(原文档核心内容):
| 调度类 | 取值 | 语义 | 类内数据(level) |
|---|---|---|---|
IOPRIO_CLASS_RT | 1 | 实时 I/O 类。优先级高于系统中任何其他类,该类进程每次都能最先访问磁盘。需谨慎使用——一个 RT 类 I/O 进程可能饿死整个系统。 | 8 个级别(0–7),决定该进程每次被服务时对磁盘时长的需求;原文档提到未来可能改为更可映射到性能的"期望数据速率"方式 |
IOPRIO_CLASS_BE | 2 | 尽力而为类,是未显式设置 I/O 优先级的所有进程的默认类。级别值直接映射到 CPU nice 级别(粒度更粗):0 最高,7 最低。映射公式:io_nice = (cpu_nice + 20) / 5 | 0–7 |
IOPRIO_CLASS_IDLE | 3 | 空闲类。进程只有在没有任何其他进程需要磁盘时才能获得 I/O 时间。该类没有类内数据(无意义)。 | 无 |
IOPRIO_CLASS_NONE(取值 0)表示"未显式设置",此时内核会回退到按 CPU nice 推导的 BE/RT/IDLE 类。该语义在 include/linux/ioprio.h 中有直接实现:task_nice_ioprio()返回(task_nice(task) + 20) / 5,task_nice_ioclass()会把SCHED_IDLE策略的任务映射为IOPRIO_CLASS_IDLE、实时/截止时间(DL)策略任务映射为IOPRIO_CLASS_RT,其余为IOPRIO_CLASS_BE。也就是说,CPU 实时进程在未设 I/O 优先级时默认走 I/O RT 类,CPU 空闲策略进程默认走 I/O IDLE 类——这一细节是原文档未展开的、由源码确认的事实。
2.1 优先级值的位布局
一个 16 位ioprio值的完整位布局由 UAPI 头 include/uapi/linux/ioprio.h 定义:
- 高 3 位(bit 13 起,
IOPRIO_CLASS_SHIFT = 13):优先级类,共 8 个类槽位(IOPRIO_NR_CLASSES = 8),其中IOPRIO_CLASS_INVALID = 7为保留的非法值; - 中间 10 位(bit 3–12):I/O hints(如
IOPRIO_HINT_DEV_DURATION_LIMIT_1..7),用于在不影响调度排序的前提下提示设备限制命令执行时长,目前仅对支持"命令持续时间限制"特性的 SCSI/ATA 设备有效; - 低 3 位:级别(level),0–7,仅 RT 与 BE 类使用。
组合与拆解的宏为IOPRIO_PRIO_VALUE(prioclass, priolevel)、IOPRIO_PRIO_CLASS(ioprio)、IOPRIO_PRIO_LEVEL(ioprio);未设置状态用IOPRIO_NORM(即IOPRIO_BE_NORM = 4)的默认值表示,内核内部IOPRIO_DEFAULT定义为IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, 0)(include/linux/ioprio.h#L14)。
3. ionice 工具:用法与完整实现
3.1 命令用法
原文档给出的ionice用法为:
# ionice -c<class> -n<level> -p<pid>如果不给 pid,默认作用于当前进程。I/O 优先级设置在 fork 时继承,因此可以用ionice以指定级别直接启动进程:
# ionice -c2 -n0 /bin/ls以上命令会以 best-effort 调度类、最高优先级(0 级)运行ls。对已经在运行的进程,可以给出 pid:
# ionice -c1 -n2 -p100以上命令把 pid 100 的进程改为 realtime 调度类、优先级 2。注意参数约定:-c取值为 1(RT)/2(BE)/3(IDLE),-n为 0–7 的级别;对 IDLE 类,级别无意义,工具会强制置为 7(见下文源码)。
3.2 文档自带的 ionice.c 参考实现
原文档内嵌了一个可直接编译的ionice.c示例工具,它通过裸系统调用号(不同架构的__NR_ioprio_set/__NR_ioprio_get)实现设置与查询。要点如下(代码完整继承自文档,供理解内核接口使用):
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <getopt.h> #include <unistd.h> #include <sys/ptrace.h> #include <asm/unistd.h> extern int sys_ioprio_set(int, int, int); extern int sys_ioprio_get(int, int); #if defined(__i386__) #define __NR_ioprio_set 289 #define __NR_ioprio_get 290 #elif defined(__ppc__) #define __NR_ioprio_set 273 #define __NR_ioprio_get 274 #elif defined(__x86_64__) #define __NR_ioprio_set 251 #define __NR_ioprio_get 252 #else #error "Unsupported arch" #endif static inline int ioprio_set(int which, int who, int ioprio) { return syscall(__NR_ioprio_set, which, who, ioprio); } static inline int ioprio_get(int which, int who) { return syscall(__NR_ioprio_get, which, who); } enum { IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE, }; enum { IOPRIO_WHO_PROCESS = 1, IOPRIO_WHO_PGRP, IOPRIO_WHO_USER, }; #define IOPRIO_CLASS_SHIFT 13 const char *to_prio[] = { "none", "realtime", "best-effort", "idle", }; int main(int argc, char *argv[]) { int ioprio = 4, set = 0, ioprio_class = IOPRIO_CLASS_BE; int c, pid = 0; while ((c = getopt(argc, argv, "+n:c:p:")) != EOF) { switch (c) { case 'n': ioprio = strtol(optarg, NULL, 10); set = 1; break; case 'c': ioprio_class = strtol(optarg, NULL, 10); set = 1; break; case 'p': pid = strtol(optarg, NULL, 10); break; } } switch (ioprio_class) { case IOPRIO_CLASS_NONE: ioprio_class = IOPRIO_CLASS_BE; break; case IOPRIO_CLASS_RT: case IOPRIO_CLASS_BE: break; case IOPRIO_CLASS_IDLE: ioprio = 7; break; default: printf("bad prio class %d\n", ioprio_class); return 1; } if (!set) { if (!pid && argv[optind]) pid = strtol(argv[optind], NULL, 10); ioprio = ioprio_get(IOPRIO_WHO_PROCESS, pid); printf("pid=%d, %d\n", pid, ioprio); if (ioprio == -1) perror("ioprio_get"); else { ioprio_class = ioprio >> IOPRIO_CLASS_SHIFT; ioprio = ioprio & 0xff; printf("%s: prio %d\n", to_prio[ioprio_class], ioprio); } } else { if (ioprio_set(IOPRIO_WHO_PROCESS, pid, ioprio | ioprio_class << IOPRIO_CLASS_SHIFT) == -1) { perror("ioprio_set"); return 1; } if (argv[optind]) execvp(argv[optind], &argv[optind]); } return 0; }从这份实现可以读出接口约定:
ioprio参数是"类 + 级别"的合成值:设置时执行ioprio | ioprio_class << IOPRIO_CLASS_SHIFT,即级别放低 13 位以下、类放高位;查询时再分别右移 13 位取类、与0xff相与取级别。- 不传
-n/-c时是查询模式:直接调用ioprio_get(IOPRIO_WHO_PROCESS, pid)并打印类名与级别。 -c 0(NONE)被工具归一化为 BE;IDLE 类强制ioprio = 7。-p参数在who=0时等价于"当前进程",这与内核侧ioprio_set中if (!who) p = current;的处理一致(block/ioprio.c#L80-L87)。
4. 内核侧实现:ioprio_set / ioprio_get 系统调用
ioprio_set/ioprio_get的完整实现位于 block/ioprio.c,文件头注释明确说明系统调用"与 getpriority/setpriority 类似",并给出了 BE 类 2 级的设置示例:
unsigned int prio = (IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT) | 2; ioprio_set(PRIO_PROCESS, pid, prio);4.1 权限检查:RT 类需要特权
ioprio_check_cap()(block/ioprio.c#L33-L63)的校验规则:
IOPRIO_CLASS_RT:调用者必须拥有CAP_SYS_ADMIN或CAP_SYS_NICE之一,否则返回-EPERM。源码注释解释了先后顺序:历史上只检查CAP_SYS_ADMIN,它会被 SELinux 等安全模块隐式放行给 pid 0;现在先检查 CAP_SYS_ADMIN,避免对可能缺失的CAP_SYS_NICE权限产生不必要的 AVC 拒绝日志。IOPRIO_CLASS_BE/IOPRIO_CLASS_IDLE:无权限限制。IOPRIO_CLASS_NONE:仅当 level 非 0 时返回-EINVAL(NONE 类不允许携带级别数据)。IOPRIO_CLASS_INVALID及未知类:一律-EINVAL。
这解释了实际操作中"普通用户设置 BE/IDLE 成功、设置 RT 报Operation not permitted"的现象,也印证了第 2 节 RT 类"需谨慎使用"的告诫。
4.2 三种作用对象:进程、进程组、用户
ioprio_set()(block/ioprio.c#L65-L136)按which参数分三类处理,who为 0 时均指"自身":
| which | who=0 | who≠0 | 行为 |
|---|---|---|---|
IOPRIO_WHO_PROCESS(1) | 当前任务current | 按 vpid 查找任务 | 对单个任务调用set_task_ioprio() |
IOPRIO_WHO_PGRP(2) | 当前进程组 | 按 pid 查找进程组 | 持tasklist_lock遍历组内每个线程逐一设置 |
IOPRIO_WHO_USER(3) | 当前用户 | 按 uid 查找user_struct | 遍历该用户名下所有任务逐一设置 |
对应地,ioprio_get()(block/ioprio.c#L180-L249)对进程组/用户范围查询时,返回组内所有任务优先级的最小值(ioprio_best()即min(aprio, bprio)),即"组里最优先的那个"。
一个容易踩坑的细节:ioprio_get(IOPRIO_WHO_PROCESS, pid)返回的是用户态设置的原始值(get_task_raw_ioprio(),block/ioprio.c#L152-L173)。若任务从未显式设置过优先级,返回IOPRIO_DEFAULT(IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, 0),类显示为 "none"),而非按 nice 推导出的等效值——源码注释说明这是为了保持历史行为,并让用户态能区分"未设置(将按任务 nice 值被覆盖)"与"已显式设置"。而调度器真正看到的有效值则由__get_task_ioprio()(include/linux/ioprio.h#L57-L74)计算:类为 NONE 时回退为IOPRIO_PRIO_VALUE(task_nice_ioclass(p), task_nice_ioprio(p))。
此外,设置/读取都会先经过 LSM 钩子security_task_setioprio()/security_task_getioprio(),SELinux/AppArmor 等可以在此拦截或修改行为。
5. 优先级如何到达 I/O 调度器
5.1 从任务到 bio:优先级的传递
当块 I/O 路径创建 bio 时,内核把当前任务的有效 I/O 优先级写入bio->bi_ioprio。关键代码:
- block/blk-core.c#L939:
bio->bi_ioprio = get_current_ioprio(); - block/blk-ioprio.c:bio 提交路径上的优先级填充逻辑(含为特定场景直接标记 RT 类的分支);
- 同步 I/O 路径中
bio->bi_ioprio = iocb->ki_ioprio(block/fops.c),bio clone 时复制bi_ioprio(block/bio.c#L859)。
5.2 mq-deadline 如何消费优先级
mq-deadline 调度器把 I/O 优先级类直接映射为调度器内部优先级(block/mq-deadline.c#L106-L112):
/* Maps an I/O priority class to a deadline scheduler priority. */ static const enum dd_prio ioprio_class_to_prio[] = { [IOPRIO_CLASS_NONE] = DD_BE_PRIO, [IOPRIO_CLASS_RT] = DD_RT_PRIO, [IOPRIO_CLASS_BE] = DD_BE_PRIO, [IOPRIO_CLASS_IDLE] = DD_IDLE_PRIO, };即 RT 与 BE(含 NONE)落入同一 BE 桶但调度器对 RT 请求有独立的高优先级处理,IDLE 请求仅在无其他请求时派发。请求派发与超时时都会再次读取IOPRIO_PRIO_CLASS(...)决定行为(如 block/mq-deadline.c#L309-L310、block/mq-deadline.c#L626-L633)。
从源码结构看,bfq 调度器(block/bfq-iosched.c)同样读取请求/任务的 I/O 优先级类来划分服务分组,与文档中"bfq 与 mq-deadline 均支持 I/O 优先级"的表述一致。
6. 实操小结与注意事项
- 典型用法:
- 让后台批量拷贝不抢占磁盘:
ionice -c3 -n7 -p <pid>(IDLE 类); - 数据库等关键进程:
ionice -c2 -n0 -p <pid>(BE 类最高级); - 实时媒体采集(需特权,且需调度器为 mq-deadline/bfq):
sudo ionice -c1 -n0 -p <pid>; - 以指定级别启动新进程:
ionice -c2 -n4 cp src dst(不写-p,工具会exec后续命令)。
- 让后台批量拷贝不抢占磁盘:
- 继承性:I/O 优先级随 fork 继承,适合在包装命令时一次性设定。
- 权限:RT 类需要
CAP_SYS_ADMIN或CAP_SYS_NICE;BE/IDLE 无限制(见ioprio_check_cap())。 - 生效前提:目标块设备当前 I/O 调度器须为 mq-deadline 或 bfq,否则优先级只被记录而不影响排序。
- 语义细节:
ionice/ioprio_get查询到 "none" 表示从未显式设置,实际生效值由 CPU nice 及调度策略推导(io_nice = (cpu_nice + 20) / 5);CPU 实时/IDLE 策略进程会默认落到 RT/IDLE 类。
参考文件
| 内容 | 路径 |
|---|---|
| 本文核心文档(2005-03-11,Jens Axboe) | Documentation/block/ioprio.rst |
| 系统调用与权限检查实现 | block/ioprio.c |
| 内核内部头:默认值、nice 映射 | include/linux/ioprio.h |
| UAPI 头:类/级别/hint 位布局 | include/uapi/linux/ioprio.h |
| mq-deadline 优先级映射 | block/mq-deadline.c |
| bio 优先级填充 | block/blk-ioprio.c、block/blk-core.c |
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考