机制详解:ionice 工具、系统调用与 bfq/mq-deadline 调度实现)
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/Oio nice。正如原文档所述I/O 优先级的支持依赖于具体的 I/O 调度器目前由bfq和mq-deadline两个调度器支持。这意味着即使你成功通过系统调用设置了优先级若当前块设备使用的是 multiqueue-deadlinemq-deadline之外的调度器如 kyber、none该优先级值仍会被记录但不会改变实际的请求排序行为。从源码结构看优先级的存储—使用链路是用户态通过ioprio_set()系统调用把优先级写入任务的io_context实现见 block/ioprio.c任务发起 I/O 时内核把当前任务的 I/O 优先级填入bio-bi_ioprio见 block/blk-core.c 与 block/blk-ioprio.cI/O 调度器如 mq-deadline读取请求携带的优先级类映射到调度器内部的优先级桶见 block/mq-deadline.c。2. 三个调度类RT、BE、IDLE内核为 I/O 优先级实现了三个通用调度类决定进程如何获得磁盘服务原文档核心内容调度类取值语义类内数据levelIOPRIO_CLASS_RT1实时 I/O 类。优先级高于系统中任何其他类该类进程每次都能最先访问磁盘。需谨慎使用——一个 RT 类 I/O 进程可能饿死整个系统。8 个级别0–7决定该进程每次被服务时对磁盘时长的需求原文档提到未来可能改为更可映射到性能的期望数据速率方式IOPRIO_CLASS_BE2尽力而为类是未显式设置 I/O 优先级的所有进程的默认类。级别值直接映射到 CPU nice 级别粒度更粗0 最高7 最低。映射公式io_nice (cpu_nice 20) / 50–7IOPRIO_CLASS_IDLE3空闲类。进程只有在没有任何其他进程需要磁盘时才能获得 I/O 时间。该类没有类内数据无意义。无IOPRIO_CLASS_NONE取值 0表示未显式设置此时内核会回退到按 CPU nice 推导的 BE/RT/IDLE 类。该语义在 include/linux/ioprio.h 中有直接实现task_nice_ioprio()返回(task_nice(task) 20) / 5task_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–12I/O hints如IOPRIO_HINT_DEV_DURATION_LIMIT_1..7用于在不影响调度排序的前提下提示设备限制命令执行时长目前仅对支持命令持续时间限制特性的 SCSI/ATA 设备有效低 3 位级别level0–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 -cclass -nlevel -ppid如果不给 pid默认作用于当前进程。I/O 优先级设置在 fork 时继承因此可以用ionice以指定级别直接启动进程# ionice -c2 -n0 /bin/ls以上命令会以 best-effort 调度类、最高优先级0 级运行ls。对已经在运行的进程可以给出 pid# ionice -c1 -n2 -p100以上命令把 pid 100 的进程改为 realtime 调度类、优先级 2。注意参数约定-c取值为 1RT/2BE/3IDLE-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 0NONE被工具归一化为 BEIDLE 类强制ioprio 7。-p参数在who0时等价于当前进程这与内核侧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 时返回-EINVALNONE 类不允许携带级别数据。IOPRIO_CLASS_INVALID及未知类一律-EINVAL。这解释了实际操作中普通用户设置 BE/IDLE 成功、设置 RT 报Operation not permitted的现象也印证了第 2 节 RT 类需谨慎使用的告诫。4.2 三种作用对象进程、进程组、用户ioprio_set()block/ioprio.c#L65-L136按which参数分三类处理who为 0 时均指自身whichwho0who≠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_DEFAULTIOPRIO_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#L939bio-bi_ioprio get_current_ioprio();block/blk-ioprio.cbio 提交路径上的优先级填充逻辑含为特定场景直接标记 RT 类的分支同步 I/O 路径中bio-bi_ioprio iocb-ki_ioprioblock/fops.cbio clone 时复制bi_ioprioblock/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 pidIDLE 类数据库等关键进程ionice -c2 -n0 -p pidBE 类最高级实时媒体采集需特权且需调度器为 mq-deadline/bfqsudo ionice -c1 -n0 -p pid以指定级别启动新进程ionice -c2 -n4 cp src dst不写-p工具会exec后续命令。继承性I/O 优先级随 fork 继承适合在包装命令时一次性设定。权限RT 类需要CAP_SYS_ADMIN或CAP_SYS_NICEBE/IDLE 无限制见ioprio_check_cap()。生效前提目标块设备当前 I/O 调度器须为 mq-deadline 或 bfq否则优先级只被记录而不影响排序。语义细节ionice/ioprio_get查询到 none 表示从未显式设置实际生效值由 CPU nice 及调度策略推导io_nice (cpu_nice 20) / 5CPU 实时/IDLE 策略进程会默认落到 RT/IDLE 类。参考文件内容路径本文核心文档2005-03-11Jens AxboeDocumentation/block/ioprio.rst系统调用与权限检查实现block/ioprio.c内核内部头默认值、nice 映射include/linux/ioprio.hUAPI 头类/级别/hint 位布局include/uapi/linux/ioprio.hmq-deadline 优先级映射block/mq-deadline.cbio 优先级填充block/blk-ioprio.c、block/blk-core.c【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考