2. 通用块层(Generic Block Layer)
通用块层是Linux内核块设备子系统的中枢,它位于文件系统与具体设备驱动之间,负责将文件系统发来的逻辑I/O请求转化为统一的块I/O请求,并向下分派给调度层与驱动层。理解通用块层的关键在于掌握其核心数据结构——bio结构体,以及围绕它构建的分配、提交与上下文管理机制。
2.1 bio结构体详解
bio(Block I/O)是通用块层中最基本的I/O请求描述符。每一个bio代表一个内存缓冲区与一个块设备上一段连续磁盘扇区之间的数据传输操作。其核心字段如下表所示:
| 字段 | 类型 | 说明 |
|---|---|---|
bi_next |
struct bio * |
指向链表中下一个bio,用于请求合并 |
bi_disk |
struct gendisk * |
目标块设备(通用磁盘对象) |
bi_partno |
unsigned int |
分区编号 |
bi_iter |
struct bvec_iter |
当前I/O在bio中的迭代位置(扇区偏移与剩余段数) |
bi_opf |
unsigned int |
操作标志(读/写、优先级、元数据、同步等) |
bi_end_io |
bio_end_io_t |
I/O完成时的回调函数 |
bi_private |
void * |
上层(如文件系统)私有数据,回调时传回 |
bi_io_vec |
struct bio_vec * |
指向bio_vec数组的指针,描述内存页片段 |
bi_vcnt |
unsigned short |
bio_vec数组中有效段的数量 |
bi_max_vecs |
unsigned short |
bio_vec数组的总容量(最大段数) |
bi_phys_segments |
unsigned short |
物理上连续的段数(用于DMA映射) |
bi_hw_segments |
unsigned short |
硬件可合并的段数 |
bi_size |
unsigned int |
剩余待传输字节数 |
bi_css |
struct cgroup_subsys_state * |
I/O控制组(cgroup)上下文 |
2.2 bio_vec与段的概念
bio_vec是描述单个内存片段的元组,其定义如下:
struct bio_vec {
struct page *bv_page; /* 指向物理页框 */
unsigned int bv_len; /* 片段长度(字节) */
unsigned int bv_offset;/* 片段在页内的偏移 */
};
一个bio可以包含多个bio_vec,这些bio_vec在内存中可能指向不连续的物理页,但它们在磁盘上对应的扇区必须是连续的。这种“内存离散、磁盘连续”的映射关系是通用块层实现高效数据传输的基础。
段(Segment)的概念在块层中有两个层次:
- 逻辑段(Logical Segment):即一个
bio_vec,是上层提交的最小内存单元。 - 物理段(Physical Segment):经过DMA映射后,物理地址连续的多个逻辑段合并而成。驱动层通常按物理段提交给硬件。
以下SVG图展示了bio、bio_vec与物理页之间的关系:
BIO_MAX_VECS(默认256)。对于大I/O,块层会通过bio_add_page()或bio_iov_iter_get_pages()动态追加段。当bi_vcnt达到bi_max_vecs时,必须分配新的bio来继续描述剩余数据,形成bio链表。
2.3 bio的分配与提交路径
bio的分配与提交遵循一套标准流程,涉及内存分配、段填充、提交与回调。以下是典型路径:
- 分配bio:调用
bio_alloc(gfp_mask, nr_iovecs),从bio slab缓存或紧急内存池中分配一个bio实例,同时预分配nr_iovecs个bio_vec槽位。 - 设置目标设备:通过
bio_set_dev(bio, bdev)绑定到具体块设备。 - 设置操作标志:通过
bio_set_op_attrs(bio, op, flags)设置读/写、同步、元数据等标志。 - 添加数据段:循环调用
bio_add_page(bio, page, len, offset),将物理页片段逐个加入bio_vec数组。 - 设置完成回调:通过
bio->bi_end_io = my_callback注册I/O完成处理函数。 - 提交bio:调用
submit_bio(bio),将bio送入块层处理管道。
以下SVG图展示了bio从分配到提交的完整生命周期:
submit_bio()内部会检查bio是否为空(bi_vcnt == 0),若为空则直接返回错误。此外,submit_bio()可能在内核上下文中直接完成I/O(如基于内存的块设备),也可能将bio加入请求队列等待调度。调用者必须确保在提交后不再修改bio字段,除非在完成回调中处理。
2.4 块层I/O上下文
块层I/O上下文(Block I/O Context, blk_io_context)是内核为每个进程或线程维护的I/O状态信息,用于跟踪该进程在块设备上的I/O行为。其核心作用包括:
- I/O优先级管理:记录进程的I/O优先级(
ioprio),影响CFQ等调度器的排队策略。 - 请求合并跟踪:记录最近提交的bio的结束扇区,用于判断新bio是否可以与现有请求合并。
- 节流与统计:为I/O节流(blk-throttle)和cgroup I/O控制提供进程级上下文。
关键数据结构关系如下表:
| 数据结构 | 所属层次 | 主要用途 |
|---|---|---|
blk_io_context |
通用块层 | 进程级I/O上下文,包含ioprio、合并状态 |
blkcg_gq |
cgroup I/O控制 | cgroup组级I/O权重与限速 |
request_queue |
块层调度层 | 设备级请求队列,包含调度器实例 |
io_cq |
通用块层 | 进程与cgroup的关联上下文 |
在I/O路径中,submit_bio()会调用blk_queue_bio(),该函数首先获取当前进程的blk_io_context(若不存在则创建),然后尝试将bio与请求队列中已有的请求合并。合并策略分为两种:
- 前向合并(Front Merge):新bio的起始扇区恰好等于请求末尾扇区。
- 后向合并(Back Merge):新bio的结束扇区恰好等于请求起始扇区。
ioprio_set系统调用)可以显著改善I/O调度公平性。例如,将日志写入线程设为较高优先级,将批量扫描线程设为较低优先级,避免扫描任务阻塞关键日志I/O。
通用块层通过bio结构体实现了文件系统与设备驱动之间的高效解耦。理解bio的段映射、分配提交路径以及I/O上下文管理,是深入掌握块设备层性能优化与问题排查的基础。下一层——I/O调度层——将在此基础上对多个bio进行排序与合并,以匹配底层存储设备的物理特性。