文章目录架构图前言名词解释IO体系VFS层superblockinodedentryfilePageCache层脏页刷盘预读策略映射层通用块层IO调度层块设备驱动层物理设备层FAQ
文章目录
- 架构图
- 前言
- 名词解释
- IO体系
- VFS层
- superblock
- inode
- dentry
- file
- PageCache层
- 映射层
- 通用块层
- IO调度层
- 块设备驱动层
- 物理设备层
- FAQ
架构图
前言
Linux I/O体系是Linux内核的重要组成部分,主要包含网络IO、磁盘IO等。基本所有的技术栈都需要与IO打交道,分布式存储系统更是如此。本文主要简单分析一下磁盘IO,看看一个IO请求从发起到完成到底经历了哪些流程。
名词解释
Buffered I/O:缓存IO又叫标准IO,是大多数文件系统的默认IO操作,经过PageCache。
Direct I/O:直接IO,By Pass PageCache。offset、length需对齐到block_size。
Sync I/O:同步IO,即发起IO请求后会阻塞直到完成。缓存IO和直接IO都属于同步IO。
Async I/O:异步IO,即发起IO请求后不阻塞,内核完成后回调。通常用内核提供的Libaio。
Write Back:Buffered IO时,仅仅写入PageCache便返回,不等数据落盘。
Write Through:Buffered IO时,不仅仅写入PageCache,而且同步等待数据落盘。
IO体系
我们先看一张总的Linux内核存储栈图片:
Linux IO存储栈主要有以下7层:
VFS层
我们通常使用open、read、write等函数来编写Linux下的IO程序。接下来我们看看这些函数的IO栈是怎样的。在此之前我们先简单分析一下VFS层的4个对象,有助于我们深刻的理解IO栈。
VFS层的作用是屏蔽了底层不同的文件系统的差异性,为用户程序提供一个统一的、抽象的、虚拟的文件系统,提供统一的对外API,使用户程序调用时无需感知底层的文件系统,只有在真正执行读写操作的时候才调用之前注册的文件系统的相应函数。
VFS支持的文件系统主要有三种类型:
基于磁盘的文件系统:Ext系列、XFS等。
网络文件系统:NFS、CIFS等。
特殊文件系统:/proc、裸设备等。
VFS主要有四个对象类型(不同的文件系统都要实现):
superblock:整个文件系统的元信息。对应的操作结构体:struct super_operations。
inode:单个文件的元信息。对应的操作结构体:struct inode_operations。
dentry:目录项,一个文件目录对应一个dentry。对应的操作结构体:struct dentry_operations。
file:进程打开的一个文件。对应的操作结构体:struct file_operations
superblock
superblock结构体定义了整个文件系统的元信息,以及相应的操作。
inode
inode结构体定义了文件的元数据,比如大小、最后修改时间、权限等,除此之外还有一系列的函数指针,指向具体文件系统对文件操作的函数,包括常见的open、read、write等,由i_fop函数指针提供。
dentry
dentry是目录项,由于每一个文件必定存在于某个目录内,我们通过路径查找一个文件时,最终肯定找到某个目录项。在Linux中,目录和普通文件一样,都是存放在磁盘的数据块中,在查找目录的时候就读出该目录所在的数据块,然后去寻找其中的某个目录项
在我们使用Linux的过程中,根据目录查找文件的例子无处不在,而目录项的数据又都是存储在磁盘上的,如果每一级路径都要读取磁盘,那么性能会十分低下。所以需要目录项缓存,把dentry放在缓存中加速。
VFS把所有的dentry放在dentry_hashtable哈希表里面,使用LRU淘汰算法。
file
用户程序能接触的VFS对象只有file,由进程管理。我们常用的打开一个文件就是创建一个file对象,并返回一个文件描述符。出于隔离性的考虑,内核不会把file的地址返回,而是返回一个整形的fd。
file对象是由内核进程直接管理的。每个进程都有当前打开的文件列表,放在files_struct结构体中。
PageCache层
在HDD时代,由于内核和磁盘速度的巨大差异,Linux内核引入了页高速缓存(PageCache),把磁盘抽象成一个个固定大小的连续Page,通常为4K。对于VFS来说,只需要与PageCache交互,无需关注磁盘的空间分配以及是如何读写的。
当我们使用Buffered IO的时候便会用到PageCache层,与Direct IO相比,用户程序无需offset、length对齐。是因为通用块层处理IO都必须是块大小对齐的。
Buffered IO中PageCache帮我们做了对齐的工作:如果我们修改文件的offset、length不是页大小对齐的,那么PageCache会执行RMW的操作,先把该页对应的磁盘的数据全部读上来,再和内存中的数据做Modify,最后再把修改后的数据写回磁盘。虽然是写操作,但是非对齐的写仍然会有读操作。
Direct IO由于跳过了PageCache,直达通用块层,所以需要用户程序处理对齐的问题。
脏页刷盘
如果发生机器宕机,位于PageCache中的数据就会丢失;所以仅仅写入PageCache是不可靠的,需要有一定的策略将数据刷入磁盘。通常有几种策略:
手动调用fsync、fdatasync刷盘,可参考浅谈分布式存储之sync详解。
脏页占用比例超过了阈值,触发刷盘。
脏页驻留时间过长,触发刷盘。
Linux内核目前的做法是为每个磁盘都建立一个线程,负责每个磁盘的刷盘。
预读策略
从VFS层我们知道写是异步的,写完PageCache便直接返回了;但是读是同步的,如果PageCache没有命中,需要从磁盘读取,很影响性能。如果是顺序读的话PageCache便可以进行预读策略,异步读取该Page之后的Page,等到用户程序再次发起读请求,数据已经在PageCache里,大幅度减少IO的次数,不用阻塞读系统调用,提升读的性能。
映射层
映射层是在PageCache之下的一层,由多个文件系统(Ext系列、XFS等,打开文件系统的文件)以及块设备文件(直接打开裸设备文件)组成,主要完成两个工作:
内核确定该文件所在文件系统或者块设备的块大小,并根据文件大小计算所请求数据的长度以及所在的逻辑块号。
根据逻辑块号确定所请求数据的物理块号,也即在在磁盘上的真正位置。
由于通用块层以及之后的的IO都必须是块大小对齐的,我们通过DIO打开文件时,略过了PageCache,所以必须要自己将IO数据的offset、length对齐到块大小。
我们使用的DIO+Libaio直接打开裸设备时,跳过了文件系统,少了文件系统的种种开销,然后进入通用块层,继续之后的处理。
通用块层
通用块层存在的意义也和VFS一样,屏蔽底层不同设备驱动的差异性,提供统一的、抽象的通用块层API。
IO调度层
Linux调度层是Linux IO体系中的一个重要组件,介于通用块层和块设备驱动层之间。IO调度层主要是为了减少磁盘IO的次数,增大磁盘整体的吞吐量,会队列中的多个bio进行排序和合并,并且提供了多种IO调度算法,适应不同的场景。
Linux内核目前提供了以下几种调度策略:
Deadline:默认的调度策略,加入了超时的队列。适用于HDD。
CFQ:完全公平调度器。
Noop:No Operation,最简单的FIFIO队列,不排序会合并。适用于SSD、NVME。
块设备驱动层
每一类设备都有其驱动程序,负责设备的读写。IO调度层的请求也会交给相应的设备驱动程序去进行读写。大部分的磁盘驱动程序都采用DMA的方式去进行数据传输,DMA控制器自行在内存和IO设备间进行数据传送,当数据传送完成再通过中断通知CPU。
通常块设备的驱动程序都已经集成在了kernel里面,也即就算我们直接调用块设备驱动驱动层的代码还是要经过内核。
spdk实现了用户态、异步、无锁、轮询方式NVME驱动程序。块存储是延迟非常敏感的服务,使用NVME做后端存储磁盘时,便可以使用spdk提供的NVME驱动,缩短IO流程,降低IO延迟,提升IO性能。
物理设备层
物理设备层便是我们经常使用的HDD、SSD、NVME等磁盘设备了
FAQ
1、write返回成功数据落盘了吗?
Buffered IO:write返回数据仅仅是写入了PageCache,还没有落盘。
Direct IO:write返回数据仅仅是到了通用块层放入IO队列,依旧没有落盘。
此时设备断电、宕机仍然会发生数据丢失。需要调用fsync或者fdatasync把数据刷到磁盘上,调用命令时,磁盘本身缓存(DiskCache)的内容也会持久化到磁盘上。
2、write系统调用是原子的吗?
write系统调用不是原子的,如果有多线程同时调用,数据可能会发生错乱。可以使用O_APPEND标志打开文件,只能追加写,这样多线程写入就不会发生数据错乱。
3、mmap相比read、write快在了哪里?
mmap直接把PageCache映射到用户态,少了一次系统调用,也少了一次数据在用户态和内核态的拷贝。
mmap通常和read搭配使用:写入使用write+sync,读取使用mmap。
4、为什么Direct IO需要数据对齐?
DIO跳过了PageCache,直接到通用块层,而通用块层的IO都必须是块大小对齐的,所以需要用户程序自行对齐offset、length。
5、Libaio的IO栈?
write()—>sys_write()—>vfs_write()—>通用块层—>IO调度层—>块设备驱动层—>块设备
6、为什么需要 by pass pagecache?
当应用程序不满Linux内核的Cache策略,有更适合自己的Cache策略时可以使用Direct IO跳过PageCache。例如Mysql。
7、为什么需要 by pass kernel?
当应用程序对延迟极度敏感时,由于Linux内核IO栈有7层,IO路径比较长,为了缩短IO路径,降低IO延迟,可以by pass kernel,直接使用用户态的块设备驱动程序。例如spdk的nvme,阿里云的ESSD。
8、为什么需要直接操作裸设备?
当应用程序仅仅使用了基本的read、write,用不到文件系统的大而全的功能,此时文件系统的开销对于应用程序来说是一种累赘,此时需要跳过文件系统,接管裸设备,自己实现磁盘分配、缓存等功能,通常使用DIO+Libaio+裸设备。例如Ceph FileStore的Journal、Ceph BlueStore。