导语 | 本文主要以一张图为基础,向大家介绍Linux在I/O上做了哪些事情,即Linux中直接I/O原理,希望本文的经验和思路能为读者提供一些帮助和思考。
引言
我们先看一张图:
这张图大体上描述了Linux系统上,应用程序对磁盘上的文件进行读写时,从上到下经历了哪些事情。这篇文章就以这张图为基础,介绍Linux在I/O上做了哪些事情。
一、文件系统
文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。比如常见的Ext4、XFS、ZFS以及网络文件系统NFS等等。
但是不同类型的文件系统标准和接口可能各有差异,我们在做应用开发的时候却很少关心系统调用以下的具体实现,大部分时候都是直接系统调用open, read, write, close来实现应用程序的功能,不会再去关注我们具体用了什么文件系统(UFS、XFS、Ext4、ZFS),磁盘是什么接口(IDE、SCSI,SAS,SATA等),磁盘是什么存储介质(HDD、SSD)应用开发者之所以这么爽,各种复杂细节都不用管直接调接口,是因为内核为我们做了大量的有技术含量的脏活累活。
开始的那张图看到Linux在各种不同的文件系统之上,虚拟了一个VFS,目的就是统一各种不同文件系统的标准和接口,让开发者可以使用相同的系统调用来使用不同的文件系统。
在Linux中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。
用 ls -l 命令看最前面的字符可以看到这个文件是什么类型
brw-r--r-- 1 root root 1, 2 4月 25 11:03 bnod // 块设备文件crw-r--r-- 1 root root 1, 2 4月 25 11:04 cnod // 符号设备文件drwxr-xr-x 2 wrn3552 wrn3552 6 4月 25 11:01 dir // 目录-rw-r--r-- 1 wrn3552 wrn3552 0 4月 25 11:01 file // 普通文件prw-r--r-- 1 root root 0 4月 25 11:04 pipeline // 有名管道srwxr-xr-x 1 root root 0 4月 25 11:06 socket.sock // socket文件lrwxrwxrwx 1 root root 4 4月 25 11:04 softlink -> file // 软连接-rw-r--r-- 2 wrn3552 wrn3552 0 4月 25 11:07 hardlink // 硬链接(本质也是普通文件)
Linux文件系统设计了两个数据结构来管理这些不同种类的文件:
inode是用来记录文件的metadata,所谓metadata在Wikipedia上的描述是data of data,其实指的就是文件的各种属性,比如inode编号、文件大小、访问权限、修改日期、数据的位置等。
wrn3552@novadev:~/playground$ stat file 文件:file 大小:0 块:0 IO 块:4096 普通空文件设备:fe21h/65057d Inode:32828 硬链接:2权限:(0644/-rw-r--r--) Uid:( 3041/ wrn3552) Gid:( 3041/ wrn3552)最近访问:2021-04-25 11:07:59.603745534 +0800最近更改:2021-04-25 11:07:59.603745534 +0800最近改动:2021-04-25 11:08:04.739848692 +0800创建时间:-
inode和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以,inode同样占用磁盘空间,只不过相对于文件来说它大小固定且大小不算大。
dentry用来记录文件的名字、inode指针以及与其他dentry的关联关系。
wrn3552@novadev:~/playground$ tree.├── dir│ └── file_in_dir├── file└── hardlink
不同于inode,dentry是由内核维护的一个内存数据结构,所以通常也被叫做dentry cache。
这里有张图解释了文件是如何存储在磁盘上的:
首先,磁盘再进行文件系统格式化的时候,会分出来3个区:Superblock、inode blocks、data blocks。(其实还有boot block,可能会包含一些bootstrap代码,在机器启动的时候被读到,这里忽略)
其中inode blocks放的都是每个文件的inode,data blocks里放的是每个文件的内容数据。
这里关注一下Superblock,它包含了整个文件系统的metadata,具体有:
Superblock对于文件系统来说非常重要,如果Superblock损坏了,文件系统就挂载不了了,相应的文件也没办法读写。
既然Superblock这么重要,那肯定不能只有一份,坏了就没了,它在系统中是有很多副本的,在Superblock损坏的时候,可以使用fsck(File System Check and repair)来恢复。
回到上面的那张图,可以很清晰地看到文件的各种属性和文件的数据是如何存储在磁盘上的:
这里解释一下什么是logical block:
二、ZFS
这里简单介绍一个广泛应用的文件系统ZFS,一些数据库应用也会用到 ZFS。
先看一张ZFS的层级结构图:
这是一张从底向上的图:
root@:~ # zpool create tank raidz /dev/ada1 /dev/ada2 /dev/ada3 raidz /dev/ada4 /dev/ada5 /dev/ada6root@:~ # zpool list tankNAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOTtank 11G 824K 11.0G - - 0% 0% 1.00x ONLINE -root@:~ # zpool status tank pool: tank state: ONLINE scan: none requestedconfig:
NAME STATE READ WRITE CKSUM tank ONLINE 0 0 0 raidz1-0 ONLINE 0 0 0 ada1 ONLINE 0 0 0 ada2 ONLINE 0 0 0 ada3 ONLINE 0 0 0 raidz1-1 ONLINE 0 0 0 ada4 ONLINE 0 0 0 ada5 ONLINE 0 0 0 ada6 ONLINE 0 0 0
除了raidz还支持其他方案:
root@:~ # zfs create -o mountpoint=/mnt/srev tank/srevroot@:~ # df -h tank/srevFilesystem Size Used Avail Capacity Mounted ontank/srev 7.1G 117K 7.1G 0% /mnt/srev
root@:~ # zfs set quota=1G tank/srevroot@:~ # df -h tank/srevFilesystem Size Used Avail Capacity Mounted ontank/srev 1.0G 118K 1.0G 0% /mnt/srev
上面的层级图和操作步骤可以看到ZFS是基于zpool创建的,zpool可以动态扩容意味着存储空间也可以动态扩容。而且可以创建多个文件系统,文件系统共享完整的zpool空间无需预分配。
当block B有修改变成B1的时候,普通的文件系统会直接在block B原地进行修改变成B1。
ZFS则会再另一个地方写B1,然后再在后面安全的时候对原来的B进行回收。这样结果就不会出现B被打开而写失败的情况,大不了就是B1没写成功。
这个特性让ZFS在断电后不需要执行fsck来检查磁盘中是否存在写操作失败需要恢复的情况,大大提升了应用的可用性。
ZFS中的ARC(Adjustable Replacement Cache) 读缓存淘汰算法,是基于IBM的ARP(Adaptive Replacement Cache) 演化而来。
在一些文件系统中实现的标准LRU算法其实是有缺陷的:比如复制大文件之类的线性大量I/O操作,导致缓存失效率猛增(大量文件只读一次,放到内存不会被再读,坐等淘汰)
另外,缓存可以根据时间来进行优化(LRU,最近最多使用),也可以根据频率进行优化(LFU,最近最常使用),这两种方法各有优劣,但是没办法适应所有场景。
ARC的设计就是尝试在LRU和LFU之间找到一个平衡,根据当前的I/O workload来调整用LRU多一点还是LFU多一点。
ARC定义了4个链表:
ARC工作流程大致如下:
三、磁盘类型
磁盘根据不同的分类方式,有各种不一样的类型。
根据磁盘的存储介质可以分两类(大家都很熟悉):HDD(机械硬盘)和SSD(固态硬盘)
根据磁盘接口分类:
不同的接口,往往分配不同的设备名称。比如,IDE设备会分配一个hd前缀的设备名,SCSI和SATA设备会分配一个sd前缀的设备名。如果是多块同类型的磁盘,就会按照a、b、c等的字母顺序来编号。
其实在Linux中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
g18-"299" on ~# ls -l /dev/sda*brw-rw---- 1 root disk 8, 0 Apr 25 15:53 /dev/sdabrw-rw---- 1 root disk 8, 1 Apr 25 15:53 /dev/sda1brw-rw---- 1 root disk 8, 10 Apr 25 15:53 /dev/sda10brw-rw---- 1 root disk 8, 2 Apr 25 15:53 /dev/sda2brw-rw---- 1 root disk 8, 5 Apr 25 15:53 /dev/sda5brw-rw---- 1 root disk 8, 6 Apr 25 15:53 /dev/sda6brw-rw---- 1 root disk 8, 7 Apr 25 15:53 /dev/sda7brw-rw---- 1 root disk 8, 8 Apr 25 15:53 /dev/sda8brw-rw---- 1 root disk 8, 9 Apr 25 15:53 /dev/sda9
四、Generic Block Layer
可以看到中间的Block Layer其实就是Generic Block Layer。
在图中可以看到Block Layer的I/O调度分为两类,分别表示单队列和多队列的调度:
老版本的内核里只支持单队列的I/O scheduler,在3.16版本的内核开始支持多队列blkmq。
这里介绍几种经典的I/O调度策略:
具体各种I/O调度策略可以参考IOSchedulers
(https://wiki.ubuntu.com/Kernel/Reference/IOSchedulers)
关于blkmq可以参考Linux Multi-Queue Block IO Queueing Mechanism (blk-mq) Details_Details)
多队列调度可以参考Block layer introduction part 2:the request layer
五、性能指标
一般来说I/O性能指标有这5个:
上面的指标除了饱和度外,其他都可以在监控系统中看到。Linux也提供了一些命令来输出不同维度的I/O状态:
作者简介
王睿
腾讯云游戏解决方案架构师
腾讯云游戏解决方案架构师,毕业于中山大学。目前负责腾讯云游戏行业解决方案设计等工作,有丰富的游戏运维及开发经验。
推荐阅读
基于Protobuf共享字段的分包和透传零拷贝技术,你了解吗?