本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-block-layer/ Linux 内核中的 block I/O 层又是非常重要的一个概念,它相对字符设备的实现来说复杂很多,而且在现今应用中,block 层可以说是随处可见,下面分别介绍 kernel block I/O 层的一些知识,你需要对块设备、字符设备的区别清楚,而且对 kernel 基础有一些了解哦。 1、buffer_head 的概念: buffer_head 是 block 层中一个常见的数据结构(当然和下面的 bio 之类的结构相比就差多了哦,HOHO)。 当块设备中的一个块(一般为扇区大小的整数倍,并不超过一个内存 page 的大小)通过读写等方式存放在内存中,一般被称为存在 buffer 中,每个 buffer 和一个块相关联,它就表示在内存中的磁盘块。kernel 因此需要有相关的控制信息来表示块数据,每个块与一个描述符相关联,这个描述符就被称为 buffer head,并用 struct buffer_head 来表示,其定义在 <linux/buffer_head.h> 头文件中。 b_state 字段说明这段 buffer 的状态,它可以是 bh_state_bits 联合(也在上面的代码中,注释说明状态,应该比较好明白哦)中的一个或多个与值。b_count 为 buffer 的引用计数,它通过 get_bh、put_bh 函数进行原子性的增加和减小,需要操作 buffer_head 时调用 get_bh,完成之后调用 put_bh。b_bdev 表示关联的块设备,下面会单独介绍 block_device 结构,b_blocknr 表示在 b_bdev 块设备上 buffer 所关联的块的起始地址。b_page 指向的内存页即为 buffer 所映射的页。b_data 为指向块的指针(在 b_page 中),并且长度为 b_size。 在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的数据结构,它曾经是 kernel 中 I/O 的基本单位(现在已经是 bio 结构),它曾被用于为一个块映射一个页,它被用于描述磁盘块到物理页的映射关系,所有的 block I/O 操作也包含在 buffer_head 中。但是这样也会引起比较大的问题:buffer_head 结构过大(现在已经缩减了很多),用 buffer head 来操作 I/O 数据太复杂,kernel 更喜欢根据 page 来工作(这样性能也更好);另一个问题是一个大的 buffer_head 常被用来描述单独的 buffer,而且 buffer 还很可能比一个页还小,这样就会造成效率低下;第三个问题是 buffer_head 只能描述一个 buffer,这样大块的 I/O 操作常被分散为很多个 buffer_head,这样会增加额外占用的空间。因此 2.6 开始的 kernel (实际 2.5 测试版的 kernel 中已经开始引入)使用 bio 结构直接处理 page 和地址空间,而不是 buffer。 2、bio: 说了一堆 buffer_head 的坏话,现在来看看它的替代者:bio,它倾向于为 I/O 请求提供一个轻量级的表示方法,它定义在 <linux/bio.h> 头文件中。 该定义中已经有详细的注释了哦。bi_sector 为以 512 字节为单位的扇区地址(即使物理设备的扇区大小不是 512 字节,bi_sector 也以 512 字节为单位)。bi_bdev 为关联的块设备。bi_rw 表示为读请求还是写请求。bi_cnt 为引用计数,通过 bio_get、bio_put 宏可以对 bi_cnt 进行增加和减小操作。当 bi_cnt 值为 0 时,bio 结构就被销毁并且后端的内存也被释放。 I/O 向量: bio 结构中最重要的是 bi_vcnt、bi_idx、bi_io_vec 等成员,bi_vcnt 为 bi_io_vec 所指向的 bio_vec 类型列表个数,bi_io_vec 表示指定的 block I/O 操作中的单独的段(如果你用过 readv 和 writev 函数那应该对这个比较熟悉),bi_idx 为当前在 bi_io_vec 数组中的索引,随着 block I/O 操作的进行,bi_idx 值被不断更新,kernel 提供 bio_for_each_segment 宏用于遍历 bio 中的 bio_vec。另外 kernel 中的 MD 软件 RAID 驱动也会使用 bi_idx 值来将一个 bio 请求分发到不同的磁盘设备上进行处理。 bio_vec 的定义也在上面的代码中,同样在 <linux/bio.h> 头文件中,每个 bio_vec 类型指向对应的 page,bv_page 表示它所在的页,bv_offset 为块相对于 page 的偏移量,bv_len 即为块的长度。 buffer_head 和 bio 总结: 因此也可以看出 block I/O 请求是以 I/O 向量的形式进行提交和处理的。 bio 相对 buffer_head 的好处有:bio 可以更方便的使用高端内存,因为它只与 page 打交道,并不直接使用地址。bio 可以表示 direct I/O(不经过 page cache,后面再详细描述)。对向量形式的 I/O(包括 sg I/O) 支持更好,防止 I/O 被打散。但是 buffer_head 还是需要的,它用于映射磁盘块到内存,因为 bio 中并没有包含 kernel 需要的 buffer 状态的成员以及一些其它信息。 3、请求队列: 块设备使用请求队列来保存等待中的 block I/O 请求,其使用 request_queue 结构来表示,定义在 <linux/blkdev.h> 头文件中,此头文件中还包含了非常重要的 request 结构: request_queue 中的很多成员和 I/O 调度器、request、bio 等息息相关。request_queue 中的 queue_head 成员为请求的双向链表。nr_requests 为请求的数量。I/O 请求被文件系统等上层的代码加入到队列中(需要经过 I/O 调度器,下面会介绍),只要队列不为空,block 设备驱动程序就需要从队列中抓取请求并提交到对应的块设备中。这个队列中的就是单独的请求,以 request 结构来表示。 每个 request 结构又可以由多个 bio 组成,一个 request 中放着顺序排列的 bio(请求在多个连续的磁盘块上)。 实际上在 request_queue 中,只有当请求队列有一定数目的请求时,I/O 调度算法才能发挥作用,否则极端情况下它将退化成 “先来先服务算法”,这就悲催了。通过对 request_queue 进行 plug 操作相当于停用,unplug 相当于恢复。请求少时将request_queue 停用,当请求达到一定数目,或者 request_queue 里最 “老” 的请求已经等待一段时间了才将 request_queue 恢复,这些见 request_queue 中的 unplug_fn、unplug_timer、unplug_thresh、unplug_delay 等成员。 4、I/O 调度器: I/O 调度器也是 block 层的大头,它肩负着非常重要的使命。由于现在的机械硬盘设备的寻道是非常慢的(常常是毫秒级),因此尽可能的减少寻道操作是提高性能的关键所在。一般 I/O 调度器要做的事情就是在完成现有请求的前提下,让磁头尽可能少移动,从而提高磁盘的读写效率。最有名的就是 “电梯算法” 了。 […]