Linux内核-块I/O

Posted by keys961 on April 3, 2019

0. 块设备与字符设备

块设备:能随机访问固定大小的数据片的设备

字符设备:只能按字符流顺序访问的设备

1. 块设备剖析

块设备的最小物理可寻址单元:扇区(设备块),一般是2的整数倍

内核访问块设备最小逻辑可寻址单元:块(文件块),一般是块个数2的整数倍,但不能超过一个页

簇、柱面、磁头是针对某些特定的块设备

2. 缓冲区和缓冲区头

一个块被载入内存时,它要存储在一个缓冲区中。每个缓冲区与一个块对应。

内核处理时,还需要一些相关的元数据,因此每个缓冲区对应一个描述符struct buffer_head,定义在<linux/buffer_head.h>

它存在的目的在于描述磁盘块和物理内存缓冲区之间的映射

不过现在I/O操作不怎么通过buffer_head,而是直接通过页面或者地址空间进行操作。

3. bio结构体

目前块I/O操作的基本容器由struct bio表示,它定义在<linux/bio.h>

它代表正在活动的以片段(segment)链表形式(链表下一个指针为bi_next域)组织的块I/O操作,每个bio代表一个操作。

每个bio有2个重要的域:

  • bi_io_vec数组(struct bio_vec),通过bi_idx找到对应的bio_vec对象,而bio_vec包含对应的页面,可进行直接操作
  • bi_cnt使用计数,若值为0,则要释放该结构体。操作时,增加计数,完成后,减少计数

3.1. I/O向量bio_vec

该结构体包含了一个特定I/O操作需要用到的片段,以<page, offset, len>向量表示(物理页、偏移、长度)。

3.2. biobuffer_head对比

bio的好处:

  • 可包括多个页,而非仅仅一个块(buffer_head
  • bio描述的块不需要连续,不需要分割I/O操作
  • 容易处理高端内存(HIGH_MEM
  • bio代表的页可以是普通页,也可以是直接I/O(不需要通过页高速缓存的操作)
  • 便于执行分散-集中的块I/O操作,数据可取自不同的物理页面
  • buffer_head更加轻量,因为不包含缓冲区本身的信息,只需要操作所需的信息即可(只是一个矢量数组)

4. 请求队列

块设备将请求保存在请求队列中,由struct request_queue结构体表示,定义在<linux/blkdev.h>中,包含一个双向链表和相关控制信息。

队列中的请求由struct request表示,而请求中可能要操作多个磁盘块,所以请求可由多个bio组成。

5. I/O调度程序

内核在提交I/O操作前,会继续预操作,以提高性能。

I/O调度程序将磁盘I/O资源分配给挂起的I/O请求,具体实现是将请求队列中的请求进行合并和排序,以降低磁盘寻址空间,提高全局吞吐量(可能对某些请求不公平)。

5.1. Linus电梯

Linus电梯能执行合并与排序预处理:

  • 向前/向后合并请求,若新请求的扇区位置正好连在现有请求之前,则向前合并,否则向后合并(通常向后,因为很少向反方向读写)
  • 若不能合并,则需要寻找插入点,要符合请求以扇区方向有序排序原则
  • 若不能找到插入点,则插入队列尾部
  • 若队列有驻留时间过长的请求,则新请求直接放到尾部,以缓解饥饿

5.2. 最终期限I/O调度

为了解决Linus电梯带来的饥饿问题而出现,但会降低全局吞吐量:

  • 每个请求都有一个超时时间(如读500ms,写5s)
  • 类似Linus电梯,维护一个特定的排序队列,合并和插入请求方法类似
  • 此外,请求还会被插入到特定的FIFO队列,读和写的队列分离(即读队列写队列
  • 提交请求时:
    • 从排序队列的头部取下请求,推入派发队列,派发队列将请求提交给磁盘驱动,并将请求从对应的读/写队列中移除
    • 若读/写队列头的请求超时,则直接从该队列头取出请求,推入派发队列,并将请求从排序队列中移除

它不能严格保证请求的响应时间,但通常情况下,能防止饥饿发生。尤其是读饥饿。

5.3. 预测I/O调度

Linux内核默认的调度。

基础是最终期限I/O调度——实现3个队列+派发队列,设置超时时间。主要的改进是增加了预测启发的能力

  • 读请求提交后,不直接返回处理其它请求,而会有意空闲片刻(默认6ms),任何相邻位置的请求都会得到处理,等待结束后,重新返回到原来的位置
  • 依靠启发和统计工作,跟踪进程的块I/O操作的习惯行为,以对读I/O请求进行优化

5.4. 完全公正排队I/O调度(CFQ)

CFQ是为专有工作负荷设计的,尤其是桌面工作负荷,但能在多种工作负荷下提供良好性能:

  • CFQ将I/O请求放入特定的队列,队列根据进程标识(如foo进程的请求进入foo队列)
  • 每个队列,尝试将新请求与相邻请求合并
  • 合并失败,则寻找插入点,尽量保证扇区方向的有序
  • 提交时,以时间片轮转(RR)调度队列,每次从队列中选取一定数量的请求(如4个),然后进行下一轮的调度

5.5. 空操作I/O调度

它专为随机访问设备设计。

除了尝试将新请求和相邻请求合并之外,其它什么都不做,队列近似以FIFO顺序排列。