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)上下文
关键设计思想:bio结构体采用“轻量级、可链接”的设计。单个bio可以描述一个不连续的物理内存区域(通过bio_vec数组),但磁盘扇区必须是连续的。这种设计使得文件系统可以将一个逻辑上连续的缓冲区映射到多个物理页上,而无需提前做页合并。

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结构体与bio_vec段映射关系 bio结构体与bio_vec段映射关系 struct bio bi_io_vec → bi_vcnt = 3 bi_iter.bi_sector bi_opf = READ bi_end_io bi_private bi_size = 12288 bi_phys_segments bio_vec[0] bv_page = page_A bv_len = 4096, offset=0 bio_vec[1] bv_page = page_B bv_len = 4096, offset=0 bio_vec[2] bv_page = page_C bv_len = 4096, offset=0 物理页 A (4KB) 物理页 B (4KB) 物理页 C (4KB) 磁盘扇区 扇区 0-7 (连续) 扇区 8-15 (连续) 扇区 16-23 (连续)
性能要点:bio_vec数组的大小(bi_max_vecs)在bio分配时确定,通常为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的分配与提交遵循一套标准流程,涉及内存分配、段填充、提交与回调。以下是典型路径:

  1. 分配bio:调用bio_alloc(gfp_mask, nr_iovecs),从bio slab缓存或紧急内存池中分配一个bio实例,同时预分配nr_iovecs个bio_vec槽位。
  2. 设置目标设备:通过bio_set_dev(bio, bdev)绑定到具体块设备。
  3. 设置操作标志:通过bio_set_op_attrs(bio, op, flags)设置读/写、同步、元数据等标志。
  4. 添加数据段:循环调用bio_add_page(bio, page, len, offset),将物理页片段逐个加入bio_vec数组。
  5. 设置完成回调:通过bio->bi_end_io = my_callback注册I/O完成处理函数。
  6. 提交bio:调用submit_bio(bio),将bio送入块层处理管道。

以下SVG图展示了bio从分配到提交的完整生命周期:

bio分配与提交路径流程图 bio分配与提交路径流程图 文件系统 / 上层调用者 bio_alloc() bio_set_dev() + bio_set_op_attrs() bio_add_page() 循环 submit_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的结束扇区恰好等于请求起始扇区。
优化技巧:在数据库或高并发存储场景中,合理设置进程的I/O优先级(通过ioprio_set系统调用)可以显著改善I/O调度公平性。例如,将日志写入线程设为较高优先级,将批量扫描线程设为较低优先级,避免扫描任务阻塞关键日志I/O。

通用块层通过bio结构体实现了文件系统与设备驱动之间的高效解耦。理解bio的段映射、分配提交路径以及I/O上下文管理,是深入掌握块设备层性能优化与问题排查的基础。下一层——I/O调度层——将在此基础上对多个bio进行排序与合并,以匹配底层存储设备的物理特性。