20231005


块I/O

块设备:随机访问固定大小数据片(chunks)的硬件设备

常见:硬盘,软盘驱动器,闪存

以安装FS的方式使用

字符设备:按照字符流的顺序有序访问

常见:串口,键盘

内核管理块设备比管理字符设备细致的多:

  1. 因为工程量大
  2. 块设备对执行性能要求很高

改写块IO层是2.5开发版内核的主要目标

14.1 剖析一个块设备

块设备的基本单元:扇区

逻辑最小可寻址单元:块

内核要求,块的大小是2的整数倍,且不能超过一个页

常用的块大小:512,1K,4K

簇,柱面和磁头:针对特定的块设备

14.2 缓冲区和缓冲区头

每个缓冲区和一个块对应,相当于磁盘块在内存中的表示

每个缓冲区有一个对应的描述符,用buffer_head表示,称为缓冲区头

在<linux/buffer_head.h>中定义

struct buffer_head
&#123;
    unsigned long b_state;
    ...
    struct page *b_page;                /*存储缓冲区的页面*/
       sector_t b_blocknr;                    /*起始块号*/
    size_t b_size;                        /*映像的大小*/
    ...
      struct block_device *b_dev;            /*相关联的快设备*/
    ...
    atomic_t b_count;
&#125;

b_state表示缓冲区状态,可以是多种标志的组合

合法的标志存储在bh_state_bits枚举中,该枚举在<linux/buffer_head.h>中定义

状态标志 意义
BH_Uptodate 该缓冲区包含可用数据
BH_Dirty 缓冲区脏(内存磁盘不一致,需要更新磁盘)
BH_Delay 缓冲区尚未于磁盘块关联
BH_Boundary 处于连续块区的边界,下个块不再连续
BH_Quiet 缓冲区禁止错误
BH_Unwritten 缓冲区在硬盘上空间被申请,但没有实际数据写出

特殊位:BH_PrivateStart,指明可被其他代码使用的起始位

块IO不会使用BH_PrivateStart或更高的位

如果驱动希望b_state域存储信息,就可以安全使用这些位。只要不与使用冲突

b_count表示缓冲区的使用记数,操作缓冲区头前,先用get_bh()函数增加缓冲区头引用计数,确保其不会再被分配出去

对应的物理块由b_blocknr.th域索引,逻辑块号

块在内存的起始地址位b_page->b_data,结束为(b_data+b_size)处

2.6内核前,缓冲区头的作用更重要:不仅描述映射,还是所有块IO操作的容器

这样的弊端:

  • 缓冲区头很大(现在缩减)
  • 数据去偷对数据的操作不方便,不清晰,内核倾向操作页面结构

2.6内核后,内核直接对页面,地址空间操作,不再使用缓冲区头,具体情况见address_space结构,pdflush等守护进程(daemon)

第二个弊端:仅能描述单个缓冲区,对于大块数据IO需要分解为多个buffer_head结构体。会造成不必要的负担和空间浪费

2.5引入新型,灵活,轻量级的容器,bio结构体

14.3 bio结构体

块IO操作的基本容器由bio结构体表示,定义在<linux/bio.h>中。

该结构代表了现场的(活动的)以片段(segment)链表形式组织的块IO操作

一个片段是一小块连续的内存缓冲区。这样不需要单个缓冲区一定连续

即时一个缓冲区分散在内存多个位置,bio也能保证IO执行

这样的IO就是聚散IO

结构体定义于<linux/bio.h>

struct bio&#123;
    ...
    unsigned short    bi_vcnt;        /*bio_vecs偏移的个数*/
    unsigned short    bi_idx;            /*bio_io_vec的当前索引*/
    ...
    struct bio_vec    *bi_io_vec;        /*bio_vec链表*/
    ...
&#125;

14.3.1 IO向量

​ bi_io_vec包含一个特定IO操作所要的所有片段。每个bio_vec都是形式为<page,offset,len>的向量,定义于<linux/bio.h>

​ 当IO操作执行完毕后,bi_idx指向数组的当前索引

​ bi_idx不断更新,指向当前片段,更重要的作用在于分割bio结构体

​ RAID可以把单独的bio结构体分隔到阵列中的各个硬盘上

​ RAID设备只要拷贝结构体,将bi_idx改为需要的位置

​ bi_cnt记录使用计数

​ bi_private只用创建者可以读写

14.3.2 新老方法对比

bio代表IO操作,可以包括多个页

buffer_head代表一个缓冲区,描述磁盘的一个块

bio的好处:

  • 容易处理高端内存,处理页面而不是指针(page,offset,len)
  • 课代表普通页IO,也可代表直接IO

内核将两种结构尽量独立,使得信息尽可能少

14.4 请求队列

挂起的块IO保存在请求队列,由reques_queue结构体表示

定义在<linux/blkdev,h>,一个双向请求链表和相关控制信息

列表中每一项都是单独请求

请求由结构体request表示,定义在<linux/blkdev,h>

一个请求可能要操作多个连续磁盘块,每个请求可由多个bio组成

14.5 IO调度程序

缩短寻址时间是提高性能关键

预提交操作前,执行名为合并与排序的预操作

14.5.1 IO调度程序的工作

两种操作:合并与排序

合并

当访问的扇区与当前请求访问的扇区响铃,可以合并为一个

多个压缩为一个

排序算法:将扇区顺序有序排列,电梯调度

14.5.2 linus电梯

2.4采用,2.6被其他两种调度程序取代

执行向前和向后合并

合并失败,寻找可能的插入点

一个请求加入到队列时,会发生四种操作:

  1. 存在相邻请求,合并
  2. 驻留时间过长的请求,新请求插入队尾
  3. 存在以扇区方向为序的合适位置,插入到该位置
  4. 不存在合适位置,插入队尾

14.5.3 最终期限IO调度

deadline:解决饥饿问题

普通的饥饿请求还会带来写-饥饿-读的问题

写操作:内核有空才给磁盘,和提交的程序异步执行

读操作:和提交的程序同步执行

读请求可能会互相依靠

如何平衡减少请求饥饿和保证吞吐量,是困难的问题

deadline:读500ms,写5s

新请求提交时,类似linus电梯

也会按类型插入到额外队列

若写FIFO,读FIFO超时,从他们中取出请求

保证了读请求迅速完成

调度程序在文件block/deadline-ioshed.c中

14.5.4 预测IO调度程序

deadline不足:先读定位,再写定位,损害了全局吞吐量

预测IO:增加了预测启发功能anticipation-heuristic

试图在进行IO操作期间,处理新到的读请求带来的寻址数量

不同之处在于,提交请求后,不直接返回,而是有意空闲片刻

用于提交其它读请求,任何相邻操作的请求会得到处理

等待时间结束后,预测程序重新返回原来位置,处理其它请求

如果存在越来越多的同区域读请求,可以避免大量的寻址操作

优势取决于能否正确预测应用程序的形为

实现在block/as-ioshed.c中

14.5.5 完全公正的排队IO调度

complete fair queuing

根据进程组织:foo进程放入foo队列,bar进程放入bar队列

时间片轮转调度,每个队列选取请求数(默认为4),程序位于block/cfq-iosched.c

14.5.6 空操作的IO调度

只执行合并

不勤奋工作的原因:真正的随机访问设备,如闪存卡,没有寻道的负担,没必要插入排序

位于block/noop-ioshed.c

14.5.7 IO调度程序的选择

通过elevator=foo覆盖缺省(as,cfq,deadline,noop),默认cfq

14.6 小结

数据结构(bio,buffer_head)+4种调度算法


文章作者: N1co5in3
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 N1co5in3 !
  目录