简述
Linux的IO路径可能是Linux系统中最纷繁复杂的模块了,而它又是如此的重要,直接决定了系统的性能。 接下来我们来看一张熟悉的老图:
由图可见,从系统调用的接口再往下,Linux下的IO栈致大致有几个层次:
应用程序:
这没什么好说的,通过相关系统调用(如open/read/write)发起IO请求,属于IO请求的源头;
文件系统:
应用程序的请求直接到达文件系统层。文件系统又分为VFS和具体文件系统(ext3、ext4等),VFS对应用层提供统一的访问接口,而ext3等文件系统则具体实现了这些接口。另外,为了提供IO性能,在该层还实现了诸如page cache等功能。同时,用户也可以选择绕过page cache,而是直接使用direct模式进行IO(如数据库)。
块设备层:
文件系统将IO请求打包提交给块设备层,该层会对这些IO请求作合并、排序、调度等,然后以新的格式发往更底层。在该层次上实现了多种电梯调度算法,如cfq、deadline等。
SCSI层:
块设备层将请求发往SCSI层,SCSI就开始真实处理这些IO请求,但是SCSI层又对其内部按照功能划分了不同层次: * SCSI高层:高层驱动负责管理disk,接收块设备层发出的IO请求,打包成SCSI层可识别的命令格式,继续往下发; * SCSI中层:中层负责通用功能,如错误处理,超时重试等; * SCSI低层:底层负责识别物理设备,将其抽象提供给高层,同时接收高层派发的scsi命令,交给物理设备处理。
各层接口
清晰的接口能让复杂的系统变得容易理解和维护。
应用程序 => 文件系统
做开发的人可能都应该了解,通过诸如open/read/pread/write/writev等POSIX接口来调用文件系统各种功能。由于其普遍性,在这里就不一一描述接口形式了。
文件系统 => 块设备层
这里我们将文件系统当成一个整体,并不区分VFS和具体文件系统。块设备层对文件系统提供的接口为submit_bio(),接口形式如下:
void submit_bio(int rw, struct bio *bio)
文件系统向块设备提交的每个bio请求都设置了完成回调函数,记录在*bio->*bi_end_io。bio请求完成后,通过该字段通知文件系统。
块设备层 => SCSI上层
scsi_reuqest_fn()和struct request_queue。 老实来说,块设备层和SCSI上层之间分的没有那么清楚,耦合的稍微紧密,块设备层看到的IO请求结构是request。而SCSI层看到的IO命令则是scsi_cmnd。
每个scsi设备(如scsi disk)均维护了一个请求队列request_queue,而每个scsi设备对上层呈现的其实是一个块设备。因此,块设备和scsi设备有着天然的联系,request_queue则是连接块设备层和SCSI层的纽带。块设备层对request请求最终会派发至request_queue中。而在特定条件下通过泄流机制将request_queue中积攒的request派发至SCSI层处理。而泄流的实际处理过程就是scsi_request_fn()函数,因此说它是块设备层和SCSI上层的接口也不为过,虽然不是特别准确。在scsi_reuqest_fn内会进行request至scsi_cmnd的转换。
SCSI上层 => SCSI中间层
SCSI上层在收到块设备层发起的scsi命令后马不停蹄又将其转发至SCSI中间层。SCSI上层至SCSI中间层的接口是 scsi_dispatch_cmd
static void scsi_request_fn(struct request_queue *q) {
......
// 设置scsi命令完成回调函数
cmd->scsi_done = scsi_done;
rtn = scsi_dispatch_cmd(cmd);
......
}
SCSI中间层 => SCSI低层
SCSI中间层收到块设备层发下来的scsi_cmnd命令后,中间层作自己处理后,然后再将该命令继续往下传递,接下来该命令到了scsi底层,而传递的接口是 queuecommand()
static int scsi_dispatch_cmd(struct scsi_cmnd *cmd) {
......
rtn = host->hostt->queuecommand(host, cmd);
......
}
host为该设备所属的主机适配器结构。任何一个SCSI主机适配器都需要实现queuecommand接口。注意这个提交过程是异步的,无需等待该命令完成便直接返回。 scsi 命令完成后,会通过记录在命令内的完成函数回调上层处理,具体是cmd->scsi_done。
linux io栈(读写流程)
linux IO 存储栈分为7层:
VFS 虚拟文件层: 在各个具体的文件系统上建立一个抽象层,屏蔽不同文件系统的差异。 PageCache 层: 为了缓解内核与磁盘速度的巨大差异。 映射层 Mapping Layer: 内核必须从块设备上读取数据,Mapping layer 要确定在物理设备上的位置。 通用块层: 通用块层处理来自系统其他组件发出的块设备请求。包含了块设备操作的一些通用函数和数据结构。 IO 调度层: IO 调度层主要是为了减少磁盘IO 的次数,增大磁盘整体的吞吐量,队列中多个bio 进行排序和合并。 块设备驱动层: 每一类设备都有其驱动程序,负责设备的读写。 物理设备层: 物理设备层有 HDD,SSD,Nvme 等磁盘设备。 PageCache 层: 两种策略: write back : 写入PageCache 便返回,不等数据落盘。 write through: 同步等待数据落盘。
读流程
- (1)系统调用read()会触发相应的VFS(Virtual Filesystem Switch)函数,传递的参数 有文件描述符和文件偏移量。
- (2)VFS确定请求的数据是否已经在内存缓冲区中;若数据不在内存中,确定如何执行读 操作。
- (3)假设内核必须从块设备上读取数据,这样内核就必须确定数据在物理设备上的位置。 这由映射层(Mapping Layer)来完成。
- (4)此时内核通过通用块设备层(Generic Block Layer)在块设备上执行读操作,启动I/O 操作,传输请求的数据。
- (5)在通用块设备层之下是I/O调度层(I/O Scheduler Layer),根据内核的调度策略,对 等待的I/O等待队列排序。
- (6)最后,块设备驱动(Block Device Driver)通过向磁盘控制器发送相应的命令,执行 真正的数据传输。
写流程
write()—>sys_write()—>vfs_write()—>通用块层—>IO调度层—>块设备驱动层—>块设备
块设备
系统中能够随机访问固定大小数据片(chunk)的设备称为块设备,这些数据片就称作 块。最常见的块设备是硬盘,除此之外,还有CD-ROM驱动器和SSD等。它们通常安装文 件系统的方式使用。
有关Android开发中的内核(Linux) 的IO栈学习,这里就做一部分浅析,如需深入了解,或者想进阶Android开发技术。大家可以参考《Android核心技术手册》这本电子书籍,里面分为很多知识板块。30个技术模块+笔记。点击查看免费方式。
总结
- IO 栈:VFS - 文件系统 - 块层 - SCSI 驱动层;
- VFS 负责通用的文件抽象语义,管理并切换文件系统;
- 文件系统负责抽象出“文件的概念”,维护“文件”数据到块层的位置映射,怎么摆放数据,怎么抽象文件都是文件系统说了算;
- 块层对底层硬件设备做一层统一的抽象,最重要的是做一些IO 调度的策略。比如,尽可能收集批量 IO 聚合下发,让 IO 尽可能的顺序,合并 IO 请求减少 IO 次数等等;
- SCSI 层则是负责最后对硬件磁盘的对接,驱动层,本质就是个翻译器;
- 文件的 buffer write 要实现 .write_begin,.write_page 等接口,前者用于分配 page 并绑定块层物理空间,后者用户异步回刷的时候调用(注意,非常规的优化在回刷的时候才去绑定物理空间);
- 文件系统 .write_begin 调用分配物理位置的时候依赖于get_block的实现,物理位置分配好之后,page 会对应到特定的 buffer head 结构,buffer head 结构则对应到具体的块设备位置;
- **direct IO 直接在用户路径上刷数据到磁盘,不走 PageCache 的逻辑,**但并不是所有文件系统都会实现它。