Linux块设备I/O
在我的一篇博客 操作系统概述 中我画了一张关于Linux的块设备I/O分层的示意图,就是下面这张图。这里再写篇整理一下这张图涉及到的知识点,尽量避免陷入源码细节,做到先有宏观认知,再了解各个分层的功能与简单原理即可。
块设备
系统中能够随机(不需要按顺序)访问固定大小数据片的硬件设备叫做块设备,这些固定大小的数据片就叫做块。最常见的块设备就是硬盘了。另一种常见的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像键盘就是最常见的字符设备。
对于这两种设备,最重要的区别就是“是否可以随机访问”。举个例子,键盘这种设备提供的就是一个数据流,当我们输入“wolf”这个字符串时,键盘驱动程序会按照和输入完全相同的顺序返回这个由四个字符组成的数据流,对键盘的读操作会得到这个数据流,首先从数据流中读取字符“w”,之后时“o”,再接着就是“l”和“f”。当没有人敲键盘时,数据流就是空的。硬盘等块设备就明显不同,我们可以读取硬盘上的任意块的内容,它们的位置不需要连续,而且没有时间上的绝对顺序要求,可以认为硬盘时被随机访问,因此它是一个块设备。
块设备I/O流程
1.块设备I/O的起点是虚拟文件系统(VFS),用户会通过一个系统调用发起一个块I/O,比如read()
调用,并将文件描述符和文件内的偏移量传递给系统调用。虚拟文件系统位于块设备处理体系的最上层,它提供了一个通用的文件模型和文件系统模型,Linux支持的所有文件系统均采用了该模型,可以理解为虚拟文件系统是磁盘文件系统的一层封装,屏蔽了不同文件系统的区别,提供了统一的函数调用来供用户使用。
2.VFS会确认请求的数据是否已经被缓存到内存,内核会将大多数最近从块设备读出或写入的数据保存到内存中,这里涉及到的是磁盘高速缓存机制。
3.映射层(Mapping Layer)包含了磁盘文件系统,如果请求的数据没有被缓存,那么就需要映射层来帮助确定数据的物理位置。映射层主要执行下面两个步骤:
(1)内核确定该文件所在文件系统的块大小,并计算出所请求数据的长度。本质上,文件被拆分成许多块,因此内核会确定请求数据所在的块号。
(2)接下来,映射层调用一个具体文件系统的函数,它访问文件的磁盘节点,然后根据块号确定数据在磁盘上的位置。
4.这时候内核可以开始对块设备发出请求了。内核会利用通用块层(Generic Block Layer)启动I/O操作来传送所请求的数据。一般而言,每个I/O操作只针对磁盘上一组连续的块。由于请求的数据不一定位于相邻的块中,所以通用块层可能会启动多次I/O操作。每个I/O操作是由一个“块I/O”(简称“bio”)结构描述,它收集底层组件需要的所有信息以发出一次I/O请求。
5.通用块层下面的I/O调度程序层会根据特定的调度算法对通用块层发起的I/O请求进行归类和整理,以充分利用磁盘的物理特性来提高I/O效率。简单来讲,I/O调度程序层会把物理介质上相邻的请求聚集在一起,方便磁盘一次性读出。
6.最后,块设备驱动程序向磁盘控制器的硬件接口发送指令,从而实际进行数据传送。当数据传送完成,磁盘控制器就会发出一个中断来通知块设备驱动程序。大多数情况下,磁盘控制器会采用直接内存访问即DMA的方式进行数据传送,简单来讲,DMA就是内存与硬件设备不通过CPU流转数据的直接数据传输。
管理磁盘数据
扇区
磁盘的每次数据传输都基于一组成为扇区的相邻字节,扇区是数据传输的基本单元,不允许传输少于一个扇区的数据,但是可以同时传送几个相邻的扇区。在Linux系统中,扇区大小按惯例都设为512字节。
块
扇区是硬件设备传送数据的基本单位,而块是VFS和文件系统传送数据的基本单位。一个块可以对应磁盘上一个或多个相邻的扇区。在Linux系统中,块的大小必须是2的幂,而且不能超过一个内存页框。此外,它必须是一个扇区大小的整数倍。块的大小不是唯一的,创建一个磁盘文件系统时,用户可以选择合适的块大小。
每个块都会有自己的缓冲区,当内核从磁盘读出一个块时,就会用读出的块数据填充它对应的缓冲区,同样,在内核向磁盘写入一个块时,也会用写入的数据填充或更新缓冲区。
段
段是块设备驱动程序能够处理的数据存储单元,一个段就是一个内存页或者内存页中的一部分。