Linux存储栈(二)
Linux存储栈(二)
对磁盘的I/O操作是一个耗时的操作,对于这部分时间的优化,有以下两个方面。
- 批量读取,以块的方式,批量读取将分摊到每一部分的数据的时间就变少。
- 预先处理,将磁盘的数据缓存到内存中,这样就把对磁盘的访问变为对物理内存的访问。
Page Cache
首先明确页高速缓存是一种软件的机制,只是将内存的一部分逻辑上化为缓存区,其内容对应磁盘上的物理块,因此其大小可以动态的调整。
读策略
当内核开始一个read()操作,首先会检查数据是否在页高速缓存中,如果在,直接从内存中读取,称为缓存命中,否则就要去磁盘读取文件,之后再将其放入页缓存。(过程类似于TLB存放页表,但TLB是一个实际的硬件,操作系统产生实际的页表项,并放入更新TLB)。
写策略
讨论写策略是在讨论
- 是否在内存缓存修改,如果不缓存,那么不存在同步问题,一旦更新那么就清空缓存,效率低
- 同步缓存,在内存中的修改,在什么时间点同步到硬盘
- 内存修改,立即更新,写穿(write through),这样不考虑不一致的失效问题,但是可以对磁盘的写入频繁,相应带来效率上的问题。
- 回写,是一种lazy的策略,先标记不更新,通过延迟写策略,方便之后的合并和一次刷新。(和fork进程中的copy-on-write,线段树的lazy tag都是同样的思想)。
回收策略
因为内存小于磁盘,所以必然要考虑回收替换策略,为LRU的优化和变种,如双链,和n链策略
Linux页高速缓存
文件缓存,主要是为了缓存内存和存储的速度因为内核基于块来访问物理文件系统,而磁盘块与内存中的缓冲区又是一一对应的映射关系,所以为了提高磁盘的存取效率,内核引入了缓冲区缓存机制,将通过虚拟文件系统访问的块的内容缓存在内存中。在早期版本的内核中,Page Cache和Buffer Cache是两个独立的缓存,前者缓存页,后者缓存块,由于一个磁盘块可以在两个缓存中同时存在,因此除了耗费额外的内存,还需要对两个缓存中的内容进行同步操作。从2.4.10版本内核开始,Buffer Cache不再是一个独立的缓存了,它被包含在Page Cache中,通过Page Cache来实现。对于4KB大小的page来说,根据不同的块大小,它可以包含1~8个缓冲区。
x86体系中,物理页大小为4KB,文件系统的块大小为512 byte = 512B,故一页可以缓存8个块。
linux内核中使用address_space
这个对象来管理内存缓存页3,其结构定义在<linux/fs.h>
,因为read/write
,这些系统调用执行页I/O,最终是通过file->f_op->read()
和file->f_op->write()
来完成
1 | /** |
其中比较重要的是基树的实现,因为频繁的检查和搜索,搜索的开销会抵消内存代替磁盘的开销,所以引入了基树,每个address_space
对象都有一个唯一的基树。
flusher线程
这是一个内核线程,周期性地运行,用来实现我们之前提到的关于写策略。
其执行的条件为:
- 空闲内存低于一个阈值,内核将脏页写回,释放内存
- 脏页在内存中驻留时间超过某个阈值
- 用户主动调用
sync()
和fssync()
我们可以在proc/sys/vm
配置相关参数。
块I/O层(Block Layer)
维持一个I/O请求在上层文件系统与底层物理磁盘之间的关系是由通用块层(Generic Block Layer)来负责的。在通用块层中,使用bio结构体来描述一个I/O请求,而到了Linux驱动,则是使用request结构体来描述向块设备发出的I/O请求的。对于慢速的磁盘设备而言,请求的处理速度很慢,这时内核就提供一种队列的机制把这些I/O请求添加到队列中(请求队列),使用request_queue
结构体描述。为了提高访问效率,在向块设备提交这些请求前,内核会先通过一定的调度算法对请求进行合并和排序预操作,对连续扇区操作的多个请求进行合并以提高执行效率,这部分工作由I/O调度器负责。
bio 与 request
对磁盘块的请求,最终被抽象为一个bio
。其中,bio
描述了磁盘里要真实操作的位置与Page Cache中页的映射关系。bio在块层会被转化为reques
t,多个连续的bio
可以合并到一个request
中,生成的request
会继续进行合并、排序,并最终调用块设备驱动的接口将request从块层的request队列(request_queue)中移到驱动层进行处理,以完成I/O请求在通用块层的整个处理流程。request用来描述单次I/O请求,request_queue
用来描述与设备相关的请求队列,每个块设备在块层都有一个request_queue
与之对应,所有对该块设备的I/O请求最后都会流经request_queue
。块层正是借助bio
、bio_vec
、request
、request_queue
这几个结构将I/O请求在内核I/O子系统各个层次的处理过程联系起来的。
bio_vec
结构是形式为<page,offset,len>
的向量,也就是说,bio
是对物理页中中的块进行操作。
I/O调度程序
I/O调度,主要从吞吐量和截止时间两个指标来考虑。
电梯调度则是比较经典的从吞吐量的算法,但是这种会造成饥饿,也就是说特定情况下某些I/O迟迟得不到调度,而比较经典的是写-饥饿-读,也就是写饥饿。考虑截止时间比较经典的最终期限I/O调度,为每一个I/O请求都加入一个截止时间,并且分成排序队列,读FIFO队列,写FIFO队列,正常情况下安装排序->派发的情况,但是一旦读/写队列超时,那么读/写队列队头出队,直接加入到派发队列。除此之外还有预测I/O调度,和完全公平的排队I/O调度队列。
可以在内核中配置不同的I/O调度程序,参数elevator,默认为完全公平的I/O调度程序。
参数 | I/O调度程序 |
---|---|
as | 预测 |
cfq | 完全公平的排队 |
deadline | 最终期限 |
noop | 空操作 |