块I/O
块设备:随机访问固定大小数据片(chunks)的硬件设备
常见:硬盘,软盘驱动器,闪存
以安装FS的方式使用
字符设备:按照字符流的顺序有序访问
常见:串口,键盘
内核管理块设备比管理字符设备细致的多:
- 因为工程量大
- 块设备对执行性能要求很高
改写块IO层是2.5开发版内核的主要目标
14.1 剖析一个块设备
块设备的基本单元:扇区
逻辑最小可寻址单元:块
内核要求,块的大小是2的整数倍,且不能超过一个页
常用的块大小:512,1K,4K
簇,柱面和磁头:针对特定的块设备
14.2 缓冲区和缓冲区头
每个缓冲区和一个块对应,相当于磁盘块在内存中的表示
每个缓冲区有一个对应的描述符,用buffer_head表示,称为缓冲区头
在<linux/buffer_head.h>中定义
struct buffer_head
{
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;
}
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{
...
unsigned short bi_vcnt; /*bio_vecs偏移的个数*/
unsigned short bi_idx; /*bio_io_vec的当前索引*/
...
struct bio_vec *bi_io_vec; /*bio_vec链表*/
...
}
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被其他两种调度程序取代
执行向前和向后合并
合并失败,寻找可能的插入点
一个请求加入到队列时,会发生四种操作:
- 存在相邻请求,合并
- 驻留时间过长的请求,新请求插入队尾
- 存在以扇区方向为序的合适位置,插入到该位置
- 不存在合适位置,插入队尾
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种调度算法