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 协作关系
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 生命周期与状态转换
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 生命周期,避免在提交后非法访问。