系统中能够随机访问固定大小的数据片的硬件设备称作块设备,常见的块设备是硬盘;另一种设备类型是字符设备,按照字符流有序访问,如串口和键盘。
内核管理块设备要比管理字符设备细致得多,因此内核为块设备提供了一个专门服务的子系统。块设备对执行性能的要求很高,对硬盘每多一份利用都会对系统带来明显的提升。
剖析一个块设备
扇区 —— 设备的最小寻址单元,又称为”硬扇区“ 或 ”设备块“,
块 —— 文件系统的最小寻址单元,又称为”文件块“或”I/O块“
虽然物理磁盘寻址是按照扇区进行级进行的,但是内核执行的所有磁盘操作都是按照块进行的。系统对块大小的最终要求是:必须是扇区大小的2的整数倍,并且要小于页面大小,所以通常是块大小的512字节、1KB或者4KB。
缓冲区和缓冲头
一个块被调入内存时,存储于一个缓冲区中,即缓冲区与块对应。由于内核在处理数据时需要相关的控制信息,所以每个缓冲区都有一个对应的描述符,用buffer_head结构体表示,称作缓冲区头,在文件<linux/bffer_head.h>中定义,他包含了内核缓冲区所需的全部信息。
与缓冲区对应的磁盘物理块由 b_blocknr-th 域表示,与缓冲区对应的内存物理页由 b_page 域表示。 缓冲区头的目的在于描述磁盘块和物理内存缓冲区之间的映射关系。
缓冲区头体积大且操作不方便,所以2.6版本之后引入一种新型、灵活且轻量级的容器,即bio结构体。
bio 结构体
目前内核中块IO的基本容器由bio结构体表示,定义在文件 <linux/bio.h> 中。
bio 描述了多块连续的片段内存缓冲区,即使缓冲分散在内存的多个位置,bio也能对内核保证IO操作的执行,像这样的IO就是所谓的聚集IO。
利用 bio 结构体代替 buffer_head 结构体还有以下好处:
- bio 结构体容易处理高端内存,因为直接处理物理指着而非直接指针
- bio 结构体既可代表普通页 IO,也可以代表直接 IO
- bio 结构体便于执行分散-集中块IO操作,数据可取自多个物理页面
- bio 相比缓冲区头属于 轻量级的结构体,只需包含块IO操作所需的信息
I/O调度程序
在内核中负责提交I/O请求的子系统成为 I/O 调度程序,它在提交前先执行合并和排序的预操作。
进程调度程序与I/O调度程序不同,前者将处理器资源分配给系统中的运行进程;后将磁盘 I/O 资源分配给系统中所有挂起的块 I/O 请求。以降低磁盘寻址时间,确保磁盘性能最大化。
I/O调度程序通过两种方法减少磁盘寻址的时间:合并和排序。通过合并请求,将多次请求的开销压缩成一次请求,明显减少系统开销和磁盘寻址次数。通过排序,优化磁盘头的运行方向,缩短整体请求的磁盘寻址时间。
在2.4内核版本中,Linus 电梯算法是默认的 I/O 调度程序,能执行合并与排序预处理,但存在饥饿现象(一个磁盘同一位置操作的请求流会造成较远位置的其他请求得不到机会,是一种不公平的饥饿现象)。
最终期限I/O调度算法解决Linus电梯带来的饥饿问题。首先给每个请求设置一个超时时间,默认情况下,读请求的超时时间是500ms,写请求的超时时间是5s。此外按磁盘物理位置维护请求队列(排序队列),同时维护一个读FIFO队列和一个写FIFO队列。对于普通操作而言,先从排序队列头部取下,推入派发队列中(FIFO),新请求总被加入队列尾部,避免了饥饿现象。最后,在请求超时时会优先调用。
预测 I/O 调度程序是Linux 中默认的I/O调度程序。其主要改进是增加了预测启发,在请求提交之后并不直接返回,而是有意空闲片刻,等待相邻的磁盘操作请求。
完全公正的排队I/O调度程序(CFQ) 为每个进程创建一个请求队列,再将I/O请求放到对应的队列中。通过时间片轮转调度队列,在线程级实现了公平。
空操作 I/O 调度程序为随机访问设备设计(闪存卡),只将新的请求进行合并而不排序,保证FIFO。