记录一下
bio 代表一个IO 请求
request 是bio 提交给IO调度器产生的数据,一个request 中放着顺序排列的bio
当设备提交bio 给IO调度器时,IO调度器可能会插入bio,或者生成新的request
request_queue代表着一个物理设备,顺序的放着request
===========================分割线===========================
以下为一位强大tx的文字
3、Generic Block Layer,通用块层。
linux内核为块设备抽象了统一的模型,把块设备看作是由若干个扇区组成的数组空间。扇区是磁盘设备读写的最小单位,通过扇区号可以指定要访问的磁盘扇区。
上层的读写请求在通用块层被构造成一个或多个bio结构,这个结构里面描述了一次请求--访问的起始扇区号?访问多少个扇区?是读还是写?相应的内存页有哪些、页偏移和数据长度是多少?等等……
这里面主要有两个问题:要访问的扇区号从哪里来?内存是怎么组织的?
前面说过,上层的读写请求通过文件pos可以定位到要访问的是相应的磁盘高速缓存的第几个页,而通过这个页index就可以知道要访问的是文件的第几个扇区,得到扇区的index。
但是,文件的第几个扇区并不等同于磁盘上的第几个扇区,得到的扇区index还需要由特定文件系统提供的函数来转换成磁盘的扇区号。文件系统会记载当前磁盘上的扇区使用情况,并且对于每一个inode,它依次使用了哪些扇区。
于是,通过文件系统提供的特定函数,上层请求的文件pos最终被对应到了磁盘上的扇区号。
可见,上层的一次请求可能跨多个扇区,可能形成多个非连续的扇区段。对应于每个扇区段,一个bio结构被构造出来。而由于块设备一般都支持一次性访问若干个连续的扇区,所以一个扇区段(不止一个扇区)可以包含在代表一次块设备IO请求的一个bio结构中。
接下来谈谈内存的组织。既然上层的一次读写请求可能跨多个扇区,它也可能跨越磁盘高速缓存上的多个页。于是,一个bio里面包含的扇区请求可能会对应一组内存页。而这些页是单独分配的,内存地址很可能不连续。
那么,既然bio描述的是一次块设备请求,块设备能够一次性访问一组连续的扇区,但是能够一次性对一组非连续的内存地址进行存取吗?
块设备一般是通过DMA,将块设备上一组连续的扇区上的数据拷贝到一组连续的内存页面上(或将一组连续的内存页面上的数据拷贝到块设备上一组连续的扇区),DMA本身一般是不支持一次性访问非连续的内存页面的。
但是某些体系结构包含了io-mmu。就像通过mmu可以将一组非连续的物理页面映射成连续的虚拟地址一样,对io-mmu进行编程,可以让DMA将一组非连续的物理内存看作连续的。所以,即使一个bio包含了非连续的多段内存,它也是有可能可以在一次DMA中完成的。当然,不是所有的体系结构都支持io-mmu,所以一个bio也可能在后面的设备驱动程序中被拆分成多个设备请求。
每个被构造的bio结构都会分别被提交,提交到底层的IO调度器中。
4、I/O Scheduler Layer,IO调度器。
我们知道,磁盘是通过磁头来读写数据的,磁头在定位扇区的过程中需要做机械的移动。相比于电和磁的传递,机械运动是非常慢速的,这也就是磁盘为什么那么慢的主要原因。
IO调度器要做的事情就是在完成现有请求的前提下,让磁头尽可能少移动,从而提高磁盘的读写效率。最有名的就是“电梯算法”。
在IO调度器中,上层提交的bio被构造成request结构,一个request结构包含了一组顺序的bio。而每个物理设备会对应一个request_queue,里面顺序存放着相关的request。
新的bio可能被合并到request_queue中已有的request结构中(甚至合并到已有的bio中),也可能生成新的request结构并插入到request_queue的适当位置上。具体怎么合并、怎么插入,取决于设备驱动程序选择的IO调度算法。大体上可以把IO调度算法就想象成“电梯算法”,尽管实际的IO调度算法有所改进。
除了类似“电梯算法”的IO调度算法,还有“none”算法,这实际上是没有算法,也可以说是“先来先服务算法”。因为现在很多块设备已经能够很好地支持随机访问了(比如固态磁盘、flash闪存),使用“电梯算法”对于它们没有什么意义。
IO调度器除了改变请求的顺序,还可能延迟触发对请求的处理。因为只有当请求队列有一定数目的请求时,“电梯算法”才能发挥其功效,否则极端情况下它将退化成“先来先服务算法”。
这是通过对request_queue的plug/unplug来实现的,plug相当于停用,unplug相当于恢复。请求少时将request_queue停用,当请求达到一定数目,或者request_queue里最“老”的请求已经等待很长一段时间了,这时候才将request_queue恢复。
在request_queue恢复的时候,驱动程序提供的回调函数将被调用,于是驱动程序开始处理request_queue。
一般来说,read/write系统调用到这里就返回了。返回之后可能等待(同步)或是继续干其他事(异步)。而返回之前会在任务队列里面添加一个任务,而处理该任务队列的内核线程将来会执行request_queue的unplug操作,以触发驱动程序处理请求。
5、Device Driver,设备驱动程序。
到了这里,设备驱动程序要做的事情就是从request_queue里面取出请求,然后操作硬件设备,逐个去执行这些请求。
除了处理请求,设备驱动程序还要选择IO调度算法,因为设备驱动程序最知道设备的属性,知道用什么样的IO调度算法最合适。甚至于,设备驱动程序可以将IO调度器屏蔽掉,而直接对上层的bio进行处理。(当然,设备驱动程序也可实现自己的IO调度算法。)
可以说,IO调度器是内核提供给设备驱动程序的一组方法。用与不用、使用怎样的方法,选择权在于设备驱动程序。
于是,对于支持随机访问的块设备,驱动程序除了选择“none”算法,还有一种更直接的做法,就是注册自己的bio提交函数。这样,bio生成后,并不会使用通用的提交函数,被提交到IO调度器,而是直接被驱动程序处理。
但是,如果设备比较慢的话,bio的提交可能会阻塞较长时间。所以这种做法一般被基于内存的“块设备”驱动使用(当然,这样的块设备是由驱动程序虚拟的)。