[toc]

block/bio.c 块I/O核心结构(Block I/O Core Structure) Linux I/O请求的载体

历史与背景

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

struct bio 及其管理代码 block/bio.c 是为了解决Linux内核中一个根本性的I/O请求表示和传递问题而诞生的。它旨在取代一个更早、更原始的结构——struct buffer_head (bh)——作为I/O请求的主要载体

buffer_head 存在以下严重问题,限制了I/O性能和系统的可扩展性:

  • 粒度过小:一个buffer_head严格地代表单个磁盘块(例如512字节或4KB)在内存中的缓存。对于一个大的I/O操作(例如写入64KB数据),内核需要创建和管理16个独立的buffer_head对象,这非常低效。
  • 紧密耦合buffer_headI/O请求(“我要写这个块”)和内存缓存(“这个块在内存中的副本”)这两个概念紧紧地耦合在一起。这种设计使得实现绕过页面缓存的直接I/O(Direct I/O)或处理非页对齐的I/O变得异常困难和笨拙。
  • 不适合向量化I/O:现代硬件能够处理“分散-聚集”(Scatter-Gather)I/O,即一次操作可以从内存中多个不连续的缓冲区读取数据,然后写入到磁盘上的一个连续区域(反之亦然)。buffer_head这种单块模型无法自然地表达这种向量化的I/O请求。

struct bio 的诞生就是为了解决这些问题。它的核心设计思想是解耦向量化

  • 解耦bio只负责描述一个I/O操作本身,它不关心数据是否在页面缓存中。它只是一个指向数据页的指针向量。
  • 向量化:一个bio可以包含一个由多个页面(或页面片段)组成的列表(bio_vec),从而能够以一个单一、紧凑的结构来描述一个涉及多个内存段的大型I/O操作。

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

bio结构的引入是Linux 2.5/2.6开发周期中对块设备层(Block Layer)进行的一次重大重构。

  • 引入biobio被引入,成为文件系统/MM层与块设备驱动之间传递I/O请求的新标准。
  • 逐渐取代buffer_head:内核中的代码被逐步重构,从直接提交buffer_head进行I/O,改为先将信息从buffer_head(如果还在使用)或页面缓存中构建成bio,再提交bio
  • 支持块设备分区bio结构中包含了对分区的支持,使得块设备驱动无需关心分区表,上层逻辑可以直接向特定分区提交I/O。
  • 支持I/O优先级和类别:为了实现更智能的I/O调度,bio中加入了字段来表示I/O的优先级(如实时I/O)和类别(读/写/同步/异步)。
  • 支持数据完整性(Integrity):为了支持企业级存储,bio被扩展以携带数据完整性校验信息(DIF/DIX)。
  • 支持请求队列拆分(Request Queue Splitting)bio的提交和完成路径被优化,以更好地适应现代多队列NVMe设备,允许多个CPU核心无锁地向硬件提交I/O。

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

block/bio.c是Linux块设备层最核心、最基础的部分。它的代码非常稳定,但也是内核I/O性能优化的焦点。

  • 绝对核心:所有对块设备(HDD, SSD, NVMe, LVM, RAID等)的I/O请求,无论是来自文件系统、交换(swap)还是裸设备访问,最终都必须被封装成一个struct bio对象才能被块层处理。
  • 社区活跃度:社区持续对其进行优化,以适应新的硬件特性(如Zoned Block Devices, NVMe over Fabrics)和新的内核接口(如io_uring)。任何对块层性能的改进都与bio的创建、合并、拆分和完成路径密切相关。

核心原理与设计

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

block/bio.c主要负责struct bio对象的分配、管理和生命周期控制bio本身是一个描述I/O操作的**“描述符”“信封”**。

其核心数据结构struct bio包含以下关键信息:

  • bi_iter: 一个迭代器,指向I/O操作在设备上的目标扇区(bi_sector)和剩余长度(bi_size)。
  • bi_io_vec: 一个bio_vec结构体数组的指针,这是bio的精髓。每个bio_vec条目包含:
    • bv_page: 指向一个物理页面的指针。
    • bv_len: 这个I/O操作涉及该页面的字节数。
    • bv_offset: I/O操作从该页面的哪个偏移量开始。
  • bi_vcnt: bi_io_vec数组中的条目数量。
  • bi_end_io: 一个完成回调函数的指针。这是异步I/O的关键。当硬件完成I/O操作后,这个函数会被调用。
  • bi_private: 一个私有数据指针,供bi_end_io回调函数使用。
  • bi_opf: 操作码,描述了I/O的类型(读/写/discard/flush等)和属性(同步/异步等)。

block/bio.c中的函数提供了以下核心功能:

  1. 分配 (bio_alloc):从一个内存池(mempool)或slab缓存中高效地分配biobio_vec结构。
  2. 添加页面 (bio_add_page):将一个数据页添加到bio的向量列表中。bio.c的逻辑能够智能地处理页面合并,如果新添加的页面与前一个bio_vec在物理上是连续的,它会扩展前一个bio_vecbv_len,而不是创建一个新的条目。
  3. 提交 (submit_bio):这是一个宏,最终会调用generic_make_request()。这个函数是bio生命周期中的一个重要转折点。它将bio传递给I/O调度器(I/O Scheduler)和请求队列(Request Queue)。在这里,多个bio可能被合并或拆分,最终被转换成一个或多个struct request对象,request是真正发送给硬件驱动的命令。
  4. 完成处理:当I/O完成后,块设备驱动会调用bio_endio()或类似函数。这个函数会负责调用bio中注册的bi_end_io回调函数,并最终释放bio结构。

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

  • 高效的向量化I/O:能够以单一结构描述涉及多个非连续内存页的大型I/O,完美匹配现代硬件的DMA能力。
  • 解耦:将I/O操作与数据缓存分离,为直接I/O、交换I/O等多种场景提供了统一的基础。
  • 灵活性:通过完成回调机制,完美支持异步I/O,这是高性能服务器应用的基础。
  • 可堆叠性bio的结构非常适合在块设备层进行“堆叠”。例如,LVM或RAID驱动可以接收上层文件系统发来的bio,对其进行转换(例如,将一个bio拆分成多个bio并发送到不同的物理磁盘),并在其完成回调中重新组合结果。

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

  • 非面向字符设备bio是为块设备设计的,其模型基于扇区地址。它不适用于串口、终端等面向字节流的字符设备。
  • 复杂性bio的生命周期管理(特别是其完成路径和在堆叠驱动中的克隆与重定向)相当复杂,是内核中容易出错的部分。
  • biorequest的二元性:历史上,块层同时存在bio(逻辑请求)和request(物理请求)两种结构,两者之间的关系和转换增加了块层的复杂性。现代的blk-mq(多队列块层)架构在一定程度上简化了这一点,但理解两者的区别仍然是理解块层的关键。

使用场景

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

它是Linux内核中处理块设备I/O的唯一且标准的解决方案。

  • 文件系统:当文件系统需要从磁盘读取或写入数据块时,它会为这些操作创建一个或多个bio并提交。
  • 交换子系统 (Swap):当内核需要将匿名页换出到交换分区或交换文件时,它会创建一个bio来执行这个写操作。
  • s 逻辑卷管理器 (LVM) 和 软件RAID (MD):这些“堆叠”驱动接收上层传来的bio,然后根据自己的逻辑创建新的bio发送给底层的物理设备。
  • 裸设备访问:当用户空间程序直接打开一个块设备文件(如/dev/sda)并进行读写时,这些操作在内核中也会被转换成bio

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

如上所述,它只适用于块设备。在需要与字符设备、网络设备或伪文件系统交互时,需要使用完全不同的内核API(如tty_driver, net_device, VFS file operations等)。

对比分析

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

struct bio vs. struct buffer_head (传统)

特性 struct bio struct buffer_head (bh)
核心概念 I/O操作描述符 (Descriptor of an I/O operation)。 内存中的磁盘块缓存 (In-memory cache of a disk block)。
粒度 向量化。一个bio可以代表涉及多个内存页的大型I/O。 单块。一个bh只代表一个磁盘块。
与缓存关系 解耦bio只携带数据页指针,不关心数据来源。 紧密耦合bh本身就是页面缓存的一部分。
适用性 通用。完美支持缓冲I/O、直接I/O、交换等。 主要用于缓冲I/O
当前状态 现代标准 遗留结构(仍在使用,但主要作为缓存管理,而不是I/O提交的载体)。

struct bio vs. struct request

在块层内部,biorequest是两个需要区分的关键概念。

特性 struct bio (逻辑请求) struct request (物理/设备请求)
抽象层次 较高层。由文件系统或MM层创建,描述“要做什么”。 较低层。由I/O调度器和请求队列管理,描述“要发给硬件什么命令”。
生命周期 相对较短。一个bio在被合并到request中后,其生命周期就与request绑定。 较长。一个request可能由多个bio合并而成,它会排队等待,直到被驱动程序处理。
合并与拆分 bio被合并或被拆分的对象。 request合并bio的结果
与硬件关系 间接。 直接。request结构最终会被转换为硬件能理解的命令(如SATA的NCQ命令或NVMe的提交队列条目)。
主要管理者 文件系统 / MM层。 I/O调度器 / 请求队列 / 设备驱动。

BIO Slab 缓存管理

此代码片段揭示了Linux内核bio子系统背后一个精巧的内存管理策略。由于bio结构体的大小可以根据bio_set的需求(特别是front_pad的存在)而变化, 系统中可能会同时存在多种不同大小的bio对象。为每一种大小都创建一个独立的kmem_cache(slab缓存)可能会导致管理混乱和资源浪费。

此代码的核心原理是实现一个全局的、共享的、引用计数的kmem_cache池, 并使用bio对象的实际大小作为索引。这允许多个bio_set实例, 只要它们需要的bio对象大小完全相同, 就能共享同一个底层的kmem_cache, 从而极大地提高了内存利用效率并简化了管理。

关键数据结构:

  • struct bio_slab: 这是一个管理结构, 它将一个真正的kmem_cache (slab) 与其元数据打包在一起, 包括引用计数(slab_ref)和对象大小(slab_size)。
  • bio_slabs (XArray): 这是一个全局的、线程安全的数据结构(由bio_slab_lock保护), 用作一个字典或映射。它将bio对象的大小(一个整数)映射到对应的struct bio_slab管理结构。
  • bio_slab_lock (Mutex): 一个全局互斥锁, 用于保护bio_slabs XArray在被并发访问(例如, 两个不同驱动同时初始化)时的数据一致性。

bs_bio_slab_size: 计算bio对象总大小

这是一个简单的内联辅助函数, 它的作用是根据bio_set的配置计算出其实际需要的bio对象的总大小。这个大小是整个共享机制的关键索引(key)

1
2
3
4
5
6
/* 计算一个 bio_set 所需的 bio 对象的总大小. */
static inline unsigned int bs_bio_slab_size(struct bio_set *bs)
{
/* 总大小 = 前部填充 + bio结构体本身大小 + 后部填充 */
return bs->front_pad + sizeof(struct bio) + bs->back_pad;
}

create_bio_slab: bio_slab的工厂函数

bio_find_or_create_slab发现需要一个特定大小的slab缓存但它尚不存在时, 就会调用这个内部函数来创建它。

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
/* create_bio_slab: 为一个特定大小创建新的 bio_slab 管理结构和底层的 kmem_cache. */
static struct bio_slab *create_bio_slab(unsigned int size)
{
/* 分配管理结构体. */
struct bio_slab *bslab = kzalloc(sizeof(*bslab), GFP_KERNEL);
if (!bslab)
return NULL;

/* 创建一个易于识别的名称, 如 "bio-256", 这在 /proc/slabinfo 中很有用. */
snprintf(bslab->name, sizeof(bslab->name), "bio-%d", size);
/* 创建真正的 kmem_cache. */
bslab->slab = kmem_cache_create(bslab->name, size,
ARCH_KMALLOC_MINALIGN,
SLAB_HWCACHE_ALIGN | SLAB_TYPESAFE_BY_RCU, NULL);
if (!bslab->slab)
goto fail_alloc_slab;

/* 初始化元数据: 引用计数为1 (给第一个使用者). */
bslab->slab_ref = 1;
bslab->slab_size = size;

/* 将新创建的 bslab 存储到全局的 bio_slabs XArray 中, 以 size 为索引. */
if (!xa_err(xa_store(&bio_slabs, size, bslab, GFP_KERNEL)))
return bslab;

/* 错误处理: 如果存储失败, 回滚所有操作. */
kmem_cache_destroy(bslab->slab);
fail_alloc_slab:
kfree(bslab);
return NULL;
}

bio_find_or_create_slab: 获取bio Slab缓存的主入口

这是bioset_init调用的主函数, 它实现了”查找或创建”的核心逻辑。

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
/* bio_find_or_create_slab: 为一个 bio_set 查找一个现有的 slab 缓存, 或者创建一个新的. */
static struct kmem_cache *bio_find_or_create_slab(struct bio_set *bs)
{
/* 计算所需的大小. */
unsigned int size = bs_bio_slab_size(bs);
struct bio_slab *bslab;

/* 获取全局锁, 保护 bio_slabs XArray. */
mutex_lock(&bio_slab_lock);
/* 在 XArray 中根据 size 查找现有的 bslab. */
bslab = xa_load(&bio_slabs, size);
if (bslab) {
/* 如果找到了, 说明已经有其他 bio_set 在使用这个大小的slab. */
bslab->slab_ref++; /* 简单地增加引用计数. */
} else {
/* 如果没找到, 调用工厂函数创建一个新的. */
bslab = create_bio_slab(size);
}
/* 释放全局锁. */
mutex_unlock(&bio_slab_lock);

/* 如果成功(无论是找到还是创建), 返回底层的 kmem_cache 指针. */
if (bslab)
return bslab->slab;
return NULL;
}

bio_put_slab: 释放对bio Slab缓存的引用

当一个bio_set被销毁时(bioset_exit), 它必须调用此函数来”归还”它对slab缓存的使用权。

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
/* bio_put_slab: 释放一个 bio_set 对其 slab 缓存的引用. */
static void bio_put_slab(struct bio_set *bs)
{
struct bio_slab *bslab = NULL;
unsigned int slab_size = bs_bio_slab_size(bs);

/* 获取全局锁. */
mutex_lock(&bio_slab_lock);

/* 根据大小找到对应的管理结构. */
bslab = xa_load(&bio_slabs, slab_size);
/* 如果找不到, 这是一个严重的逻辑错误, 打印警告. */
if (WARN(!bslab, KERN_ERR "bio: unable to find slab!\n"))
goto out;

/* ... 一些额外的健全性检查 ... */
WARN_ON_ONCE(bslab->slab != bs->bio_slab);
WARN_ON(!bslab->slab_ref);

/* 递减引用计数. */
if (--bslab->slab_ref)
goto out; /* 如果还有其他使用者, 直接解锁并返回. */

/*
* 如果引用计数变为0, 说明我们是最后一个使用者.
* 现在可以安全地销毁这个 slab 缓存了.
*/
xa_erase(&bio_slabs, slab_size); /* 从全局 XArray 中移除. */
kmem_cache_destroy(bslab->slab); /* 销毁 kmem_cache. */
kfree(bslab); /* 释放管理结构体. */

out:
/* 释放全局锁. */
mutex_unlock(&bio_slab_lock);
}

BIO 子系统初始化

此代码片段展示了Linux内核块设备I/O层最核心的数据结构——bio——的内存管理和初始化机制。bio (Block I/O) 结构体是内核中用于描述一个块设备读写请求的基本单元。它像一个”集装箱”, 里面包含了目标设备、起始扇区、数据大小以及指向实际数据缓冲区的一组向量(bio_vec)等所有信息。由于系统在运行时会产生海量的bio请求, 如何高效地分配和释放bio结构体本身, 对I/O性能至关重要。

此代码的核心原理是biobio_vec结构体建立多层次、可定制的内存池(mempool)和slab缓存(slab cache), 以实现极快的无锁(lock-free)或低锁(low-contention)分配, 并提供一个”救援”机制来应对内存紧张的状况

关键组件与原理:

  1. kmem_cache / Slab分配器 (最底层):

    • init_bio函数首先会为不同大小的bio_vec数组创建多个kmem_cache实例。Slab分配器是内核中用于高效分配和释放小块、固定大小内存对象的标准机制。它通过预先分配”slab”页并将它们切分成多个对象, 避免了频繁调用伙伴系统的开销。SLAB_HWCACHE_ALIGN标志确保了对象在硬件缓存行上对齐, 提升性能。
  2. mempool (中间层):

    • 在Slab缓存之上, bioset_init函数会创建一个mempool_t实例。内存池(mempool)是一种保证分配成功的机制。它会预先从Slab分配器中申请并保留一定数量的对象(pool_size)。
    • 当驱动程序需要分配一个bio时, mempool会首先尝试从其内部的预留池中快速、无锁地提供一个对象。
    • 关键: 如果预留池为空, mempool会尝试从底层的Slab分配器中申请新的对象。如果Slab分配也失败了(因为系统内存不足), mempool可以被配置为睡眠等待, 直到有其他代码释放对象回池中。这保证了即使在内存压力下, 只要等待, I/O请求的描述符最终总能被分配出来, 避免了I/O流程因内存分配失败而死锁。
  3. bio_set (最高层/定制层):

    • bio_set是一个封装了biobio_vec内存池, 以及一个可选的”救援工作队列”的完整管理单元。不同的块设备驱动(如RAID, LVM, or 文件系统)可以创建自己的bio_set, 以隔离它们的bio分配, 避免相互干扰, 并可以定制front_pad(在bio结构体前预留空间)等特性。
    • 救援工作队列 (rescue_workqueue): 这是一个非常重要的死锁避免机制。当mempool因为内存紧张而无法立即分配bio时, 它会将等待分配的请求放入一个”救援列表”(rescue_list), 然后调度一个工作项到rescue_workqueue中。这个工作队列被标记为WQ_MEM_RECLAIM, 保证了即使在内存极度紧张的情况下, 它的工作者线程也能够被创建和运行。工作者线程(bio_alloc_rescue)会在一个可以安全睡眠的上下文中, 慢慢地等待内存被释放, 然后再重新尝试处理救援列表中的请求。这就将”等待内存”这个阻塞操作从可能持有锁的、紧急的I/O提交路径中, 转移到了安全的后台线程中。
  4. Per-CPU 缓存 (BIOSET_PERCPU_CACHE):

    • 为了达到极致的性能, bioset_init还可以选择性地启用per-cpu缓存。这会在每个CPU核心上创建一个小型的、完全无锁的bio对象缓存。当一个CPU需要分配bio时, 它会首先尝试从自己的本地缓存中获取, 这几乎没有任何开销。只有当本地缓存为空时, 它才会去访问有锁的、共享的mempool。这极大地减少了多核系统上的锁竞争。

初始化流程总览 (init_bio)

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
static struct biovec_slab {
int nr_vecs;
char *name;
struct kmem_cache *slab;
} bvec_slabs[] __read_mostly = {
{ .nr_vecs = 16, .name = "biovec-16" },
{ .nr_vecs = 64, .name = "biovec-64" },
{ .nr_vecs = 128, .name = "biovec-128" },
{ .nr_vecs = BIO_MAX_VECS, .name = "biovec-max" },
};

static int __init init_bio(void)
{
int i;
BUILD_BUG_ON(BIO_FLAG_LAST > 8 * sizeof_field(struct bio, bi_flags));

/* 步骤1: 为不同大小的 bio_vec 数组创建底层的 slab 缓存. */
for (i = 0; i < ARRAY_SIZE(bvec_slabs); i++) {
struct biovec_slab *bvs = bvec_slabs + i;

bvs->slab = kmem_cache_create(bvs->name,
bvs->nr_vecs * sizeof(struct bio_vec), 0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
}

/* 步骤2: 设置CPU热插拔回调, 用于管理per-cpu缓存的生命周期. */
cpuhp_setup_state_multi(CPUHP_BIO_DEAD, "block/bio:dead", NULL,
bio_cpu_dead);

/*
* 步骤3: 初始化一个全局的、供文件系统使用的 bio_set, 名为 fs_bio_set.
* @pool_size: BIO_POOL_SIZE, 预留的对象数量.
* @front_pad: 0, bio前没有额外空间.
* @flags:
* - BIOSET_NEED_BVECS: 需要一个独立的 bio_vec 内存池.
* - BIOSET_PERCPU_CACHE: 启用高性能的per-cpu缓存.
*/
if (bioset_init(&fs_bio_set, BIO_POOL_SIZE, 0,
BIOSET_NEED_BVECS | BIOSET_PERCPU_CACHE))
panic("bio: can't allocate bios\n"); /* 如果失败, 这是致命错误, 系统无法工作. */

return 0;
}
/* 确保在子系统初始化阶段被调用. */
subsys_initcall(init_bio);

bioset_init 详细流程

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
/*
* fs_bio_set is the bio_set containing bio and iovec memory pools used by
* IO code that does not need private memory pools.
*/
struct bio_set fs_bio_set;
EXPORT_SYMBOL(fs_bio_set);

int bioset_init(struct bio_set *bs,
unsigned int pool_size,
unsigned int front_pad,
int flags)
{
/* ... 初始化 front_pad, back_pad, 锁, 救援工作项 ... */
bs->front_pad = front_pad;
if (flags & BIOSET_NEED_BVECS)
bs->back_pad = BIO_INLINE_VECS * sizeof(struct bio_vec);
else
bs->back_pad = 0;

spin_lock_init(&bs->rescue_lock);
bio_list_init(&bs->rescue_list);
INIT_WORK(&bs->rescue_work, bio_alloc_rescue);

/* 为 bio 结构体本身找到或创建一个 slab 缓存. */
bs->bio_slab = bio_find_or_create_slab(bs);
if (!bs->bio_slab)
return -ENOMEM;

/* 基于 slab 缓存, 初始化 bio 的内存池. */
if (mempool_init_slab_pool(&bs->bio_pool, pool_size, bs->bio_slab))
goto bad;

/* 如果需要, 初始化 bio_vec 的内存池. */
if ((flags & BIOSET_NEED_BVECS) &&
biovec_init_pool(&bs->bvec_pool, pool_size))
goto bad;

/* 如果需要, 创建救援工作队列. */
if (flags & BIOSET_NEED_RESCUER) {
bs->rescue_workqueue = alloc_workqueue("bioset",
WQ_MEM_RECLAIM, 0);
if (!bs->rescue_workqueue)
goto bad;
}

/* 如果需要, 创建 per-cpu 缓存. */
if (flags & BIOSET_PERCPU_CACHE) {
bs->cache = alloc_percpu(struct bio_alloc_cache);
if (!bs->cache)
goto bad;
cpuhp_state_add_instance_nocalls(CPUHP_BIO_DEAD, &bs->cpuhp_dead);
}

return 0;
bad:
/* 如果任何步骤失败, 调用 bioset_exit 来回滚所有已成功的初始化操作. */
bioset_exit(bs);
return -ENOMEM;
}