learn-tech/专栏/计算机基础实战课/31外设通信:IOCache与IO调度.md
2024-10-16 10:18:29 +08:00

142 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
31 外设通信IO Cache与IO调度
你好我是LMOS。
从这节课开始我们进入IO相关基础知识的学习想要开发高性能的应用程序这些基础知识必不可少。
前面的课程里我们已经对进程和内存有了一定了解。进程在运行时刻和CPU是紧密相关的抽象出进程就是为了提高CPU的利用率。因此我们关注进程和内存等同于关注CPU和RAM。
一个计算机系统无论是PC还是手机除了有CPU和RAM还有各种外设如键鼠、硬盘、显卡、以太网卡、声卡等各种USB扩展设备。
这些设备独立在CPU和内存之外统称为外设。但是外设通信的速度、大小、数据类型和传输方式各不相同所以为了实现系统的整体效率最大化操作系统实现了IO Cache和IO调度。今天我们就来研究它们。
IO Cache
顾名思义Cache即为缓存IO是指令外设传输IN/OUT数据的操作。
缓存是怎么回事我们都知道由此我们就可以这样理解IO Cache把外设的IO操作的数据保存起来当重新执行IO操作时先从之前保存的地方开始查找若找到需要的数据即为命中这时就不要去操作外设了若没有命中就去操作外设。其中的数据会根据IO操作频率进行组织把操作最频繁的内容放在最容易找到的位置达到性能最优化。
我们在终端中输入如下命令,感受一下
free -m
该命令是用来显示Linux系统上内存的使用情况的单位以MB计。
输入这条命令,我们会得到如下图所示的情况:
上图中的buff/cache就是我们所说的IO Cache占用的内存。从这个角度是不是看得更透彻了所谓IO Cache不过是操作系统基于某种算法管理的一块内存空间用该内存空间缓存IO设备的数据应用多次读写外设数据会更方便而不需要反复发起IO操作。
其实早期的Cache是位于CPU和内存之间的高速缓存由于硬件实现的Cache芯片的速度仅次于CPU而内存速度远小于CPUCache只是为了缓存内存中的数据加快CPU的性能避免CPU等待内存。而Buffer是在内存中由软件实现的用于缓存IO设备的数据缓解由于IO设备过慢带来系统性能下降。
但是现在Buffer和Cache成了在计算机技术中被用滥的两个名词。在Linux的内存管理中Buffer指Linux内存的Buffer Cache而Cache是指Linux内存中的Page Cache翻译成中文可以叫做缓冲区缓存和页面缓存用来缓存IO设备的读、写数据。补充一句这里的IO设备主要指的是块设备文件和文件系统上的普通文件。
在当前的Linux内核中BufferCache建立Page Cache之上如下图所示
在现代Linux的实现中远比上图画得要复杂得多不过我们只需要关注这个层次结构就行了。Buffer Cache中有多个小块组成块大小通常为512字节在Linux内核中用一个struct Bio结构来描述块而一个物理内存页中存在多个块多个struct Bio结构形成Buffer Cache多个这种页就形成了Page Cache。
在操作系统理论中这一套实现机制被抽象为IO Cache。但是各种操作系统的实现的叫法不同在此不必展开了我们只需要明白它们能在内存中缓存设备数据就行了。
我们明白了Buffer Cache和Page Cache的概念下面我们以Linux读写硬盘的过程为例研究一下IO操作时IO Cache发挥的作用。
一般情况下Linux内核中的IO操作会从上至下经过三大逻辑层具体如下
文件系统层。因为Linux中万物皆为文件IO操作首先会经过文件系统Linux为了兼容不同的文件系统对文件、目录等文件系统对象进行了抽象形成了VFS层也是IO操作经历的第一层。
块层。Linux内核把各种设备分成块设备字符设备、网络设备和硬盘都属于块设备块层主要负责管理块设备的 IO 队列,对 IO 请求进行合并、排序等操作。
设备层。具体设备驱动通过 DMA 与内存交互,完成数据和具体设备之间的交换,此例子中的设备为硬盘。
我们画一幅图,表示一下这个过程:
IO操作在到达Linux的VFS层后会根据相应的IO操作标志确定是DirectIO还是BufferedIO如果是前者则不经过Cache直接由块层发送到设备层完成IO操作如果是后者则IO操作到达Page Cache之后就返回了。
在某一时刻Linux会启动pdflush线程该线程会扫描PageCache中的脏页进而找到对应的Bio结构然后把Bio结构发送给块层的IO调度器调度器会对bio进行合并、排序以提高IO效率。
之后调用设备层的相关函数将Bio转发到设备驱动程序处理设备驱动程序函数对IO请求队列中每个Bio进行分别处理根据Bio中的信息向磁盘控制器发送命令。处理完成后调用Bio完成函数以通知上层完成了操作。这便是一个IO操作的过程。
IO调度
在前面我们已经明白了IO Cache的概念它本质是把IO操作的数据保存在内存中使得在读取外设数据时能直接从内存中读取或者数据缓存到一定量时由一个特定任务在以后的某个时间批量地写入外设这不但会提高系统整体吞吐量还能保护设备以延长寿命。
我们把IO操作缓存起来了这样操作系统就对IO操作有了控制权具体点说就是可以对IO操作进行调度。
我先不直接说明IO调度是干什么的先结合例子带你一起分析看看。我们从软件层面来看一个场景假如一个应用程序往硬盘中写入1GB大小的文件但是这个应用程序很调皮它每次只写入一个字节。如果没有 IO Cache和IO调度可以想见这需要发生多少次IO操作才能完成如果硬件能说话估计要骂人。
再来说说硬件自己结构的问题,这里以机械硬盘为主。千万不要感觉机械硬盘已经淘汰了,其实在很多服务器上仍然大量使用它。硬盘结构如下所示:
一个硬盘中有多个盘片一个盘片上有多个同心圆组成的多条磁道每条磁道上有多个扇区一个扇区512字节磁头来回移动经过多个同心圆形成的柱面定位到一个扇区。很显然找到一个扇区花费的时间等于磁头移动时间加上盘片旋转的时间。这些运动都是机械运动是非常缓慢的。
以上两个场景提醒我们有两个问题需要考虑一是怎么降低IO操作次数二是如何优化硬盘寻址。这两个问题解决好了都能大大提升系统性能。想解决第一个问题我们可以对IO操作进行缓存和合并而对于第二个问题我们可以对IO操作进行排序能让硬盘磁头按照一定的顺序定位扇区解决这些问题的就是IO调度器。
有了IO调度器还得有相应的调度算法IO调度器提供了多种调度算法来适应不同的IO请求场景。有的场景需要的是提高IO吞吐量比如数据库后台的储存引擎有的场景则是要降低IO响应时间比如游戏应用程序。
我们先看看第一种调度算法该算法名为Noop。Noop是最简单的IO调度算法其实可以说它是没有“调度”的IO调度因为Noop会把所有的IO请求几乎按照先来后到的顺序放入先进先出队列之中。
之所以说“几乎”是因为Noop在先进先出队列的基础上还做了相邻IO操作的合并而不是完完全全按照先进先出的规则满足IO操作。我来给你画一幅图展示一下这个算法实施的操作如下所示
一个个BIo结构进入Noop IO调度器产生request结构这个结构中包含Bio链表。Noop IO调度器把扇区相邻的Bio合并在一起形成request结构然后将requset结构挂载到块设备的requset_queue中块设备通常是你的硬盘。
然后我们来看看第二种调度算法该算法名为CFQ全称为Completely Fair Queuing。由于传统的机械硬盘上硬盘寻址花去了绝大多数的IO操作的时间所以要优化硬盘寻址所花的时间。
CFQ调度器的出发点就是对IO操作扇区地址进行排序比如硬盘旋转到1号扇区很快就旋转到2号扇区如果你先访问2号扇区再次访问1号扇区则要等到硬盘旋转一周后才能到达1号扇区。CFQ调度器对其进行排序后就能通过尽量少的硬盘旋转次数来满足尽可能多的IO操作。CFQ调度器算法执行逻辑如下图所示
我们看到在CFQ调度器下将多个BIO结构生成requset结构时会按照扇区地址升序挂载到块设备的requset_queue中这会使机械硬盘的吞吐量大大提高。
相比Noop调度器不知道你有没有发现一个问题先来的IO操作并不一定能被满足还可能会出现饿死的情况。比如先来一个IO操作扇区地址是1000然后不停地进入扇区地址小于1000的IO操作就会出现饿死现象。
我们来看一看最后一种IO调度算法该算法名为DeadlineDeadline调度器提供了两个红黑树以及两个先进先出队列两个红黑树分别对读、写的IO操作按照其扇区地址排序同时给每个IO操作添加超时时间并插入到对应的读、写先进先出的队列尾部。这样一来一个IO操作会同时挂在红黑树和先进先出队列中。
当Deadline调度器在发送一个IO操作时会综合考虑IO操作是否超时、是否饥饿由此决定到底发送哪个IO操作发送IO操作之后会将该IO操作同时在红黑树和先进先出队列中删除。
我来画一幅图,展示一下这个算法实施的操作,如下所示:
上图中读写队列分开同时用红黑树对其排序而且还加入了超时机制。硬盘驱动会找Deadline IO调度器获取IO requestDeadline IO调度器根据这些数据结构和算法分配request完美地解决了CFQ IO调度器的缺陷由于读写分开且读优先于写导致该算法非常适合数据库这种随机读写的场景。
我们发现IO调度器算法多种多样那么要怎么选择呢
其实选择IO调度器算法既要考虑硬件特性也要考虑应用程序场景。在传统的机械硬盘上CFQ、Deadline算法是不错的选择对于专属的数据库服务器Deadline IO调度器的IO吞吐量和IO响应时间综合性能都表现非常好。
然而在新兴的固态硬盘比如SSD、NVMe上最简单的NOOP IO调度器反而是最好的IO调度器。因为CFQ和Deadline调度算法最主要是为缩短机械硬盘寻址时间而优化的而固态硬盘没有所谓的机械运动寻址部件需要的时间而且很快能准备好数据所以IO响应时间非常短。
重点回顾
今天我们一起学习了外设通信中的重要组件——缓存它主要是在内存中开辟一大空间来暂时保存与外设通信的大量数据。这一点我们通过在Linux上输入free命令已经看到其实其它操作系统也具有类似机制这里我们只是以Linux为例子。
为了搞明白IO Cache的概念我们从Linux的缓存结构入手发现Linux用物理内存页面为基础建立了Page Cache。在这个Page Cache之上又建立了Buffer CacheBufferCache组织了传输到IO设置上的数据块。我们通过对IO流程的探讨发现IO操作可以不经过IO Cache而是直接到达设备。
之后我们对软件场景和硬盘结构进行了讨论发现有了IO Cache以后还需要对IO请求进行调度才能使IO效率最大化针对不同的场景有不同IO调度器我们重点讨论了三种IO调度算法分别是Noop、CFQ、Deadline其中综合性能最好的是Deadline。然而硬件技术的升级又产生了固态硬盘导致这些IO调度器没有了用武之地不调度就是最好的调度。
这节课的导图如下所示:
思考题
操作系统为什么要开发 IO Cache
欢迎你在留言区和我交流讨论,如果觉得这节课对你有启发,别忘了分享给更多朋友!