BIO发展历程

linux kernel 2.4 中的块层是围绕缓冲区头数据结构组织的。 然而,缓冲头的限制早已明确。 当底层缓冲区头结构强制每个 I/O 请求拆分为 512 字节的块时,很难创建真正高性能的块 I/O 子系统。 因此,linux kernel 2.5 “待办事项”列表中的第一项是创建一种方法来表示支持更高性能和更大灵活性的块 I/O 请求。 结果是 BIO 结构。

BIO基本原理

bio结构体是内核中块I/O的基本容器,定义在<linux/bio.h>中。该结构将正在活动的块I/O操作表示为段列表,段是内存中连续的缓冲区块(请注意,不同段不一定是连续的),通过允许以块的形式描述缓冲区,bio 结构为内核提供了从内存中的多个位置执行单个缓冲区的块 I/O 操作的能力。

bio结构体中包含了大量棘手的细节。但是,其结构的核心并没有那么复杂,bio的核心结构如下图所示:

bio结构核心

包含指向bio_vec结构数组的指针bi_io_vec。该数组表示构成此I/O请求的段(可能有多个)。而索引bi_idx指出了bi_io_vec数组的偏移量。

bio_vec结构体本身的定义比较简单:

1
2
3
4
5
struct bio_vec{
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

bio_vec是组成bio的最小单位,它包含了一块数据所在的页,这块数据所在的页内偏移以及数据的长度,通过这些信息就可以很清楚地描述数据具体处于什么位置。

从BIO中获取请求信息

bio结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned int bi_flags; /* status, command, etc */
int bi_error;
unsigned long bi_rw; /* 末尾 bit 表示 READ/WRITE,
* 起始 bit 表示优先级
*/
struct bvec_iter bi_iter; /* current index into bio_vec array */

/* 当完成物理地址合并之后剩余的段的数量 */
unsigned int bi_phys_segments;

/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;

/* 关联 bio 的数量 */
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
unsigned short bi_vcnt; /* how many bio_vec's */

/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
/* 当前 bio 的引用计数,当该数据为 0 时才可以 free */
atomic_t __bi_cnt; /* pin count: free when it hits zero */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;

/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
* 表示跟在 bio 后面的数据集合
*/
struct bio_vec bi_inline_vecs[0];
};

其中bvec_iter结构体携带着下盘的诸多信息,如下盘的扇区起始地址以及bio的大小。其定义如下:

1
2
3
4
5
6
struct bvec_iter {
sector_t bi_sector; /* device address in 512 byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};

如果把bio比作一辆货车,那么其功能就可以理解成:把不连续的物理段装上车,然后到下盘的一块连续区域卸货。而bvec_iter就是表示着货车的目的地,也就是下盘信息。

注意:单个bio结构体只能引用一组连续的磁盘扇区,但是系统内存可以是非连续的,并由bio_vec结构体表示。详见stackoverflow的讨论:https://stackoverflow.com/questions/31951233/linux-kernel-struct-bio-how-pages-are-read-written

作为一般原则,不鼓励直接访问bio_vec数组。linux提供了一组惯例的访问方法,它们隐藏了BIO结构如何工作的细节,并简化了对该结构的访问。

  • bi_iter.bi_sector字段指示了整个BIO的起始扇区,且并没有针对该字段的访问方法。
  • bi_iter.bi_size字段指示了请求操作的总大小(以字节为单位),也可以通过 bio_sectors(struct bio *bio) 方法获得以扇区为单位的大小。
  • bi_rw字段指示了请求的类型(READ 还是 WRITE),也可以通过 int bio_data_dir(struct bio *bio) 方法来获取其值。
  • 可以通过struct page* bio_page(struct bio *bio) 方法来返回一个当前struct page的指针。
  • 更多的方法见:https://lwn.net/Articles/26404/

虽然不鼓励直接访问bio_vec数组,但是几乎所有其他事情都需要通过该数组来处理。建议的处理方法是使用bio_for_each_segment宏来进行遍历:

1
2
3
4
5
6
int segno;
struct bio_vec *bvec;

bio_for_each_segment(bvec, bio, segno){
/* Do something with this segment */
}

在循环中,segno变量表示数组当前的索引,bvec变量指向当前的bio_vec结构体。

关于段的说明

BIO结构中需要一些缓冲内存来保存运往或者来自块层的数据,这些缓冲内存就被称为段。上文说到,段是内存中连续的缓冲区块,但是不同的段是不一定连续的,这样就产生了段列表。

在内核中有两种方式分配内存:

  1. 使用kmalloc()虚拟地址连续,物理地址连续。能表现出良好的性能,但是大小有限。
  2. 使用vmalloc()虚拟地址连续,而物理地址不连续。适用于需要大内存的场景。

bio的每一段的物理地址是连续的,所以是使用的kmalloc()的分配方式。

而我们仔细看bio结构体,会发现一个bio由两个字段用来描述携带的数据。其实,bio结构在申请内存的时候会多申请4个bvec的位置跟随在bio结构体上,这就是bi_inline_vecs。它用来存放内联数据(不能太多,申请了不用会造成内存浪费),当往bio注入数据时,小块的bio会直接使用这个内联的数据区域保存小于4个bvec的信息,而无需重新调用kmalloc申请内存,达到对小bio的加速。

关于scatter-gather I/O

前文将bio的功能比喻为货车,其实关键的就是,一个bio结构体的缓冲块是不连续的,而下盘时的读写操作总是在连续的扇区上进行,这被称为向量IO(vector I/O),也称为scatter-gather I/O。

分散/聚集 I/O是一种可以在单次系统调用中对多个缓冲区输入输出的方法,可以把多个缓冲区的数据写到单个数据流,也可以把单个数据流读到多个缓冲区中。其命名的原因在于数据会被分散到指定缓冲区向量,或者从指定缓冲区向量中聚集数据。

那么,如果一个bio需要将多个缓冲区写到磁盘不连续的位置,该怎么办呢?

就需要拆分bio了。(可参考:Linux block 层 - BIO拆分分析 (V5.4内核) - 知乎 (zhihu.com)

BIO的逻辑架构图

最后,一图概全文:

bio逻辑架构

关于BIO的介绍暂时到这里,如果后面学到了新的,就继续补充。