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-31(实时),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)    收藏  举报

导航