先讲一个作者大概 7 年前我在某过后很火的一个利用散发守业公司的面试小插曲,该公司安顿了一个刚工作 1 年多的一个同学来面我,聊到咱们我的项目中的配置文件里写的一个开关,这位同学就跳出来说,你这个读文件啦,每个用户申请来了还得多一次的磁盘 IO,性能必定差。借由这个故事其实我发现了一个问题,尽管咱们中的大部分人都是计算机科班出身,代码也写的很遛。然而在一些看似司空见惯的问题上,咱们中的绝大多数人并没有真正了解,或者了解的不够透彻。
不论你用的是啥语言,C/PHP/GO、还是 Java,置信大家都有过读取文件的经验。咱们来思考两个问题,如果咱们读取文件中的一个字节:
- 是否会产生磁盘 IO?
- 产生的话,Linux 理论向磁盘读取多少字节了呢?
为了便于了解问题,咱们把 c 的代码也列出来:
int main()
{
char c;
int in;
in = open("in.txt", O_RDONLY);
read(in,&c,1);
return 0;
}
如果不是从事 c /c++ 开发工作的同学,这个问题想深度了解起来的确不那么容易。因为目前罕用的支流语言,PHP/Java/Go 啥的封装档次都比拟高,把内核的很多细节都给屏蔽的比拟彻底。要想把下面的两个问题搞的比较清楚,须要剖开 Linux 的外部来了解 Linux 的 IO 栈。
Linux IO 栈简介
废话不多说,咱们间接把 Linux IO 栈的一个简化版本画进去:(官网的 IO 栈参考这个 Linux.IO.stack_v1.0.pdf)
咱们在后面也分享了几篇文章探讨了上图图中的硬件层,还有文件系统模块。但通过这个 IO 栈咱们发现,咱们对 Linux 文件的 IO 的了解还是远远不够,还有好几个内核组件:IO 引擎、VFS、PageCache、通用块管理层、IO 调度层等模块咱们并没有理解太多。别着急,让咱们一一道来:
IO 引擎
咱们开发同学想要读写文件的话,在 lib 库层有很多种函数能够抉择,比方 read,write,mmap 等。这事实上就是在抉择 Linux 提供的 IO 引擎。咱们最罕用的 read、write 函数是属于 sync 引擎,除了 sync,还有 map、psync、vsync、libaio、posixaio 等。sync,psync 都属于同步形式,libaio 和 posixaio 属于异步 IO。
当然了 IO 引擎也须要 VFS、通用块层等更底层的反对能力实现。在 sync 引擎的 read 函数里会进入 VFS 提供的 read 零碎调用。
VFS 虚构文件系统
在内核层,第一个看到的是 VFS。VFS 诞生的思维是形象一个通用的文件系统模型,对咱们开发人员或者是用户提供一组通用的接口,让咱们不必 care 具体文件系统的实现。VFS 提供的外围数据结构有四个,它们定义在内核源代码的 include/linux/fs.h 和 include/linux/dcache.h 中。
- superblock:Linux 用来标注具体已装置的文件系统的无关信息
- inode:Linux 中的每一个文件都有一个 inode,你能够把 inode 了解为文件的身份证
- file:内存中的文件对象,用来保留过程和磁盘文件的对应关系
- desty:目录项,是门路中的一部分,所有的目录项对象串起来就是一棵 Linux 下的目录树。
围绕这这四个外围数据结构,VFS 也都定义了一系列的操作方法。比方,inode 的操作方法定义 inode_operations
(include/linux/fs.h),在它的外面定义了咱们十分相熟的mkdir
和rename
等。
struct inode_operations {
......
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
......
在 file 对应的操作方法 file_operations
外面定义了咱们常常应用的 read 和 write:
struct file_operations {
......
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
......
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
Page Cache
在 VFS 层往下看,咱们留神到了 Page Cache。它的中文译名叫页高速缓存,是 Linux 内核应用的次要磁盘高速缓存,是一个纯内存的工作组件,其作用就是来给拜访绝对比较慢的磁盘来进行拜访减速。如果要拜访的文件 block 正好存在于 Page Cache 内,那么并不会有理论的磁盘 IO 产生。如果不存在,那么会申请一个新页,收回缺页中断,而后用磁盘读取到的 block 内容来填充它,下次间接应用。Linux 内核应用搜寻树来高效治理大量的页面。
如果你有非凡的需要想要绕开 Page Cache,只有设置 DIRECT_IO 就能够了。有两种状况须要绕开:
- 测试磁盘 IO 的真实性能
- 节约使用 Page Cache 时零碎调用陷入到内核态,以及内核内存向用户过程内存拷贝到开销。
文件系统
在我在之前的文章《新建一个空文件占用多少磁盘空间?》、《了解格式化原理》里探讨的都是具体的文件系统。文件系统里最重要的两个概念就是 inode 和 block,这两个咱们在之前的文章里也都见过了。一个 block 是多大呢,这是运维在格式化的时候决定的,个别默认是 4KB。
除了 inode 和 block,每个文件系统还会定义本人的实际操作函数。例如在 ext4 中定义的 ext4_file_operations 和 ext4_file_inode_operations 如下:
const struct file_operations ext4_file_operations = {
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.mmap = ext4_file_mmap,
.open = ext4_file_open,
......
};
const struct inode_operations ext4_file_inode_operations = {
.setattr = ext4_setattr,
.getattr = ext4_file_getattr,
......
};
通用块层
通用块层是一个解决零碎中所有块设施 IO 申请的内核模块。它定义了一个叫 bio 的数据结构来示意一次 IO 操作申请(include/linux/bio.h)。
那么一次 bio 里对应的 IO 大小单位是页面,还是扇区呢?都不是,是段!每个 bio 可能会蕴含多个段。一个段是一个残缺的页面,或者是页面的一部分,具体请参考 https://www.ilinuxkernel.com/files/Linux.Generic.Block.Layer.pdf。
为什么要搞出个段这么让人费解的货色呢?这是因为在磁盘中间断存储的数据,到了内存 Page Cache 里的时候可能内存并不间断了。这种情况呈现是失常的,不能说磁盘中间断的数据我在内存中就非得用间断的空间来缓存。段就是为了能让一次内存 IO DMA 到多“段”地址并不间断的内存中的。
一个常见的扇区 / 段 / 页的大小对比方下图:
IO 调度层
当通用块层把 IO 申请理论收回当前,并不一定会立刻被执行。因为调度层会从全局登程,尽量让整体磁盘 IO 性能最大化。大抵的工作形式是让磁头相似电梯那样工作,先往一个方向走,到头再回来,这样磁盘效率会比拟高一些。具体的算法有 noop,deadline 和 cfg 等。
在你的机器上,通过 dmesg | grep -i scheduler
来查看你的 Linux 反对的算法,并在测试的时候能够抉择其中的一种。
读文件过程
咱们曾经把 Linux IO 栈里的各个内核组件都介绍一边了。当初咱们再从头整体过一下读取文件的过程
- lib 里的 read 函数首先进入零碎调用 sys_read
- 在 sys_read 再进入 VFS 里的 vfs_read、generic_file_read 等函数
- 在 vfs 里的 generic_file_read 会判断是否缓存命中,命中则返回
- 若不命中内核在 Page Cache 里调配一个新页框,收回缺页中断,
- 内核向通用块层发动块 I / O 申请,块设施屏蔽了磁盘、U 盘的差别
- 通用块层把用 bio 代表的 I / O 申请放到 IO 申请队列中
- IO 调度层通过电梯算法来调度队列中的申请
- 驱动程序向磁盘控制器收回读取命令管制,DMA 形式间接填充到 Page Cache 中的新页框
- 控制器收回中断告诉
- 内核将用户须要的 1 个字节填充到用户内存中
- 而后你的过程被唤醒
能够看到,如果 Page Cache 命中的话,基本就没有磁盘 IO 产生。所以,大家不要感觉代码里呈现几个读写文件的逻辑就感觉性能会慢的不行。操作系统曾经替你优化了很多很多,内存级别的拜访提早大概是 ns 级别的,比机械磁盘 IO 快了 2 - 3 个数量级。如果你的内存足够大,或者你的文件被拜访的足够频繁,其实这时候的 read 操作极少有真正的磁盘 IO 产生。
咱们再看第二种状况,如果 Page Cache 不命中的话,Linux 理论进行了多少个字节的磁盘 IO。整个 IO 过程中波及到了好几个内核组件。而每个组件之间都是采纳不同长度的块来治理磁盘数据的。
- Page Cache 是以页为单位的,Linux 页大小个别是 4KB(防止有大神挑刺,这里说下 Linux 能设置大内存页)
- 文件系统是以块为单位来治理的。应用
dumpe2fs
能够查看,个别一个块默认是 4KB - 通用块层是以段为单位来解决磁盘 IO 申请的,一个段为一个页或者是页的一部分
- IO 调度程序通过 DMA 形式传输 N 个扇区到内存,扇区个别为 512 字节
- 硬盘也是采纳“扇区”的治理和传输数据的
能够看到,尽管咱们从用户角度的确是只读了 1 个字节(开篇的代码中咱们只给这次磁盘 IO 留了一个字节的缓存区)。然而在整个内核工作流中,最小的工作单位是磁盘的扇区,为 512 字节,比 1 个字节要大的多。另外 block、page cache 等高层组件工作单位更大,所以理论一次磁盘读取是很多字节一起进行的。假如段就是一个内存页的话,一次磁盘 IO 就是 4KB(8 个 512 字节的扇区)一起进行读取。
Linux 内核中咱们没有讲到的是还有一套简单的预读取的策略。所以,在实践中,可能比 8 更多的扇区来一起被传输到内存中。
最初
操作系统的本意是做到让你简略可依赖,让你尽量把它当成一个黑盒。你想要一个字节,它就给你一个字节,然而本人默默干了许许多多的活儿。咱们尽管国内绝大多数开发都不是搞底层的,但如果你非常关注你的应用程序的性能,你应该明确操作系统的什么时候轻轻进步了你的性能,是怎么来进步的。以便在未来某一个时候你的线上服务器扛不住快要挂掉的时候,你能迅速找出问题所在。
咱们再扩大一下,如果 Page Cache 没有命中,那么肯定会有传动到机械轴上的磁盘 IO 吗?
其实也不肯定,为什么,因为当初的磁盘自身就会带一块缓存。另外当初的服务器都会组建磁盘阵列,在磁盘阵列里的外围硬件 Raid 卡里也会集成 RAM 作为缓存。只有所有的缓存都不命中的时候,机械轴带着磁头才会真正工作。
开发内功修炼之硬盘篇专辑:
- 1. 磁盘开篇:扒开机械硬盘坚挺的外衣!
- 2. 磁盘分区也是隐含了技术技巧的
- 3. 咱们怎么解决机械硬盘既慢又容易坏的问题?
- 4. 拆解固态硬盘构造
- 5. 新建一个空文件占用多少磁盘空间?
- 6. 只有 1 个字节的文件理论占用多少磁盘空间
- 7. 文件过多时 ls 命令为什么会卡住?
- 8. 了解格式化原理
- 9.read 文件一个字节理论会产生多大的磁盘 IO?
- 10.write 文件一个字节后何时发动写磁盘 IO?
- 11. 机械硬盘随机 IO 慢的超乎你的设想
- 12. 搭载固态硬盘的服务器到底比搭机械硬盘快多少?
我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~