Linux 内核 mq-deadline IO 调度器调优参数详解:read_expire、fifo_batch 与 front_merges 的源码级解析
2026/9/14 16:41:56 网站建设 项目流程

Linux 内核 mq-deadline IO 调度器调优参数详解:read_expire、fifo_batch 与 front_merges 的源码级解析

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

本文基于内核文档 Documentation/block/deadline-iosched.rst,完整讲解 deadline(mq-deadline)IO 调度器暴露给高级用户的五个可调参数:read_expirewrite_expirefifo_batchwrites_starvedfront_merges的含义与调优方法,并结合 block/mq-deadline.c 源码还原每个参数在请求入队、合并与派发路径中的实际作用,帮助你在块设备上做低延迟/高吞吐权衡时有据可依。

1. 背景:deadline 调度器要解决什么问题

deadline 调度器的设计目标是为每个请求保证一个"开始服务时间"。由于读请求对交互延迟更敏感,调度器分别维护读、写两条队列,并在各自的"截止时间"(expire)到来前尽量完成派发。文档原文明确了这一取向:

The goal of the deadline io scheduler is to attempt to guarantee a start service time for a request. As we focus mainly on read latencies, this is tunable.

在当前内核中,deadline 调度器以 blk-mq 框架的适配实现存在:block/mq-deadline.c 文件头注释写着"MQ Deadline i/o scheduler - adaptation of the legacy deadline scheduler, for the blk-mq scheduling framework",其注册名为mq-deadline,别名仍保留为deadline(见 elevator_type 定义):

static struct elevator_type mq_deadline = { ... .elevator_name = "mq-deadline", .elevator_alias = "deadline", ... };

因此 sysfs 中你看到的是mq-deadline,而写deadline也能通过别名生效。

2. 如何查看与切换 IO 调度器

关联文档将"如何选择调度器"指向 Documentation/block/switching-sched.rst,其核心操作如下(原文内容完整保留):

每个 IO 队列都有一组调度器调优参数,入口位于(假设 sysfs 已挂载在 /sys):

/sys/block/<device>/queue/iosched

若 sysfs 未挂载:

# mount none /sys -t sysfs

可以在运行期为某个块设备在线切换调度器(可选mq-deadlinenonebfqkyber):

echo SCHEDNAME > /sys/block/DEV/queue/scheduler

其中SCHEDNAME是已注册的调度器名,DEV是设备名(如sda)。查看可用调度器列表及当前值:

# cat /sys/block/sda/queue/scheduler [mq-deadline] kyber bfq none # echo none >/sys/block/sda/queue/scheduler # cat /sys/block/sda/queue/scheduler [none] mq-deadline kyber bfq

选中的括号项即当前生效的调度器。切换到 mq-deadline 后,其调优参数就会出现在/sys/block/<device>/queue/iosched/目录下。

3. 调优参数全解(含源码默认值与取值范围)

以下五个 sysfs 属性在 deadline_attrs 表中注册:

static const struct elv_fs_entry deadline_attrs[] = { DD_ATTR(read_expire), DD_ATTR(write_expire), DD_ATTR(writes_starved), DD_ATTR(front_merges), DD_ATTR(fifo_batch), DD_ATTR(prio_aging_expire), __ATTR_NULL };

3.1 read_expire(毫秒)

文档语义:当一个读请求进入 IO 调度器时,它会被分配一个截止时间,等于"当前时间 + read_expire"(单位毫秒)。该参数用于控制读请求允许在调度器中等待的最大时长,是读延迟调优的核心旋钮。

源码印证

  • 默认值定义在 block/mq-deadline.c 第 30 行:
static const int read_expire = HZ / 2; /* max time before a read is submitted. */

即默认 500 ms(HZ 为 1000 时)。

  • 请求入队时的截止时刻计算在 dd_insert_request:
rq->fifo_time = jiffies + dd->fifo_expire[data_dir]; list_add_tail(&rq->queuelist, &per_prio->fifo_list[data_dir]);
  • sysfs 写入范围:STORE_JIFFIES(deadline_read_expire_store, &dd->fifo_expire[DD_READ], 0, INT_MAX)(第 771 行),单位是毫秒,内核通过msecs_to_jiffies()转换后存入,因此取值范围 0~INT_MAX,单位 ms;读取时再由jiffies_to_msecs()转回。

调优含义read_expire越小,读请求越快被"强制"派发,读延迟上限越低,但顺序合并窗口也被压缩;取 0 相当于每个读请求立即过期。

3.2 write_expire(毫秒)

文档语义:与 read_expire 相同,但作用于写请求。

源码印证:默认值在 第 31 行:

static const int write_expire = 5 * HZ; /* ditto for writes, these limits are SOFT! */

即默认 5000 ms。注意源码注释特别标注这些限制是"SOFT"(软限制)——写队列通常按扇区顺序派发,到期检查只是兜底手段。sysfs 写入范围同为 0~INT_MAX ms(deadline_write_expire_store,第 772 行)。

调优含义:写请求默认容忍更长的排队时间,以便按扇区聚合成顺序写、提升吞吐。若你的负载对写延迟敏感(如日志型写入),可适当调小该值。

3.3 fifo_batch(请求个数)

文档语义(完整继承原文):请求被按数据方向(读或写)分组为若干"批(batch)",每批内按扇区递增顺序服务。为限制额外寻道,deadline 的到期检查只在批与批之间进行。fifo_batch控制每批的最大请求数。

This parameter tunes the balance between per-request latency and aggregate throughput. When low latency is the primary concern, smaller is better (where a value of 1 yields first-come first-served behaviour). Increasing fifo_batch generally improves throughput, at the cost of latency variation.

源码印证

  • 默认值(第 38 行):
static const int fifo_batch = 16; /* # of sequential requests treated as one ... */
  • 派发时的批处理逻辑在 __dd_dispatch_request:若上一次派发的方向上仍有按扇区排序的后续请求,且当前批计数dd->batching < dd->fifo_batch,则直接继续派发该请求(goto dispatch_request),不重新检查到期状态:
rq = deadline_next_request(per_prio, dd->last_dir); if (rq && dd->batching < dd->fifo_batch) { /* we have a next request and are still entitled to batch */ data_dir = rq_data_dir(rq); goto dispatch_request; }
  • sysfs 写入范围:STORE_INT(deadline_fifo_batch_store, &dd->fifo_batch, 0, INT_MAX)(第 776 行)。

调优含义fifo_batch=1时退化为严格的先来先服务(每派一个请求就重新检查到期与方向);调大(如 16 以上的值)会让顺序请求成批派发,吞吐更高但单请求延迟波动变大。

3.4 writes_starved(派发次数)

文档语义(完整继承原文):当把请求从调度器队列移到块设备派发队列时,调度器总是优先读请求。但也不希望写被无限饿死,因此writes_starved控制"读相对写被优先处理多少次"。读被优先writes_starved次之后,调度器会按与读相同的准则派发若干写。

源码印证

  • 默认值(第 37 行):
static const int writes_starved = 2; /* max times reads can starve a write */
  • 派发决策(__dd_dispatch_request 第 346-351 行):只要读 FIFO 非空,就检查写 FIFO 中请求的饿死计数dd->starved,当dd->starved++ >= dd->writes_starved时跳到dispatch_writes派发写,并把starved清零:
if (deadline_fifo_request(per_prio, DD_WRITE) && (dd->starved++ >= dd->writes_starved)) goto dispatch_writes; ... dispatch_writes: dd->starved = 0;
  • sysfs 写入范围:STORE_INT(deadline_writes_starved_store, &dd->writes_starved, INT_MIN, INT_MAX)(第 774 行),即理论上可为负;负值意味着读永远优先于写(写只能等读队列清空)。

调优含义:读密集且对交互延迟敏感的负载可调大;读写都要求及时响应的混合负载保持默认 2 或调小。

3.5 front_merges(布尔值)

文档语义(完整继承原文):当一个新请求与队列中已有请求扇区相邻时,既可能接在其尾部(back merge),也可能接在其头部(front merge)。由于文件的典型布局,back merge 远比 front merge 常见;某些负载下可以认为尝试 front merge 纯属浪费时间,把front_merges设为 0 即可禁用。注意:即便禁用后,front merge 仍可能通过缓存的last_merge提示发生——因为该路径几乎零成本,所以保留;内核只是禁用了"调用合并函数时的红黑树前扇区查找"。

源码印证(该文档描述的实现细节与 dd_request_merge 完全吻合):

static int dd_request_merge(struct request_queue *q, struct request **rq, struct bio *bio) { struct deadline_data *dd = q->elevator->elevator_data; ... if (!dd->front_merges) return ELEVATOR_NO_MERGE; __rq = elv_rb_find(&per_prio->sort_list[bio_data_dir(bio)], sector); if (__rq) { ... if (elv_bio_merge_ok(__rq, bio)) { *rq = __rq; ... return ELEVATOR_FRONT_MERGE; } } return ELEVATOR_NO_MERGE; }
  • front_merges为 0 时直接返回ELEVATOR_NO_MERGE,跳过elv_rb_find红黑树查找,省下一次 O(log n) 查找开销;
  • 而 bio 层的 back merge 走blk_mq_sched_try_merge()(dd_bio_merge),并不受front_merges影响,这与文档"last_merge 提示仍保留"的说明一致;
  • 默认值为 1(dd_init_sched 第 549 行:dd->front_merges = 1;),sysfs 写入被限制在 0~1(第 775 行)。

调优含义:若负载几乎不存在头部相邻请求(例如单向顺序追加写),可echo 0 > .../iosched/front_merges关闭前向查找,换取微弱的入队路径提速。

3.6 补充:prio_aging_expire(源码中额外暴露的参数)

文档未提及但源码同样通过DD_ATTR(prio_aging_expire)暴露的参数是优先级老化时间,默认 10 秒(第 36 行:static const int prio_aging_expire = 10 * HZ;)。mq-deadline 按 I/O 优先级类(RT/BE/IDLE,见 ioprio_class_to_prio)分三级派发,正常派发时高优先级有请求就跳过低优先级;dd_dispatch_prio_aged_requests 会检查"入队超过prio_aging_expire的非 RT 请求",若多个优先级都有排队,则允许老化的低优先级请求被提前派发,避免饿死。单位同样是毫秒(SHOW_JIFFIES/STORE_JIFFIES,范围 0~INT_MAX)。

4. 请求在调度器内的组织方式(理解参数为何有效)

从源码结构看,mq-deadline 为每个优先级维护两组结构(struct dd_per_prio):

struct dd_per_prio { struct rb_root sort_list[DD_DIR_COUNT]; /* 按扇区位置排序的红黑树,读/写各一棵 */ struct list_head fifo_list[DD_DIR_COUNT]; /* 按到达顺序组织的 FIFO 链表 */ sector_t latest_pos[DD_DIR_COUNT]; /* 最近派发请求的位置,作为顺序派发起点 */ struct io_stats_per_prio stats; };
  • sort_list(红黑树)deadline_next_request()latest_pos出发调用 deadline_from_pos 做树上查找,找到"扇区不小于上次派发位置"的第一个请求——这就是批内"按扇区递增服务"的落地实现;
  • fifo_list(到达序链表):deadline_check_fifo 用链表头请求的fifo_time(入队时设置的过期 jiffies)判断是否已有请求到期;一旦到期(或方向切换、或扇区上无后续请求),派发就改回 FIFO 头请求,重新起一批;
  • batching 计数:新批开始时dd->batching = 0,此后每派发一个同方向顺序请求dd->batching++,直到达到fifo_batch才允许重新选择方向/检查到期——这正是第 3.3 节"到期检查只在批之间进行"的机制来源。

理解了这两层结构后,五个参数的作用点就很清楚:read_expire/write_expire决定 FIFO 头何时"到期",fifo_batch决定批内能连续顺带多少个请求,writes_starved决定读/写方向切换的节奏,front_merges决定入队时是否多做一次红黑树前向查找。

5. 实操建议与验证方法

典型调优命令(以 sda 为例,切换到 mq-deadline 后调参,参数文件位于/sys/block/sda/queue/iosched/下):

# 查看当前调度器与可调参数 cat /sys/block/sda/queue/scheduler ls /sys/block/sda/queue/iosched/ cat /sys/block/sda/queue/iosched/read_expire # 单位 ms cat /sys/block/sda/queue/iosched/fifo_batch # 低延迟取向:读到期 100ms,批大小 1(严格 FCFS),关闭 front merge echo mq-deadline > /sys/block/sda/queue/scheduler echo 100 > /sys/block/sda/queue/iosched/read_expire echo 1 > /sys/block/sda/queue/iosched/fifo_batch echo 0 > /sys/block/sda/queue/iosched/front_merges # 吞吐取向:默认 fifo_batch=16 附近,适当放大写到期 echo 5000 > /sys/block/sda/queue/iosched/write_expire

参数速查表(默认值取自 源码常量定义,范围取自 STORE 宏):

参数单位默认值写入范围作用
read_expirems500(HZ/2)0~INT_MAX读请求最大排队时长,控制读延迟上限
write_expirems5000(5*HZ)0~INT_MAX写请求最大排队时长(软限制)
fifo_batch请求数160~INT_MAX批内顺序派发上限;1 为 FCFS,越大吞吐越高、延迟波动越大
writes_starved派发次数2INT_MIN~INT_MAX读连续优先多少次后必须派发写
front_merges0/110~1是否启用红黑树前向合并查找
prio_aging_expirems10000(10*HZ)0~INT_MAX低优先级请求排队多久后允许老化派发

运行期观察:在开启CONFIG_BLK_DEBUG_FS的内核上,mq-deadline 还注册了 debugfs 队列属性(deadline_queue_debugfs_attrs),包括batchingstarveddispatch、各优先级的readN_fifo_list/writeN_fifo_listqueued/owned_by_driver计数,可用于直观核对批处理与饿死计数是否符合预期。

6. 适用前提与限制

  • 本文所有 sysfs 路径与参数语义以当前仓库 block/mq-deadline.c 的实现为准;read_expire等参数单位为毫秒且读写均做 jiffies 换算,默认值与 HZ 配置成正比(HZ=250/1000/1000 时 read_expire 分别为 2000/500/200 ms);
  • 文档中"deadline"指 legacy deadline 调度器;当前内核块层默认走 blk-mq,对应实现为 mq-deadline(别名 deadline),二者参数语义一致但结构不同,本文以 mq 版源码为证据;
  • 对于 NVMe 等内部已做队列化与命令重排的设备,IO 调度器的收益有限,可参照 switching-sched.rst 直接选用none
  • 参数修改即时生效,无需重新挂载或重启,但对已在队内的请求只影响后续派发/入队行为。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询