518_缓冲区管理
# 5.1_8_缓冲区管理
在小节中我们会介绍这门课的最后一个知识点,缓冲区管理,我们会首先介绍什么是缓冲区,缓冲区有什么作用,之后会介绍几种常见常考的缓冲区的解决方案。
# 什么是缓冲区
首先来看一下什么是缓冲区,其实所谓的缓冲区就是一个存储区域,可以由专门的硬件寄存器组成,也可以由内存作为缓冲区,如果用硬件作为专门的缓冲区的话,一般来说成本会比较高,并且容量比较小。像咱们之前学到过的联想寄存器,也就是快表,它就是一种用硬件来实现的缓冲区。由于对快表的访问频率是很高的,所以如果快表的速度高的话,那么整个系统的性能提升会很明显,因此在这种情况下,使用这种成本高的解决方案是值得的。
但是一般情况下更多的是用内存作为缓冲区,像设备独立性软件这一层的缓冲区管理,主要其实指的是对内存作为缓冲区的这种缓冲区进行组织和管理。所以我们这一小节介绍的所有的这些缓冲区,主要还是围绕内存作为缓冲区这种类型
# 缓冲区的作用
首先来看一下缓冲区有什么作用。在内存中可以开辟一小片区域作为缓冲区
- 那么如果要输出数据的话,CPU 产生的这些数据首先会被放到内存的缓冲区当中,不过 CPU 的速度很快,所以它很快就可以把缓冲区给充满,等缓冲区放满了之后,CPU 就可以转头去做别的事情之后,IO 设备可以慢慢的从缓冲区当中取走数据;而在数据输入的时候其实也是类似的,IO 设备可以慢慢的把数据放到缓冲区当中,当缓冲区满了之后,CPU 在很快速的从缓冲区中取走数据,所以采用这种方式的话,很明显可以缓和 CPU 和 IO 设备之间的速度不匹配的矛盾。
- 如果说没有采用缓冲区这种策略的话,IO 设备每输入或者每输出一定单位的数据之后,就需要对 CPU 发出一个中断信号,请求 CPU 介入处理。假设 IO 设备是一种字符型的设备的话,每输入完一个字符或者每输出一个字符,IO 设备都会打断 cpo 向 CPU 发出中断信号,而之前咱们强调过对中断的处理序是需要付出一定的时间代价的,因此 CPU 频繁的处理这些中断显然会降低系统的性能,而如果采用缓冲区这种策略的话,只有缓冲区中的数据被全部取走了,或者输入的数据充满了缓冲区的,CPU 才需要介入来处理中断。因此采用这种方式可以减少 CPU 的中断频率,放宽 CPU 对中断响应时间的限制。
- 缓冲区还有一个作用就是解决数据粒度不匹配的问题。比如说此时在 CPU 上运行的输出进程,每次可以生成一整块的数据,但是 IO 设备每次只能输出一个字符,如果没有采用缓冲区策略的话,输出进程只能一个字符一个字符的给 IO 设备来传送数据,但如果采用的缓冲区这种策略的话,输出进程可以直接把一整块的数据放到缓冲区里,然后 IO 设备从缓冲区当中一个字符一个字符的往外去。在输入的时候其实也是类似的,缓冲区同样可以解决这种数据力度不匹配的问题
- 另外采用了缓冲区之后,很显然是可以提高 CPU 和 IO 设备之间的并行性的。
# 单缓冲
接下来我们介绍几种我们需要掌握的缓冲区管理的策略。首先来看一下什么是单缓冲,假设我们的用户进程请求某种快设备读入若干块的数据的话,如果我们采用的是单缓冲区的策略,那操作系统会在储存当中为用户进程分配一个缓冲区,而缓冲区的大小和块是相等的。
我们需要重点注意的是缓冲区有一个特点,当缓冲区当中的数据不空的时候,就不能往缓冲区里冲入数据,只能从缓冲区当中把数据传出。而如果缓冲区为空的时候,可以往缓冲区当中冲入数据,但必须把缓冲区充满之后,才可以从缓冲区当中把数据传出。
我们首先来看一下对一个数据块的处理需要经过哪些步骤。首先系统在主存中为这个用户进程分配了一块大小的缓冲区,那么这个块设备会产生一块大小的数据,把它输入到缓冲区当中,这个过程我们假设所需要耗费的时间为 t
那之后这块数据需要传送到用户进程的工作区当中,才可以被用户进程所处理。还记得我们之前小节中提到的例子吗?C 语言的 scanf 这个函数读入一个数字的时候,其实我们从键盘输入的数据,最终是要被传送到用户进程的内存空间当中的,所以在这个地方数据块被传入了缓冲区还不够,它还需要把数据块把它传送到用户进程的工作区当中。
同样的在考研当中,我们默认用户进程的工作区也是刚好可以放得下这样一块数据,所以接下来会用 m 这么长的时间,把缓冲区当中的数据传送到用户进程的工作区中,
之后用户进程就可以开始对这一块数据进行处理。我们假设对一块数据进行计算处理所需要的时间为 c 这么多,当它处理完之后,用户进程的工作区就可以被腾空了,
我们在考研当中经常考察的题型是会让我们计算每处理一块数据平均需要多长的时间,咱们的书上给出了一个很好用的技巧,我们可以假定一个初始状态,然后来分析下一次到达相同状态的时候需要多少时间,这个时间的长度就是处理一块数据平均所需要的时间的长度。直接看文字描述可能不太好理解,咱们之后结合动画来进一步的分析。
我们在单缓冲的问题当中,一般来说可以假设初始状态为工作区满,但是缓冲区空这种状态
所以我们要分析下一次达到工作区满,缓冲区空这种状态需要花多少时间,这个时间的长度就是处理一个数据快平均所需要消耗的时间。
来看第一种情况,假设输入时间 t,大于处理时间 c,根据我们假设的条件,刚开始工作区是满的,缓冲区是空的,所以刚开始 CPU 就可以处理工作区中的这一块数据了,处理的过程需要花 c 这么长的时间。
另外由于刚开始缓冲区就是空的,而我们刚才说过只要缓冲区空的话,其实是可以往缓冲区当中冲入数据的,所以刚开始快设备就会往缓冲区当中冲入一块数据,这个时间总共是耗费了 t这么久,把缓冲区充满总共需要花费 t 这么长的时间。由于数据充入的时间大于处理数据的时间,因此 CPU 在花了 c 这么长的时间处理完数据之后,它并不可以紧接着继续处理下一块的数据,因为当它处理完这个数据之后,其实缓冲区当中的数据是还暂时没有充满的,因此必须先等到缓冲区的数据充满,并且把这些数据传送到工作区之后,才可以进行下一块数据的处理。
那显然在 t 这个时间点缓冲区充满了,接下来可以紧接着把这一块数据传送到工作区当中,这个过程又需要花费 m 这么长的时间。还记得我们刚开始假设的初始状态吗?工作区满,缓冲区空,其实在经历了 t+m 这么长的时间之后,就再一次回到了我们刚刚假设的这种初始状态,也就是工作区缓冲区空。接下来的数据处理其实无非也就是重复刚才我们分析的这个过程
因此通过刚才的分析,我们发现平均每处理一块数,据需要花的时间是 t+m 这么多。
再来看第二种情况,假设输入一块数据的时间,要小于处理一块数据的时间的话,那么由于刚开始缓冲区就是空的,所以从 0 这个时刻,块设备就可以开始往缓冲区当中冲入数据,这个过程需要花费 t 这么长的时间可以把缓冲区充满。同时由于刚开始工作区是满的,所以 CPU 可以在 0 这个时刻就开始处理这块数据,但是这块数据处理的时间需要 c 这么长,而 c 是大于 t 的。因此虽然缓冲区当中的数据充满了,但是由于工作区中的这些数据暂时还没有处理完,所以在 t 这个时刻并不可以直接把缓冲区中的数据把它继续传送到工作区当中,必须等到工作区中的这块数据处理完了,也就是到 c 这个时刻才可以开始把这块数据传送到工作区当中。
所以接下来传送的过程需要耗费 m 这么长的时间,到这一步又回到了,我们刚开始假设的这种初始状态,工作区是满的,缓冲区是空的,所以这个过程总共耗费了 c+m 这么长的时间。经过 m 这么长的时间之后,缓冲区的数据就全部被取空了。因此接下来快设备又可以开始往里边输入数据,并且 CPU 也可以开始处理工作区当中的这块数据。
接下来的过程其实无非也就是在重复我们刚才所说的过程,因此可以发现如果 t 小于 c 的话,那么每处理一块数据的平均用时应该是 c+m 这么多
所以经过刚才的分析我们得到一个结论,如果采用的是单缓冲区的策略的话,那么每处理一块数据平均需要耗时 C 和 T 当中更大的一个再加 m 这么多的时间,这是对单缓冲策略的一个分析过程。
# 双缓冲策略
接下来我们再来看双缓冲策略,如果采用的是双缓冲策略的话,操作系统会在储存当中为进程分配两个缓冲区,同样的每个缓冲区的大小也是一个快,类似的,双缓冲问题,也经常会考察每处理一块数据平均需要耗时多久这样一个问题,我们和刚才的思路是类似的,我们可以假设一个初始状态,然后分析一下,下一次到达初始状态总共需要耗时多少,
我们假设刚开始工作区是空的,其中一个缓冲区是满的,而另一个缓冲区是空的。
先来看第一种情况,假设 t>c+m根据我们假设的初始状态,缓冲区一当中此时是有一块数据的,缓冲区二是空的,工作区也是空的,所以在 0 这个时刻可以把缓冲区一当中的数据传送到工作区当中,这个过程耗时应该是 m 这么多,
而接下来 CPU 就可以开始处理工作区当中的数据,耗时是 c 这么多。
另一方面在刚开始缓冲区二就是空的,所以刚开始块设备就可以往缓冲区二当中输入数据,充满这个数据总共需要耗时 t 这么多,由于 t>c+m所以虽然在这个时间点,CPU 已经把工作区当中的这块数据给处理完了,工作区已经空了,但是由于缓冲区二此时这个时刻还没有充满,所以暂时不能把缓冲区二当中的数据把它传送到工作区当中,必须等到 t 这个时间点,缓冲区二才可以被充满。
所以到 t 这个时刻其实就回到了我们假设的那种工作状态,工作区此时是空的,然后这两个缓冲区当中其中一个是空的,另一个是满的。因此当 t>c+m的时候,处理一块数据平均用时应该是 t 这么多,而接下来无非就是在对刚才咱们分析的这一系列过程,在进行重复。整体来看,每处理完一个数据快耗时都是 t 这么多。
接下来再看第二种情况,t<c+m刚开始缓冲区二是空的,所以刚开始设备可以往缓冲区二当中充入数据,耗时是 t 秒这么多。另一方面,由于刚开始缓冲区一当中是充满数据的,所以刚开始就可以把缓冲区 0 当中的数据传送到工作区当中,到 m 这个时刻工作区会被充满,然后 CPU 就可以开始处理这些数据处理数据,总共耗时 c 这么长。
处理完工作区当中的这块数据之后,此时缓冲区二当中其实也已经充满了下一块的数据了,因此接下来可以紧接着把缓冲区二当中的数据传送到工作区当中,接着继续处理工作区中的这块数据。
我们再回头来看数据输入的过程,在 t 这个时刻缓冲区二已经被充满,然后设备开始空闲,并且由于缓冲区一当中的数据在 m 这个时刻就已经被取空了,因此缓冲区二的数据被充满了之后,设备就可以紧接着往缓冲区一当中充入数据这个耗时也是 t 这么多。
我们假设在这个时间点,缓冲区二当中的数据还没有完全被取走,所以在这个时间点这儿,虽然设备空闲了,但是由于缓冲区二还没有被取空,而缓冲区一又刚刚被充满。因此在这个时间点,设备并不能紧接着往缓冲区二当中冲入下一块数据,只有缓冲区二当中的数据被取空之后,这个设备才可以继续往缓冲区二当中写入下一块的数据。
所以其实这样一步一步分析下来,大家会发现,如果采用的是双缓冲结构,并且 t<c+m的话,那么我们很难找到一个和刚开始的这种初始状态一模一样的状态。
比如说像 t 这个时刻,虽然其中缓冲区二是满的,但是由于此时工作区中的数据还没有全部被处理完,因此工作区又不是空的,这和我们刚开始的初始状态是不一样的;而在 m+c 这个时刻,虽然我们的工作区是空的,但是缓冲区二当中的数据是满的,并且缓冲区一当中也充入了一部分数据,也就是说在 m+c 这个时刻工作区是空的,然后一个缓冲区是满的,另一个缓冲区是半满的,所以如果 t<c+m的话,课本当中介绍的这种方法其实就不太好使了。
所以自己用这种甘特图的方式多往下分析几次就会发现,其实每经过 m+c 这么长的时间,就会有一块数据被处理完毕。因此当 t<c+m的时候,每处理一个数据块平均耗时应该是 c+m 这么多。
所以经过刚才的这两种情况的分析,我们发现如果 t>c+m的话,那么平均处理一个数据快需要 t 这么长的时间。而如果 t<c+m的话,那么平均处理一个数据块需要的是 m+c 这么长的时间。
# 单缓冲和双缓冲在通信时的区别
像单缓冲和双缓冲策略,其实不仅仅是在主机和设备之间的这种数据传送当中可以使用,其实在两台主机通信时也可以采用这种缓冲的策略用于数据的发送和接收。
如果说为两台通信的主机配置单缓冲区的话, a 主机想要发送数据,首先需要把这些数据放入到自己的缓冲区当中,等缓冲区充满之后,就可以往 b 主机的缓冲区当中一个的发送数据,
之后 b 主机可以开始处理这些数据,等这些数据处理完之后,缓冲区才会变空,只有等这些数据全部被取走,全部处理完之后, b 主机的缓冲区才可以被腾空。而只有缓冲区中的数据被全部取走之后,b 主机要向 a 主机发送的数据才可以开始放入到缓冲区当中。
所以如果两台通信的机器只设置单缓冲区的话,那么在任何一个时刻都只能实现数据的单向传输。这其中的原因其实就是咱们之前强调过的缓冲区的特性决定的,缓冲区只有充满之后才可以取走数据,而只有取空之后才可以往里边放入数据
为了实现同一时刻双向传输,我们可以给两台机器配置双缓冲区,其中一个缓冲区用来暂存即将发送的那些数据,而另一个缓冲区用于接收输入的数据。
所以如果采用双缓冲的结构的话,那么这两台主机都可以同时往自己的发送缓冲区当中,冲入自己想要发送出去的数据,接下来可以同时往对方的接收缓冲区当中冲入数据,这样的话就实现了同一时刻双向传输的功能。
讲到这个地方,大家可以再回忆一下咱们在进程通信小节当中提到过的管道通信这种方式,有很多同学都有疑问,说为什么管道通信当中只有管道充满的时候才可以取出数据,只有管道取空的时候才可以往管道里放入数据,其实管道通信当中的管道它就是一种缓冲区,而由缓冲区的特性就决定了管道通信就应该是那样的。
所以在管道通信中我们也强调过,如果要实现双向传输的话,必须设置两个管道,也就是设置两个缓冲区,这是缓冲区这一块的知识点和管道通信的一个联系。
# 循环缓冲区
接下来我们再来学习下一种常见的缓冲区叫做循环缓冲区,很多时候只有两个缓冲区依然不能满足进程的实际需要,所以操作系统可以给一个进程分配多个大小相等的缓冲区,让这些缓冲区连成一个循环的队列。
在这个图当中橙色表示的是已经被充满数据的缓冲区,而绿色表示的是此时为空的缓冲区,那系统会保持两个指针用于缓冲区的管理。in 指针需要指向下一个可以充入数据的空缓冲区,而 out 指针可以指向下一个可以取出数据的满的缓冲区
当缓冲区的数据被取空之后,out 指针就可以开始指向下一个缓冲区了。类似的如果 in 指针所指向的缓冲区被充满之后,那么 in 指针也需要指向下一个空的缓冲区,这是循环缓冲区的方式。
一般来说只有单缓冲和双缓冲,需要我们分析处理一块数据平均所需要消耗的时间。循环缓冲区我们只需要了解一个它的大致原理就可以了。接下来我们再来看一个不太容易理解的缓冲池,
# 缓冲池
这种方案。缓冲池其实是由一系列的缓冲区组成的,就像它这个名字一样,缓冲池其实就是放满了各种各样缓冲区的一个池子,当我们需要使用一个缓冲区或者要归还一个缓冲区的时候,就可以对池子当中的各种缓冲区进行操作就可以了
所以缓冲池当中的缓冲区可以按照使用状况分为空缓冲队列,还有装满了输入数据的输入队列,还有装满了输出数据的输出队列,这样三个队列组成。
另外根据一个缓冲区在实际运算当中所扮演的功能不同,又设置了 4 种工作缓冲区,一个是用于收容输入数据的工作缓冲区,还有用于提取输入数据的工作缓冲区,还有用于收容输出数据的工作缓冲区,和用于提取输出数据的工作缓冲区
这些名词都很奇怪并且很拗口,不过其实它们的原理并不复杂,如果一个输入进程要请求输入一块数据的话,那么系统会从空缓冲队列的对头当中取下这一块空的缓冲区,把它作为用于收容输入数据的缓冲区,
当这块缓冲区被充满之后,就会被挂到输入队列的队尾上
如果计算进程想要取得一块之前已经输入了的数据的话,那么操作系统会从输入队列的队头取下一个缓冲区,
把它作为提取输入的工作缓冲区,接下来这块缓冲区当中的数据会被传送到计算进程的工作区当中,所以这块缓冲区当中的数据就被取空了。当它取空了之后,缓冲区又会被挂回到空缓冲队列的队尾。
第三种情况,如果此时计算进程已经准备好了,数据想要把这些数据冲入到缓冲区的话,那么系统会从空缓冲队列的对头取下一个空闲的缓冲区,
把这个缓冲区作为收容这个进程想要输出数据的工作缓冲区。因此接下来这个缓冲区慢慢的会被充满,由于这块缓冲区当中的数据接下来是要输出到 IO 设备上的,所以这块数据会被挂到输出队列的队尾
第四种操作,如果某个输出进程请求输出一块数据的话,那么系统会从输出队列的对头取下一块缓冲区,把它作为提取输出数据的工作缓冲区,
接下来这块缓冲区当中的输出数据会被慢慢取走,当缓冲区被取空之后,就可以把缓冲区挂回到空缓冲区队列的队尾了,
所以这是缓冲池相关的几种操作和原理。
# 小结
那么这个小节我们介绍了缓冲区管理相关的知识点,我们需要理解缓冲区的各种优点,需要对这些优点有一些大体的印象,可以应付选择题。
另外大家需要重点注意的是单缓冲和双缓冲结构当中,每处理一块数据平均需要耗时多少,这种题型是经常在选择题当中出现的。
最后我们还介绍了循环缓冲和缓冲池,对于这两种缓冲管理方案来说,我们并不需要去研究太深的那些细节,我们只需要对它们的原理有一个大体的印象,理解就可以了。