3. bio结构体深度解析

在 Linux 块设备驱动中,bio 是 I/O 请求的最小载体,也是块设备层与文件系统、通用块层之间的核心数据结构。理解 bio 的定义、向量迭代机制以及分配初始化流程,是编写高效块设备驱动的基石。本章从源码出发,逐层拆解 bio 的内部构造与操作接口。

3.1 bio 结构体定义

bio 结构体定义在 include/linux/blk_types.h 中,它描述了一个内存缓冲区与块设备上连续扇区之间的映射关系。每个 bio 可以携带多个不连续的物理内存段,但对应设备上连续的扇区范围。

字段 类型 说明
bi_sector sector_t 起始扇区号(512 字节为单位)
bi_size unsigned int 剩余待传输字节数
bi_bdev struct block_device * 目标块设备
bi_opf unsigned int 操作标志(读/写/元数据/优先级等)
bi_status blk_status_t I/O 完成状态
bi_iter struct bvec_iter 当前 bio_vec 迭代游标
bi_io_vec struct bio_vec * 指向 bio_vec 数组的指针
bi_vcnt unsigned short bio_vec 数组有效元素个数
bi_max_vecs unsigned short bio_vec 数组最大容量
bi_end_io bio_end_io_t I/O 完成回调函数
bi_private void * 驱动私有数据指针
关键设计思想:bio 将“设备上的连续扇区”与“内存中可能分散的页”解耦。驱动只需按 bi_iter 顺序处理 bi_io_vec 中的每个段,无需关心上层文件系统的页缓存布局。

3.2 bio_vec 与 bvec_iter

bio_vec 描述一个物理连续的内存段,而 bvec_iter 则记录当前遍历到哪个段、段内偏移和剩余长度。两者配合实现了零拷贝的分散/聚集 I/O。

3.2.1 bio_vec 结构

struct bio_vec {
    struct page *bv_page;   /* 指向物理页框 */
    unsigned int  bv_len;   /* 段内有效字节数 */
    unsigned int  bv_offset;/* 段内起始偏移 */
};

每个 bio_vec 代表一个 页框 + 偏移 + 长度 的三元组。多个 bio_vec 组成数组,对应内存中不连续的物理页片段。

3.2.2 bvec_iter 结构

struct bvec_iter {
    sector_t        bi_sector;   /* 当前扇区位置 */
    unsigned int    bi_size;     /* 剩余字节数 */
    unsigned int    bi_idx;      /* 当前 bio_vec 索引 */
    unsigned int    bi_bvec_done;/* 当前 bio_vec 已处理字节数 */
};

bvec_iter 是驱动遍历 bio 数据时的“指针”。驱动通过 bvec_iter 可以安全地逐个取出 bio_vec,而无需手动管理索引和偏移。

迭代宏:内核提供 bio_for_each_segment(bv, bio, iter) 宏,自动从 bi_iter 中取出下一个 bio_vec。驱动应始终使用该宏,而非直接操作 bi_io_vec 数组。

3.2.3 bio_vec 与 bvec_iter 协作关系

bio_vec 与 bvec_iter 协作关系 struct bio bi_io_vec → bi_vcnt = 3 bi_iter .bi_idx = 0 .bi_size = 4096 .bi_bvec_done = 0 .bi_sector = 100 bio_vec 数组(bi_io_vec) vec[0] page=0x1000 len=2048 off=0 vec[1] page=0x2000 len=1024 off=256 vec[2] page=0x3000 len=1024 off=0 bvec_iter 游标状态(当前指向 vec[0]) bi_idx=0 → 取出 vec[0] 处理 2048 字节 处理完后 bi_idx=1, bi_bvec_done=0, bi_size=2048

3.3 bio 的分配与初始化

内核提供了一套完整的 bio 分配与初始化 API。驱动在提交 I/O 前,必须正确创建并填充 bio 结构。

3.3.1 分配 bio

主要分配函数如下:

函数 说明 适用场景
bio_alloc(gfp_mask, nr_iovecs) 分配 bio 及内联 bio_vec 数组 通用场景,nr_iovecs 预分配固定大小
bio_alloc_bioset(gfp_mask, nr_iovecs, bs) 从指定 bioset 分配 bio 驱动自定义内存池,减少碎片
bio_clone(bio, gfp_mask) 克隆一个 bio(共享 bio_vec) 拆分 I/O 请求时使用
bio_split(bio, sectors, gfp_mask, bs) 将 bio 拆分为两个 设备有最大扇区数限制时
注意:bio_alloc 可能休眠,不能在原子上下文中调用。若在中断或 spinlock 中分配,需使用 GFP_ATOMIC 标志,且 nr_iovecs 建议不超过 BIO_MAX_PAGES(通常为 256)。

3.3.2 初始化 bio 字段

分配完成后,驱动需要手动设置以下关键字段:

struct bio *bio = bio_alloc(GFP_NOIO, 4);

bio->bi_bdev    = bdev;              /* 绑定块设备 */
bio->bi_iter.bi_sector = sector;     /* 设置起始扇区 */
bio->bi_opf     = REQ_OP_READ;       /* 读操作 */
bio->bi_end_io  = my_bio_endio;      /* 完成回调 */
bio->bi_private = my_private_data;   /* 驱动私有数据 */

3.3.3 添加 bio_vec 段

bio 初始化后,通过 bio_add_page 或 bio_add_vec 添加数据段:

/* 方式一:添加单页 */
int ret = bio_add_page(bio, page, len, offset);

/* 方式二:添加预构造的 bio_vec */
struct bio_vec bv = { .bv_page = page, .bv_len = 512, .bv_offset = 0 };
int ret = bio_add_vec(bio, &bv);

if (ret != len) {
    /* 添加失败,bio 已满或参数错误 */
    bio_put(bio);
    return -ENOMEM;
}
返回值检查:bio_add_page 返回实际添加的字节数。若返回值小于请求的 len,说明 bio 的 bio_vec 数组已满(bi_vcnt == bi_max_vecs),此时驱动应分配新的 bio 继续添加。

3.3.4 提交 bio

bio 构造完成后,通过 submit_bio(bio) 提交到块设备层:

bio->bi_end_io = my_completion;
submit_bio(bio);
/* 提交后不再访问 bio,直到回调被调用 */

3.4 bio 生命周期与状态转换

bio 生命周期状态转换 分配 bio_alloc() 构造 设置字段 + 添加 vec 提交 submit_bio() 完成 bi_end_io 回调 bio_put() 释放

3.5 驱动开发中的常见陷阱

  • bio 复用:bio 在提交后不应再修改其字段,除非在 bi_end_io 回调中重新初始化。复用前必须调用 bio_reset()。
  • bio_vec 越界:添加 bio_vec 前检查 bi_vcnt < bi_max_vecs,否则 bio_add_page 会静默失败。
  • 内存屏障:在 submit_bio 前,确保所有 bio_vec 对应的页已写入(写操作)或已准备好读取(读操作),必要时使用 dma_map_page 或 flush_dcache_page。
  • 回调上下文:bi_end_io 可能在软中断或工作队列中调用,不能在其中调用可能休眠的函数。
调试技巧:使用 bio_dump(bio) 打印 bio 的完整信息(需开启 CONFIG_BLK_DEV_IO_TRACE),或通过 /sys/kernel/debug/block/<device>/bio 查看实时 bio 状态。

3.6 本章小结

bio 是块设备驱动与通用块层之间的核心契约:

  • bio 结构体 封装了扇区、设备、操作标志和回调函数。
  • bio_vec 描述物理内存段,bvec_iter 提供安全遍历机制。
  • 分配与初始化 遵循 bio_alloc → 设置字段 → 添加 vec → submit_bio 的固定流程。
  • 驱动必须严格遵循 bio 生命周期,避免在提交后非法访问。