[toc]

在这里插入图片描述

block/bfq-iosched.c BFQ I/O调度器(Budget Fair Queueing) 追求极致公平与交互响应的重量级调度器

历史与背景

这项技术是为了解决什么特定问题而诞生的?

BFQ(Budget Fair Queueing)I/O调度器的诞生是为了解决一个长期困扰Linux用户,尤其是桌面用户的核心问题:I/O密集型任务对系统交互响应能力的严重破坏

在BFQ出现之前,当一个后台进程(如大型文件复制、软件更新、文件索引)开始疯狂读写磁盘时,用户会明显感觉到整个系统变得卡顿:启动应用程序变慢、窗口响应迟钝、甚至鼠标指针都会跳动。这是因为后台任务产生的海量I/O请求“淹没”了存储设备,导致前台交互式应用(如浏览器、文本编辑器)的关键读写请求必须排很长的队。

BFQ的核心目标就是实现进程间的I/O公平性,并优先保障交互式应用的低延迟。它不仅仅是调度I/O请求,更是在管理不同进程对I/O带宽的访问权,确保没有任何一个进程可以霸占整个存储设备。

它的发展经历了哪些重要的里程碑或版本迭代?

  1. CFQ的继承者:BFQ的思想源于并极大地改进了其前身——单队列时代的CFQ(Completely Fair Queuing)调度器。CFQ是第一个为每个进程创建I/O队列的调度器,但其延迟控制和公平性模型仍有不足。
  2. Paolo Valente的持续努力:BFQ主要由开发者Paolo Valente领导开发。在很长一段时间里,它作为一个高质量的“树外”内核补丁存在,在社区中积累了良好的声誉。
  3. blk-mq重构(关键里程碑):BFQ面临的最大挑战是适配全新的多队列块层(blk-mq)。这需要对其核心逻辑进行重大重构,使其能够从管理单个全局队列,转变为在多个硬件队列上协同工作。block/bfq-iosched.c正是这个现代化、mq-aware版本的实现。
  4. 合入主线并成为默认:在为blk-mq重构成功后,BFQ最终被合入Linux内核主线。由于其出色的交互性能,许多面向桌面的主流发行版(如Fedora, Ubuntu)和Android都已将其作为默认的I/O调度器。

目前该技术的社区活跃度和主流应用情况如何?

BFQ是目前Linux内核中最复杂、功能最强大的I/O调度器。它是一个非常活跃的项目,仍在不断地进行优化和调整。

  • 应用情况:它是Linux桌面、Android移动设备以及任何需要保证多任务环境下交互响应性的场景的事实标准
  • 社区地位:被公认为解决I/O公平性和交互性问题的最佳通用方案。

核心原理与设计

它的核心工作原理是什么?

BFQ的核心是一个复杂的、基于**预算(Budget)和时间片(Time Slice)的加权调度算法。它为每个进程(或cgroup)**创建一个专属的I/O队列。

  1. Per-Process Queues:这是BFQ实现公平性的基础。不同进程的I/O请求在逻辑上是隔离的。
  2. 预算分配 (Budget Assignment):调度器会轮流服务这些进程队列。当轮到一个进程的队列时,BFQ会赋予它一个“预算”——通常是允许其读写的扇区数量。
  3. 服务与抢占:被选中的进程可以持续派发I/O请求,直到其预算耗尽。一旦预算用完,或者它的时间片到期,BFQ就会抢占它,并选择下一个进程的队列进行服务。
  4. 复杂的启发式算法(Heuristics - BFQ的“魔法”):BFQ的强大之处在于其智能的启发式规则,用于动态调整调度行为:
    • 交互性检测:BFQ会试图“猜测”一个进程是交互式的还是批处理的。例如,一个发出同步读请求(意味着进程会等待I/O完成)后就去睡眠的进程,很可能是交互式的。
    • 低延迟优先:对于被识别为交互式的进程,BFQ会给予极高的优先级,几乎可以立即抢占正在服务的后台任务,以处理这个交互式请求。
    • 权重与cgroups:BFQ与cgroups v2的io.bfq.weight控制器紧密集成,允许管理员为不同的服务(cgroups)设置不同的I/O权重,实现精细化的服务质量(QoS)控制。
    • 队列合并:它能检测到多个协作进程(如tar | gzip)并可能合并它们的队列,以提高HDD上的寻道效率。

它的主要优势体现在哪些方面?

  • 卓越的交互响应性:核心优势。即使在最重的I/O负载下,也能保证系统UI和前台应用的流畅。
  • 强大的公平性:确保没有进程可以饿死其他进程,所有进程都能获得公平的I/O带宽份额。
  • 精细化的QoS控制:是唯一与cgroups深度集成的mq调度器,提供了强大的I/O资源控制能力。
  • 高吞吐量:虽然以公平性著称,但其内部的多种优化(如请求合并、空闲等待等)也使其在许多场景下能提供非常高的吞吐量,尤其是在HDD上。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 高CPU开销:BFQ的复杂算法和启发式规则使其成为CPU开销最高的I/O调度器。在CPU成为瓶颈的系统中,这可能会带来负面影响。
  • 对超高速设备可能过度调度:对于延迟极低的顶级NVMe SSD,BFQ的软件排队和调度开销本身可能大于其带来的好处。在这种纯粹追求IOPS和最低延迟的场景下,更简单的调度器可能更优。
  • 复杂性:其内部有大量的可调参数和复杂的行为模式,使得性能分析和问题诊断变得非常困难。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

  • 桌面Linux系统:这是BFQ的“主场”,能提供最佳的用户体验。
  • Android和其他移动设备:后台同步、应用安装等操作不应影响前台应用的流畅度。
  • 多租户服务器:在共享存储的虚拟化或容器环境中,需要使用cgroups为不同租户或服务提供公平的I/O份额并防止相互干扰。
  • 通用服务器:在运行混合工作负载(例如,同时有Web服务、数据库和批处理任务)的服务器上,BFQ可以防止批处理任务影响在线服务的响应时间。

是否有不推荐使用该技术的场景?为什么?

  • 单任务、高性能计算/数据库:在一个只运行单一数据库实例且该实例自己管理I/O的服务器上,BFQ的公平性机制和CPU开销是不必要的。在这种场景下,**nonekyber**调度器通常是更好的选择,以追求极致的IOPS或可预测的延迟。
  • CPU资源极其受限的系统:如果系统的CPU已经满载,BFQ的额外开销可能会加剧CPU瓶颈,反而降低整体性能。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 bfq (Budget Fair Queueing) kyber mq-deadline none
核心原理 复杂的预算和权重,为每个进程创建队列 基于延迟目标的反馈控制 截止时间 + LBA排序 简单的FIFO,无重排
CPU开销 中等 最低
主要目标 公平性交互性 可预测的I/O延迟 平衡吞吐量与延迟 最大化IOPS/吞吐量,最低延迟
最佳适用硬件 HDD, SATA SSD, 桌面环境 高速NVMe/SATA SSD SATA SSD, HDD, 企业级SSD 超高速NVMe SSD
cgroups I/O控制
关键可调参数 众多启发式参数 read_lat_nsec, write_lat_nsec read_expire, write_expire

BFQ I/O 调度器模块:定义、注册与初始化

本代码片段展示了 BFQ (Budget Fair Queueing) I/O 调度器在 Linux 内核中的定义、多阶段初始化和注册过程。其核心功能是:通过填充一个 elevator_type 结构体(iosched_bfq_mq),将 BFQ 调度算法的所有核心操作封装起来;在模块加载时 (bfq_init),它不仅向内核的 I/O 调度框架注册自己,还会执行一系列 BFQ 特有的初始化步骤,包括注册 cgroup I/O 控制策略建立专用的 SLAB 缓存,从而为系统提供一个复杂的、注重公平性和吞吐量的 I/O 调度策略。

实现原理分析

此代码展示了一个比 mq-deadlinekyber 更复杂的调度器模块的初始化模式,其原理体现在分层初始化和与 cgroup 子系统的深度集成。

  1. elevator_type 结构:BFQ 调度器的蓝图:

    • 与所有调度器一样,iosched_bfq_mq 结构是其核心定义,通过 .ops 成员将抽象的调度操作映射到具体的 bfq_* 实现函数。
    • dispatch_request: 这是 BFQ 算法的核心,它会根据每个进程队列的“预算”(budget)和内部的服务树(service tree)来选择下一个要分发的请求,以实现公平性。
    • icq_sizeicq_align: BFQ 明确请求icq (I/O context queue) 缓存。icq 是一个 per-process/per-cgroup 的数据结构,BFQ 使用它来为每个 I/O 发起实体(进程或 cgroup)创建独立的队列,这是实现其“公平队列”思想的物理基础。elv_register 函数会根据这两个字段为 BFQ 创建一个名为 bfq_io_cq 的专用 SLAB 缓存。
  2. Sysfs 可调参数 (BFQ_ATTR 宏):

    • BFQ_ATTR 是一个便利的宏,用于快速定义一个 sysfs 属性。它将属性名 name 自动扩展为对应的 bfq_name_showbfq_name_store 函数。
    • bfq_attrs 数组利用这个宏,声明了一系列可以在运行时通过 sysfs 调整的 BFQ 算法参数,如 fifo_expire_sync (同步请求超时), max_budget (最大预算) 等,为系统管理员提供了高度的灵活性。
  3. 多阶段初始化 (bfq_init):

    • bfq_init 的逻辑比简单的 elv_register 调用要复杂,体现了 BFQ 的高级特性:
      a. cgroup 策略注册: blkcg_policy_register(&blkcg_policy_bfq) 是第一步。BFQ 是一个 cgroup-aware 的调度器,能够为不同的 cgroup 提供 I/O 带宽的隔离和权重分配。此调用将 BFQ 的 I/O 控制策略注册到内核的块设备 cgroup (blkcg) 子系统中,使得用户可以通过 cgroupfs 来配置 BFQ 的行为。
      b. SLAB 缓存设置: bfq_slab_setup() 调用用于创建 BFQ 内部所需的其他专用 SLAB 缓存(除了 icq 之外),例如用于分配 bfq_queue(per-process 队列)或 bfq_group(cgroup 对应的实体)的缓存。
      c. 调度器注册: elv_register(&iosched_bfq_mq) 是最后一步,将 BFQ 自身注册到 I/O 调度框架。
    • 健壮的错误处理: bfq_init 同样使用了 goto 链来实现严谨的错误处理。如果在任何一步失败,它会按严格的逆序撤销所有已经成功的初始化步骤,确保没有资源泄漏。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// 定义一个便利宏,用于快速创建 BFQ 的 sysfs 属性项。
#define BFQ_ATTR(name) \
__ATTR(name, 0644, bfq_##name##_show, bfq_##name##_store)

// 定义一个 sysfs 属性数组,列出了所有 BFQ 的可调参数。
static const struct elv_fs_entry bfq_attrs[] = {
BFQ_ATTR(fifo_expire_sync), // 同步请求FIFO超时
BFQ_ATTR(fifo_expire_async), // 异步请求FIFO超时
BFQ_ATTR(back_seek_max), // 最大反向寻道距离
BFQ_ATTR(back_seek_penalty), // 反向寻道惩罚因子
BFQ_ATTR(slice_idle), // 队列空闲时间
BFQ_ATTR(slice_idle_us), // 队列空闲时间 (微秒)
BFQ_ATTR(max_budget), // 最大预算
BFQ_ATTR(timeout_sync), // 同步请求超时 (旧接口)
BFQ_ATTR(strict_guarantees), // 严格保证模式
BFQ_ATTR(low_latency), // 低延迟模式
__ATTR_NULL
};

// 定义一个 elevator_type 结构体,用于向块层描述 bfq 调度器。
static struct elevator_type iosched_bfq_mq = {
.ops = { // 核心操作函数指针
.limit_depth = bfq_limit_depth,
.prepare_request = bfq_prepare_request,
.requeue_request = bfq_finish_requeue_request,
.finish_request = bfq_finish_request,
.exit_icq = bfq_exit_icq, // icq 退出回调
.insert_requests = bfq_insert_requests,
.dispatch_request = bfq_dispatch_request,
.next_request = elv_rb_latter_request,
.former_request = elv_rb_former_request,
.allow_merge = bfq_allow_bio_merge,
.bio_merge = bfq_bio_merge,
.request_merge = bfq_request_merge,
.requests_merged = bfq_requests_merged,
.request_merged = bfq_request_merged,
.has_work = bfq_has_work,
.depth_updated = bfq_depth_updated,
.init_sched = bfq_init_queue,
.exit_sched = bfq_exit_queue,
},

.icq_size = sizeof(struct bfq_io_cq), // 请求 icq 缓存,并指定大小
.icq_align = __alignof__(struct bfq_io_cq), // 指定 icq 的对齐要求
.elevator_attrs = bfq_attrs, // sysfs 可调参数
.elevator_name = "bfq", // 调度器名称
.elevator_owner = THIS_MODULE, // 关联到本内核模块
};
MODULE_ALIAS("bfq-iosched");

static int __init bfq_slab_setup(void)
{
bfq_pool = KMEM_CACHE(bfq_queue, 0);
if (!bfq_pool)
return -ENOMEM;
return 0;
}

/**
* @brief bfq_init - bfq 调度器模块的初始化函数。
* @return int: 成功返回0,失败返回错误码。
*/
static int __init bfq_init(void)
{
int ret;

#ifdef CONFIG_BFQ_GROUP_IOSCHED
// 步骤1: 注册 BFQ 的 cgroup I/O 控制策略。
ret = blkcg_policy_register(&blkcg_policy_bfq);
if (ret)
return ret;
#endif

ret = -ENOMEM;
// 步骤2: 创建 BFQ 内部所需的 SLAB 缓存。
if (bfq_slab_setup())
goto err_pol_unreg;

// 初始化用于估算设备峰值速率的参考工作负载持续时间。
ref_wr_duration[0] = msecs_to_jiffies(7000);
ref_wr_duration[1] = msecs_to_jiffies(2500);

// 步骤3: 向内核的电梯框架注册 bfq 调度器。
ret = elv_register(&iosched_bfq_mq);
if (ret)
goto slab_kill;

return 0;

// 错误处理 goto 链:按初始化的逆序清理已分配的资源。
slab_kill:
bfq_slab_kill();
err_pol_unreg:
#ifdef CONFIG_BFQ_GROUP_IOSCHED
blkcg_policy_unregister(&blkcg_policy_bfq);
#endif
return ret;
}

/**
* @brief bfq_exit - bfq 调度器模块的退出函数。
*/
static void __exit bfq_exit(void)
{
// 按初始化的逆序执行清理操作。
elv_unregister(&iosched_bfq_mq);
#ifdef CONFIG_BFQ_GROUP_IOSCHED
blkcg_policy_unregister(&blkcg_policy_bfq);
#endif
bfq_slab_kill();
}

module_init(bfq_init);
module_exit(bfq_exit);

MODULE_AUTHOR("Paolo Valente");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MQ Budget Fair Queueing I/O Scheduler");