Linux内核 -- Linux 的 BIO框架
深入理解 Linux 的 BIO
在 Linux 内核中,BIO(Block I/O)是块层(block layer)用于描述块设备 I/O 请求的核心数据结构。它在文件系统与块设备驱动程序之间充当“载体”,负责把数据页及相关元数据从上层提交到下层(驱动程序)并跟踪数据的读写完成情况。本文将从以下几个方面对 BIO 进行详细讲解:
- BIO 的背景与作用
- BIO 数据结构核心字段
- BIO 的生命周期
- BIO 提交与处理流程
- BIO 的常用操作接口
- BIO 与 Request 的关系
- 调试与参考
1. BIO 的背景与作用
在 Linux 内核中,块 I/O 流程大体可分为以下几个层次:
- 文件系统层:处理逻辑文件地址与块地址之间的映射,如 ext4、xfs 等。
- 通用块层(块 I/O 层):对上层提交的 I/O 进行排队、合并、调度等处理;核心数据结构包括
struct bio
、struct request
、I/O 调度器等。 - 设备驱动层:与具体硬件打交道,完成真正的磁盘/SSD I/O 操作。
BIO(struct bio
)最初是为实现 I/O 的“向量化”而设计的,也就是可以把多个物理页的数据聚合到同一个结构中,一起提交给下层驱动,从而减少请求次数、提高效率。BIO 通常会被进一步封装进 struct request
,在完成 I/O 调度后交由驱动处理。
简言之:
- BIO:更“轻量”,描述一次 I/O 操作中需要使用的页、偏移量,以及操作类型(读/写/丢弃等)。
- Request:通常是基于 BIO 进行的更高层次的“请求”打包,经 I/O 调度器处理后才会提交给硬件。
2. BIO 数据结构核心字段
struct bio
定义位于内核源码的 include/linux/bio.h
中(不同内核版本可能略有差别)。下面列出一些关键字段,并简要说明其含义:
struct bio {
struct bio *bi_next; /* 连接在同一个请求上的其他 BIO */
struct block_device *bi_bdev; /* 目标块设备(如 /dev/sda 等) */
unsigned int bi_flags; /* BIO 标志位,如是否只读、是否是分散聚合等 */
unsigned short bi_opf; /* 操作类型和标志(op + flags),如 READ, WRITE 等 */
sector_t bi_iter.bi_sector;
/* 起始扇区号(相对于块设备) */
struct bio_vec *bi_io_vec; /* 用于描述所涉及的物理页及其在页内偏移的数组 */
unsigned short bi_vcnt; /* 当前 bio_vec 数组中实际使用的元素数量 */
unsigned short bi_max_vecs; /* bio_vec 数组可容纳的最大元素数量 */
atomic_t __bi_remaining; /* 该 BIO 合并的多个 I/O 子请求的完成计数 */
bio_end_io_t *bi_end_io; /* I/O 完成回调函数 */
void *bi_private; /* 供结束回调使用的私有数据 */
/* ... 其他字段略 ... */
};
关键字段解释
bi_bdev
:指明目标块设备。bi_opf
:表示具体操作类型(读写/丢弃/刷新等)以及操作标志(同步/分散聚合等)。bi_iter.bi_sector
:起始扇区号(逻辑扇区),BIO 需要从设备的哪个扇区开始进行操作。bi_io_vec
:BIO 使用的 page + offset + length 向量数组,指向内核中实际的数据页。- 其中
struct bio_vec
包含bv_page
,bv_offset
,bv_len
。
- 其中
bi_end_io
:BIO 完成时的回调函数。BIO 最终完成时需要调用此函数来通知上层操作结束。__bi_remaining
:用于 BIO 的引用计数或子请求计数(在存在分片/拆分时很关键)。bi_private
:在提交时设置的私有数据指针,通常在完成回调时用于传递上下文。
3. BIO 的生命周期
1. 申请/初始化(Allocation / Initialization)
- 上层调用
bio_alloc
或bio_kmalloc
等接口分配struct bio
结构。 - 初始化关键字段,例如
bi_bdev
,bi_opf
,bi_end_io
等。
2. 添加数据(Attach Data)
- 使用
bio_add_page
或__bio_add_page
将数据页添加到bio->bi_io_vec
中。 - 如果空间不足,BIO 需要拆分成多个 BIO。
3. 提交(Submit)
- 数据页添加完毕后,通过
submit_bio
或类似接口将 BIO 提交到块层。
4. 执行 I/O(I/O Execution in Driver)
- 底层驱动程序从调度层获取 BIO,进行实际硬件读写操作。
5. 完成回调(Completion)
- 硬件完成 I/O 后,驱动层调用
bio->bi_end_io
回调函数,通知操作完成。 - 回调函数负责处理错误码、统计信息等,之后释放 BIO。
4. BIO 提交与处理流程
下面给出一个简化的 BIO 提交流程图:
+-----------------+
| 文件系统/上层 |
+--------+--------+
|
| bio_alloc/...
v
+-----------------+
| 组装 BIO |
| (pages, etc.) |
+--------+--------+
|
| submit_bio()
v
+-----------------+
| 通用块层/队列 | <-- 此阶段可能进行合并、调度等
+--------+--------+
|
| 提交到驱动
v
+-----------------+
| 硬件驱动层 |
+--------+--------+
|
| 硬件完成操作
v
+-----------------+
| bio->bi_end_io()|
+-----------------+
5. BIO 的常用操作接口
1. 分配与初始化
-
struct bio *bio_alloc(gfp_t gfp_mask, int nr_iovecs)
分配一个新的 BIO,支持最多nr_iovecs
个bio_vec
。 -
void bio_init(struct bio *bio, struct bio_vec *table, unsigned short maxvecs)
初始化一个已分配的 BIO。
2. 添加数据
int bio_add_page(struct bio *bio, struct page *page, unsigned int len, unsigned int offset)
将一个 page 的部分范围添加到 BIO。
3. 提交 BIO
void submit_bio(struct bio *bio)
将 BIO 提交给块层处理。
4. 结束回调
void (*bio_end_io_t)(struct bio *bio)
BIO 完成后调用的回调函数。
6. BIO 与 Request 的关系
- BIO 是对要传输数据的描述,而 Request 是更高层次的请求封装。
- 一个 Request 可以包含多个 BIO,方便进行合并和调度。
- 调度器对 Request 进行排序后,提交给硬件驱动。
7. 调试与参考
调试
- 使用
CONFIG_DEBUG_BLOCK_EXT_DEVT
等内核调试选项。 - 通过
ftrace
或bpftrace
跟踪 BIO 提交和完成过程。
源码参考
include/linux/bio.h
block/blk-core.c
block/blk-mq.c
总结
BIO 是 Linux 块 I/O 子系统的核心数据结构,用于描述一次 I/O 操作的页、扇区及元数据。它与 Request 紧密结合,共同完成从文件系统到硬件设备的 I/O 传输任务。掌握 BIO 的结构与生命周期对开发文件系统、块设备驱动以及优化性能非常重要。
原文地址:https://blog.csdn.net/sz66cm/article/details/145307031
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!