IO优先级-ioprio
一、ioprio简介
1. ioprio.rst
注: 本文翻译自 msm-5.4/Documentation/block/ioprio.rst
随着 cfq v3(又称 cfq-ts 或时间片 cfq)的引入,文件读操作已支持基本的 I/O 优先级设置。这使得用户可以对进程或进程组进行 I/O nice 调整,类似于 CPU 调度领域长期以来支持的 nice 机制。本文档主要介绍 cfq 当前的功能;其他 I/O 调度器目前尚不支持 I/O 优先级。
1.1 调度类
cfq 实现了三种通用调度类,用于决定如何为进程服务 I/O 请求。
(1) IOPRIO_CLASS_RT:这是实时 I/O 调度类。该调度类在系统中拥有高于其他所有类的优先级,每次都会优先访问磁盘。因此需要谨慎使用,单个 RT 类 I/O 进程可能会导致整个系统陷入饥饿。在 RT 类内部,有 8 个等级的类数据,精确控制该进程每次服务时需要占用磁盘的时长。
未来可能会改为通过传入期望数据速率的方式,更直观地映射到性能。
(2) IOPRIO_CLASS_BE:这是最佳努力调度类,是所有未显式设置 I/O 优先级的进程的默认类。类数据决定进程能获得多少 I/O 带宽,与 CPU nice 等级直接对应,但粒度更粗。0 是最高 BE 优先级,7 是最低。CPU nice 等级与 I/O nice 等级之间的映射关系为:
io_nice = (cpu_nice + 20) / 5。//详见 task_nice_ioprio()
(3) IOPRIO_CLASS_IDLE:这是空闲调度类,运行在此级别的进程只有在没有其他人需要磁盘时才能获得 I/O 时间。空闲类没有类数据,因为在这里类数据并不适用。
1.2 工具
(1) 用法
# ionice --help Toybox 0.8.4-android 多调用二进制文件:https://landley.net/toybox(参见 toybox --help) usage:ionice [-t] [-c CLASS] [-n LEVEL] [COMMAND...|-p PID] 更改进程的 I/O 调度优先级。不带参数(或仅使用 -p)时,显示进程现有的 I/O 调度类和优先级。 -c CLASS = 1-3:1(实时),2(尽力而为,默认),3(空闲时) -n LEVEL = 0-7:(0 为最高优先级,默认值为 5, 见上面的转换公式) -p 指定这次操作的 pid。 -t 忽略设置 I/O 优先级失败的情况。 系统默认的 I/O 优先级通常为 -c 2 -n 4。
(2) 示例
以下是一个 ionice 工具示例。使用方法::
# ionice -c<class> -n<level> -p <pid>
如果不指定 pid,则默认对当前进程生效。I/O 优先级设置在 fork 时会被继承,因此可以使用 ionice 以指定优先级启动进程,比如以最佳努力调度类、最高优先级运行 ls:
# ionice -c2 -n0 /bin/ls
对于已在运行的进程,可以指定 pid,如将 pid=100 修改为以实时调度类、优先级 2 运行:
# ionice -c1 -n2 -p 100
(3) ionice代码实现
//android/external/toybox/toys/other/ionice.c #define FOR_ionice #include "toys.h" #include <sys/syscall.h> GLOBALS( long p, n, c; ) static int ioprio_get(void) { return syscall(__NR_ioprio_get, 1, (int)TT.p); } static int ioprio_set(void) { int prio = ((int)TT.c << 13) | (int)TT.n; return syscall(__NR_ioprio_set, 1, (int)TT.p, prio); } /* 这是 ionice 实现代码 */ void ionice_main(void) { if (!TT.p && !toys.optc) error_exit("Need -p or COMMAND"); if (toys.optflags == FLAG_p) { int p = ioprio_get(); xprintf("%s: prio %d\n", (char *[]){"unknown", "Realtime", "Best-effort", "Idle"}[(p>>13)&3], p&7); } else { if (-1 == ioprio_set() && !(toys.optflags&FLAG_t)) perror_exit("set"); if (!TT.p) xexec(toys.optargs); } } /* 这是 iorenice 实现代码 */ void iorenice_main(void) { char *classes[] = {"none", "rt", "be", "idle"}; TT.p = atolx(*toys.optargs); if (toys.optc == 1) { int p = ioprio_get(); if (p == -1) perror_exit("read priority"); TT.c = (p>>13)&3; p &= 7; xprintf("Pid %ld, class %s (%ld), prio %d\n", TT.p, classes[TT.c], TT.c, p); return; } for (TT.c = 0; TT.c<4; TT.c++) if (!strcmp(toys.optargs[toys.optc-1], classes[TT.c])) break; if (toys.optc == 3 || TT.c == 4) TT.n = atolx(toys.optargs[1]); else TT.n = 4; TT.c &= 3; if (-1 == ioprio_set()) perror_exit("set"); }
(4) 系统调用封装
使用内核定义好的宏,可以自己封装如下:
#include <sys/syscall.h> /* from kernel ioprio.h */ #define IOPRIO_CLASS_SHIFT (13) #define IOPRIO_PRIO_MASK ((1UL << IOPRIO_CLASS_SHIFT) - 1) #define IOPRIO_PRIO_CLASS(mask) ((mask) >> IOPRIO_CLASS_SHIFT) #define IOPRIO_PRIO_DATA(mask) ((mask) & IOPRIO_PRIO_MASK) #define IOPRIO_PRIO_VALUE(class, data) (((class) << IOPRIO_CLASS_SHIFT) | data) enum { IOPRIO_WHO_PROCESS = 1, IOPRIO_WHO_PGRP, IOPRIO_WHO_USER, }; enum { IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE, }; #define ioprio_valid(mask) (IOPRIO_PRIO_CLASS((mask)) != IOPRIO_CLASS_NONE) /* bionic库中没有,需要自己封装,失败返回-1 */ static int ioprio_get(pid_t tid) { return syscall(__NR_ioprio_get, IOPRIO_WHO_PROCESS, tid); } static int ioprio_set(pid_t tid, int io_class, int io_data) { int prio = IOPRIO_PRIO_VALUE(io_class, io_data); return syscall(__NR_ioprio_set, IOPRIO_WHO_PROCESS, tid, prio); }
2.mq-deadline 对io优先级的支持
简答:支持,但是“有限支持”。mq-deadline 对 I/O 优先级的支持主要体现在 class 级别(RT/BE/IDLE):
RT 会比 BE 更优先, IDLE 最低优先级, NONE 通常按 BE 处理。但它一般不像 BFQ/CFQ 那样细致使用 BE 的 0-7 level 做带宽配额,所以你会感觉:
ionice -c<class> 有一定效果
ionice -n<level> 效果弱或不稳定
二、ioprio优先级
1. 优先级查看与设置
ionice -p <PID> //查看当前进程的I/O优先级 ionice -c 1 -n 0 -p <PID> //将进程ID为<PID>的进程设置为实时类最高优先级 ionice -c 2 -n 7 -p <PID> //将进程ID为<PID>的进程设置为尽力而为类最低优先级 ionice -c 3 -p <PID> //将进程ID为<PID>的进程设置为空闲类
实测如下:
# ps -e | grep surfaceflinger system 601 1 2823008 54260 do_epoll_wait 0 S surfaceflinger # ionice -p 601 unknown: prio 0 # ionice -c 1 -n 0 -p 601 //设置实时类最高优先级 # ionice -p 601 Realtime: prio 0 # ionice -c 2 -n 7 -p 601 //设置为尽力而为类最低优先级 # ionice -p 601 Best-effort: prio 7 # ionice -c 3 -p 601 # ionice -p 601 Idle: prio 5 # ionice -c 0 -n 0 -p 602 //设置回 unknow-0 状态 # ionice -p 602 unknown: prio 0
3. 代码优先级设置
系统调用定义位置如下,但是bionic库没有帮助我们进行封装,需要自己封装,封装方法如上。
SYSCALL_DEFINE3(ioprio_set, int, which, int, who, int, ioprio) //block/ioprio.c SYSCALL_DEFINE2(ioprio_get, int, which, int, who) //block/ioprio.c
进程保存 ioprio 的位置:
static inline int get_current_ioprio(void) //ioprio.h { struct io_context *ioc = current->io_context; if (ioc) return ioc->ioprio; return IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, 0); //对应"unknown: prio 0" }
4. "unknown: prio 0"的含义
上面执行 ionice -p 601 时,看到打印结果为 unknown: prio 0。这表示该线程/进程的 I/O 优先级类是 IOPRIO_CLASS_NONE(值为 0, 显示为 "unknown"),优先级等级也是 0, 但 class="0" 时等级无实质意义。
IOPRIO_CLASS_NONE 意味着该进程没有显式设置 I/O 优先级,内核会将其按 CPU 调度优先级(nice 值)折算为 BE 等级来处理,转换公式:
I/O BE level = (nice + 20) / 5 //范围 0-7, 见 task_nice_ioprio() 实现: static inline int task_nice_ioprio(struct task_struct *task) { return (task_nice(task) + 20) / 5; }
比如 nice=0 即 BE 等级 4,这也是BE-4优先级比较多的原因。如果想拿到 nice 值,可以:
cat /proc/2675/stat | awk '{print $19}'(nice 值在第 19 列)
IO调度类也会跟随CPU调度类,见 task_nice_ioclass():
static inline int task_nice_ioclass(struct task_struct *task) { if (task->policy == SCHED_IDLE) return IOPRIO_CLASS_IDLE; else if (task_is_realtime(task)) return IOPRIO_CLASS_RT; else return IOPRIO_CLASS_BE; }
即,在没有调整IO优先级的情况下,IO优先级跟随CPU调度上的优先级,包括调度类和调度优先级####。
5. 内核 ioprio 设置
示例:
#define BTRFS_IOPRIO_READA (IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 0)) reada_start_machine_worker() //fs/btrfs/reada.c { int old_ioprio = IOPRIO_PRIO_VALUE(task_nice_ioclass(current), task_nice_ioprio(current)); set_task_ioprio(current, BTRFS_IOPRIO_READA); //还有 get_task_ioprio(p) ... set_task_ioprio(current, old_ioprio); }
保存和恢复ioprio:
old_ioprio = get_current_ioprio(); set_task_ioprio(current, IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 0)); ... do actual kcopyd IO submission ... set_task_ioprio(current, old_ioprio);
kworker现场中调用 set_task_ioprio() 设置IO优先级可以无视 selinux 权限。
三、项目实践
1. 主要关注userdata
(1) 看使用的IO调度类
# for F in `find /sys/devices -name scheduler`; do echo $F; cat $F; done /sys/devices/platform/vdevs/1c0e0000.virtio_blk/virtio8/block/userdata/queue/scheduler mq-deadline kyber [bfq] none /sys/devices/virtual/block/dm-4/queue/scheduler none ...
(2) 看挂载点
在Android中 userdata 默认挂载在 /data 下。dm-4 是 userdata 加密的,挂了一层 dm crypt。
# mount | grep /data /dev/block/dm-4 on /data type f2fs (rw,lazytime,seclabel,nosuid,nodev,noatime,background_gc=on,discard,no_heap,user_xattr,inline_xattr,acl,inline_data,inline_dentry,flush_merge,extent_cache,mode=adaptive,
active_logs=6,alloc_mode=default,fsync_mode=posix)
cat /proc/mounts 每行的格式是固定的,都是" <设备> <挂载点> <文件系统> <选项> ..."
读大小,可以发现大小是一样的:
# blockdev --getsize64 /dev/block/userdata 98781769728 # blockdev --getsize64 /dev/block/dm-4 //userdata加密的,挂了一层 dm crypt 98781769728
为什么说它对应 userdata:
在 Android 上 userdata 分区的逻辑挂载点是 /data 目录。现代 Android(动态分区/加密) 常见是 /dev/block/by-name/userdata --> dm-* 映射,不一定直接显示 /dev/block/by-name/userdata,而是显示最终映射设备,这里是 dm-4。
后面大量同源挂载也都来自 dm-4(如 /data/user/0、/data_mirror/...),进一步印证 dm-4 就是 data 基座。
posted on 2026-06-03 14:22 Hello-World3 阅读(56) 评论(0) 收藏 举报
浙公网安备 33010602011771号