通用块设备层( Generic Block Layer)是内核的一个组成部分,它处理系统所有对块设备的请求。有通用块设备层后,内核可以方便地:
• 将数据存放在高端内存—当CPU访问高端内存的数据时,页就被临时映射到内核的线性地址空间,然后解除映射。
• 实现零拷贝( zero-copy),即磁盘数据直接拷贝到用户地址空间,而不需要先拷贝到内核地址空间。实际上就是,内核进行I/O数据传输使用的页面被映射到用户进程的地址空间中。
• 管理逻辑卷,如LVM( Logical Volume Manager)和RAID( Redundant Array ofInexpensive Disks)。
• 使用现在的磁盘控制器新特性,如大的磁盘缓存、增强的DMA功能、板上I/O
1.1 缓冲区和缓冲区头
当一个块被调入内存时(也就是,在读入或等待写出时),它要存储在一个缓冲区中。每个缓冲区都有一个块对应,它相当于是磁盘块在内存中的表示。前面提到过,块包含一个或多个扇区,但大小不能超过一个页面,所以一个页面可以容纳一个或多个内存中的块。由于内核在处理数据时,需要一些相关的控制信息(比如块属于哪一个块设备,块对应于哪一个缓冲区等),所以每个缓冲区都有一个对应的描述符。
每个块都要有自己的块缓冲区( block buffer),内核使用内存上的这块缓冲区来保存块数据。当内核从块设备上读取一个块时,就用从硬件上读取的数据填充块缓冲区;同理,当内核向块设备写数据时,就用块缓冲区中的数据写到块设备上。每个缓冲区都有一个缓冲区头( buffer head),描述类型为buffer_head,这个数据结构里包含了内核处理缓冲区所需要的信息;于是,在每个缓冲区上操作之前,内核都要先检查它的缓冲区头。的定义在文件include/linux/buffer_head.h中。
struct buffer_head {
unsignedlong b_state; /* 表示缓冲区状态 */
structbuffer_head *b_this_page; /* 当前页中缓冲区 */
structpage *b_page; /* 当前缓冲区所在内存页 */
sector_tb_blocknr; /* 起始块号 */
size_t b_size; /* buffer在内存中的大小 */
char*b_data; /* 块映射在内存页中的数据 */
structblock_device *b_bdev; /* 关联的块设备 */
bh_end_io_t *b_end_io; /* I/O完成方法 */
void*b_private; /* 保留的 I/O 完成方法 */
structlist_head b_assoc_buffers; /* 关联的其他缓冲区 */
structaddress_space *b_assoc_map;/* 相关的地址空间 */
atomic_tb_count; /* 引用计数 */
};
缓冲区与缓冲区头部的关系:
b_state域表示缓冲区的状态,可以是中一种标志或多种标志的组合。合法的标志存放在bh_state_bits枚举中,其定义也在文件include/linux/buffer_head.h中。
enum bh_state_bits {
BH_Uptodate, /* Contains valid data */
BH_Dirty, /* Is dirty */
BH_Lock, /* Is locked */
BH_Req, /* Has been submitted for I/O */
BH_Uptodate_Lock,/*Used by the first bh in a page, to serialise
* IO completion of other buffers in the page
*/
BH_Mapped, /* Has a disk mapping */
BH_New, /* Disk mapping was newly created byget_block */
BH_Async_Read, /* Is under end_buffer_async_read I/O */
BH_Async_Write, /* Is under end_buffer_async_write I/O */
BH_Delay, /* Buffer is not yet allocated on disk */
BH_Boundary, /* Block is followed by a discontiguity */
BH_Write_EIO, /* I/O error on write */
BH_Unwritten, /* Buffer is allocated on disk but notwritten */
BH_Quiet, /* Buffer Error Prinks to be quiet */
BH_Meta, /* Buffer contains metadata */
BH_Prio, /* Buffer should be submitted with REQ_PRIO*/
BH_Defer_Completion,/* Defer AIO completion to workqueue */
BH_PrivateStart,/*not a state bit, but the first bit available
* for private allocation by other entities
*/
};
其标识位含义:
bh_state_bits列表还包含了一个特殊标志—BH_PrivateStart,该标志不是可用状态标志,使用它是为了指明可被其他代码使用的起始位。块I/O层不会使用BH_PrivateStart或更高的位,那么某个驱动程序希望通过b_state域存储信息时就可以安全地使用这些位。驱动程序可以在这些位中定义自己的状态标志,只要保证自定义的状态标志不与块I/O层的专用位发生冲突就可以了。
b_count域表示缓冲区的使用计数,可以通过内联函数get_bh() 和put_bh() 来此域增减,其定义也在文件include/linux/buffer_head.h中
static inline void get_bh(struct buffer_head *bh)
{
atomic_inc(&bh->b_count);
}
static inline void put_bh(struct buffer_head *bh)
{
smp_mb__before_atomic();
atomic_dec(&bh->b_count);
}
在操作缓冲区头之前,应该先使用get_bh()函数增加缓冲区头的引用计数,确保该缓冲区头不会再被释放;当完成对缓冲区头的操作之后,还必须使用put_bh()函数减少引用计数。
与缓冲区对应的磁盘物理块由b_blocknr域索引,该值是b_bdev域指明的块设备中的逻辑块号。与缓冲区所对应的内存物理页由b_page域表示,另外, b_data域直接指向相应的块(它位于b_page域所指明的页面中某个位置上),块的大小由b_size域表示,所以块在内存中的起始位置在b_data处,结束位置在( b_data+b_size)处。
缓冲区头的目的在于描述磁盘块和物理内存缓冲区(在特定页面上的字节序列)之间的映射关系。这个结构体在内核中只扮演一个描述符的角色,说明从缓冲区到块的映射关系。
1.2 bio结构体
I/O操作的基本容器由bio结构体表示,它定义在文件include/linux/bio.h中。该结构体代表了正在活动的以段( segment)链表形式组织的块I/O操作。一个段是一小块连续的内存缓冲区。这样,单个缓冲区就不一定要连续。所以使用段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上, bio结构体也能对内核保证I/O操作的执行。这样的向量I/O称为分散-聚合I/O。
bio结构体的定义如下:
structbio {
struct bio *bi_next; //请求链表
struct block_device *bi_bdev; //block设备
unsigned long bi_flags; //状态和命令标志
unsigned long bi_rw; //读写
struct bvec_iter bi_iter; //
unsigned int bi_phys_segments;//结合后的片段数
unsigned int bi_seg_front_size;//第一个可合并的段大小;
unsigned int bi_seg_back_size;//最后一个可合并段大小
atomic_t bi_remaining;
bio_end_io_t *bi_end_io; //bio完成
void *bi_private; //所有数据
#ifdef CONFIG_BLK_CGROUP
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#ifdefined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
unsigned short bi_vcnt; //bio数目
unsigned short bi_max_vecs; //最大数
atomic_t bi_cnt; //使用计数
struct bio_vec *bi_io_vec; //bio_vec链表
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0]; //内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少量的 bio_vecs
}
使用bio结构体的目的主要是代表正在现场执行的I/O操作,所以该结构体中的主要成员变量都是用来管理相关信息的,其中最重要的几个成员变量是bi_io_vecs、 bi_vcnt和bi_idx。下图显示了bio结构体及相关结构体之间的关系。
bi_io_vecs指向一个bio_vec结构体数组,该结构体链表包含了一个特定I/O操作所需要使用到的所有段( segment)。每个bio_vec结构都是一个形式为<page, offset, len>的向量,它描述的是一个特定的段:段所在的物理页、块在物理页中的偏移量、从给定偏移量开始的块长度。整个bio_io_vec结构体数组表示了一个完整的缓冲区。 bio_vec结构体定义在文件include/linux/bio.h中。
struct bio_vec {
struct page *bv_page; /* 指向这个缓冲区所驻留的物理页面 */
unsigned int bv_len; /* 这个缓冲区以字节为单位的大小 */
unsigned int bv_offset; /* 缓冲区所驻留的页中以字节为单位的偏移量 */
};
在每个给定的块I/O操作中, bi_vcnt域用来描述bi_io_vec所指向的bio_vec数组中的向量数目。当块I/O操作执行完毕后, bi_idx指向数组的当前索引。
总之,每个块I/O请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。 I/O操作的第一个段( segment)由bi_io_vec结构体所指向,其他的段在其后依次放置,共有bi_vcnt个段。当块I/O层开始执行请求,需要使用各个片段时,bi_idx就会不断更新,从而总指向当前片段。bi_idx指向数组的当前bio_vec段,块I/O层通过它可以跟踪块I/O操作的完成进度。但该成员变量更重要的作用在于分割bio结构体。像RAID这样的驱动器可以把单独的bio结构体(原本是为单个设备使用准备的) 分割到RAID阵列中的各个硬盘上去。RAID设备驱动只需拷贝这个bio结构体,再把bi_idx域设置为每个独立硬盘操作时需要的位置就可以了。
void bio_get(structbio *bio)
void bio_put(structbio *bio)
前者增加使用计数,后者减少使用计数(如果减到0,则销毁bio结构体)。在操作正在活动的bio结构体时,一定要首先增加它的使用计数,以免在操作过程中bio结构体被释放;相反,在操作完毕后,要减少使用计数。最后要说明的是bi_private域,这是一个拥有者(也就是创建者)的私有域,只有创建了bio结构体的拥有者可以读写该域。
1.3 缓冲区头和bio结构体比较
缓冲区头( buffer head)和bio结构体之间存在明显差别。 bio结构体代表的是I/O操作,它可以包括内存中的一个或多个页;而另一方面, buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。因为缓冲区头关联的是单独页中的单独磁盘块,所以它可能会引起不必要的分割,将请求按块为单位划分,只能靠以后才能重新组合。由于bio结构是轻量级的,它描述的块可以不需要连续存储区,并且不需要分割I/O操作。利用bio结构体代替buffer_head结构体有以下好处:
• bio结构体很容易处理高端内存,因为它处理的是物理页而不是直接指针。
• bio结构体既可以代表普通页I/O,同时也可以代表直接I/O。
• bio结构体便于执行分散-聚合I/O操作,操作中的数据可以来自多个物理页面。
• bio结构体相比缓存区头属于轻量级的结构体,因为它只需要包含块I/O操作所需的信息就行了,不用包含与缓冲区本身相关的不必要信息。
但还是需要缓冲区头这个概念,因为它还负责描述磁盘块到页面的映射。 bio结构体不包含任何和缓冲区相关的状态信息—它仅仅是一个向量数组,描述一个或多个块I/O操作的数据段和相关信息。在当前设置中,当bio结构体描述当前正在使用的I/O操作时,buffer_head结构体仍然需要保护缓冲区信息。内核通过两种结构分别保存各自的信息,可以保证每种结构所包含的信息量尽可能地少。