Linux块设备I/O栈浅析

时间:2024-04-04 08:15:21

Linux存储系统包括两个部分:第一部分是给在用户的角度提供读/写接口,数据以流为表现形式;第二部分是站在存储设备的角度提供读/写接口,数据以块为表现形式。文件系统位于两者中间起到承上启下的作用。

以块为表现形式,既块存储,简单来说就是使用块设备来为系统提供存储服务。本问重点在于块设备的IO栈。



0x01 块设备基本概念

块设备将信息存储在固定大小的块中,每个块都有自己的地址。对操作系统来说,块设备是以字符设备的外观展现的,虽然对这种字符设备可以按照字节为单位进行访问,但是实际上到块设备上却是以块为单位(最小512byte,既一个扇区)。这之间的转换是由操作系统来完成的。

下面介绍块设备的基本概念:

  1. 扇区:磁盘盘片上的扇形区域,逻辑化数据,方便管理磁盘空间,是硬件设备数据传输的基本单位,一般为512byte。
  2. 块:块是VFS(虚拟文件系统)和文件系统数据传输的基本单位,必须是扇区的整数倍,格式化文件系统时,可以指定块大小。

0x02 块设备I/O栈

1 基本概念

本节介绍几个块设备I/O栈的基本概念。

  1. bio:bio是通用块层I/O请求的数据结构,表示上层提交的I/O求情。一个bio包含多个page(既page cache 内核缓冲页 在内存上),这些page对应磁盘上的一段连续的空间。由于文件在磁盘上并不连续存放,文件I/O提交到块这杯之前,极有可能被拆分成多个bio结构。
  2. request:表示块设备驱动层I/O请求,经由I/O调度层转换后(既电梯算法合并)的I/O请求,将会被发送到块设备驱动层进行处理。
  3. request_queue: 维护块设备驱动层I/O请求的队列,所有的request都插入到该队列,每个磁盘设备都只有一个queue(多个分区也只有一个)。

这三个结构的关系如下图所示,一个request_queue中包含多个request,每个request可能包含多个bio,请求的合并就是根据各种算法(1.noop 2.deadline 3.CFQ)将多个bio加入到同一个request中。

Linux块设备I/O栈浅析


2. 请求处理流程

Linux块设备I/O栈浅析

先说一个Direct I/O和Buffer I/O的区别:

  1. Direct I/O绕过page cache,Buffer I/O是写到page cache中表示写请求完成,然后由文件系统的刷脏页机制把数据刷到硬盘。因此,使用Buffer I/O,掉电时有可能page cache中的脏数据还未刷到磁盘上,导致数据丢失。
  2. Buffer I/O机制中,DMA方式可以将数据直接从磁盘读到page cache中(与直接读不同的是,不需要CPU参与),或者从page cache中将数据写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传输过程中需要在应用程序地址空间和page cache之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存的开销是非常大的(磁盘到page cache使用DMA CPU不参与,page cache到应用地址空间的数据复制需要CPU的参与)。而Direct I/O的优点就是通过减少操作系统内核缓冲区和应用程序地址空间的数据拷贝次数,降低了对文件读取和写入时所带来的的CPU的使用和内存的占用,但是Direct I/O的读操作不能从page cache中获取数据,而是直接从磁盘上读取,带来性能上的损失。为了解决这个问题,Direct I/O会与异步I/O结合起来使用。
  3. Direct I/O一般用于需要自己管理缓存的应用,如数据库系统。

下面说I/O的读写流程,简单描述如下:

  1. 用户调用系统调用write写一个文件,会调到sys_write函数
  2. 经过VFS虚拟文件系统层,调用vfs_write, 如果是缓存写方式,则写入page cache,然后就返回,后续就是刷脏页的流程;如果是Direct I/O的方式,就会走到do_blockdev_direct_IO的流程
  3. 如果操作的设备是逻辑设备如LVM(logical volume manager),MDRAID设备等,会进入到对应内核模块的处理函数里进行一些处理,否则就直接构造bio请求,调用submit_bio往具体的块设备下发请求,submit_bio函数通过generic_make_request转发bio,generic_make_request是一个循环,其通过每个块设备**册的make_request_fn函数与块设备进行交互
  4. 请求下发到底层的块设备上(应该是属于I/O调度层),调用块设备请求处理函数__make_request进行处理,在这个函数中就会调用blk_queue_bio,这个函数就是合并bio到request中,也就是I/O调度器的具体实现:如果几个bio要读写的区域是连续的,就合并到一个request;否则就创建一个新的request,把自己挂到这个request下。合并bio请求也是有限度的,如果合并后的请求超过阈值(在/sys/block/xxx/queue/max_sectors_kb里设置),就不能再合并成一个request了,而会新分配一个request
  5. 接下来的I/O操作就与具体的物理设备有关了,交由相应的块设备驱动程序进行处理,这里以scsi设备为例说明,queue队列的处理函数request_fn对应的scsi驱动的就是scsi_request_fn函数,将请求构造成scsi指令下发到scsi设备进行处理,处理完成后就会依次调用各层的回调函数进行完成状态的一些处理,最后返回给上层用户

3. bcache

bcache是Linux内核的块层缓存,它使用固态硬盘作为硬盘驱动器的缓存,既解决了各台硬盘容量太小的问题,有解决了硬盘驱动器运行速度太慢的问题。

bcache从3.10版本开始被集成进内核,支持三种缓存策略,分别是写回(writeback)、写透(writethrough)、writearound, 默认使用writethrough,缓存策略可被动态修改。、


0x03 参考

  1. 《Linux开源存储全栈详解》