先讲一个作者大约5-6年前我在某当时很火的一个应用分发创业公司的面试小插曲,该公司安排了一个刚工作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栈。

1Linux IO栈简介

废话不多说,我们直接把Linux IO栈的一个简化版本画出来:(官方的IO栈参考这个:http://www.ilinuxkernel.com/files/Linux.IO.stack_v1.0.pdf)

图1 Linux硬盘IO栈

我们在前面也分享了几篇文章讨论了上图图中的硬件层,还有文件系统模块。但通过这个IO栈我们发现,我们对Linux文件的IO的理解还是远远不够,还有好几个内核组件:IO引擎、VFS、PageCache、通用块管理层、IO调度层等模块我们并没有了解太多。别着急,让我们一一道来:

1. 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系统调用。

2. 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),在它的里面定义了我们非常熟悉的mkdirrename等。

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);

3. Page Cache

在VFS层往下看,我们注意到了Page Cache。它的中文译名叫页高速缓存,是Linux内核使用的主要磁盘高速缓存,是一个纯内存的工作组件,其作用就是来给访问相对比较慢的磁盘来进行访问加速。如果要访问的文件block正好存在于Page Cache内,那么并不会有实际的磁盘IO发生。如果不存在,那么会申请一个新页,发出缺页中断,然后用磁盘读取到的block内容来填充它 ,下次直接使用。Linux内核使用搜索树来高效管理大量的页面。

如果你有特殊的需求想要绕开Page Cache,只要设置DIRECT_IO就可以了。有两种情况需要绕开:

  • 测试磁盘IO的真实性能

  • 节约使用Page Cache时系统调用陷入到内核态,以及内核内存向用户进程内存拷贝到开销。

4. 文件系统

在我在之前的文章《新建一个空文件占用多少磁盘空间?》、《理解格式化原理》里讨论的都是具体的文件系统。文件系统里最重要的两个概念就是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,        ......};

5. 通用块层

通用块层是一个处理系统中所有块设备IO请求的内核模块。它定义了一个叫bio的数据结构来表示一次IO操作请求(include/linux/bio.h)。

那么一次bio里对应的IO大小单位是页面,还是扇区呢?都不是,是段!每个bio可能会包含多个段。一个段是一个完整的页面,或者是页面的一部分,具体请参考这个(https://www.ilinuxkernel.com/files/Linux.Generic.Block.Layer.pdf)。

为什么要搞出个段这么让人费解的东西呢?这是因为在磁盘中连续存储的数据,到了内存Page Cache里的时候可能内存并不连续了。这种状况出现是正常的,不能说磁盘中连续的数据我在内存中就非得用连续的空间来缓存。段就是为了能让一次磁盘IO能DMA到多“段”地址并不连续的内存中的。

一个常见的扇区/段/页的大小对比如下图:

图2 Linux的页/段/扇区的关系示例

6. IO调度层

当通用块层把IO请求实际发出以后,并不一定会立即被执行。因为调度层会从全局出发,尽量让整体磁盘IO性能最大化。大致的工作方式是让磁头类似电梯那样工作,先往一个方向走,到头再回来,这样磁盘效率会比较高一些。具体的算法有noop,deadline和cfg等。

在你的机器上,通过dmesg | grep -i scheduler来查看你的Linux支持的算法,并在测试的时候可以选择其中的一种。

2读文件过程

我们已经把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更多的扇区来一起被传输到内存中。

3最后

操作系统的本意是做到让你简单可依赖, 让你尽量把它当成一个黑盒。你想要一个字节,它就给你一个字节,但是自己默默干了许许多多的活儿。我们虽然国内绝大多数开发都不是搞底层的,但如果你十分关注你的应用程序的性能,你应该明白操作系统的什么时候悄悄提高了你的性能,是怎么来提高的。以便在将来某一个时候你的线上服务器扛不住快要挂掉的时候,你能迅速找出问题所在。

我们再扩展一下,假如Page Cache没有命中,那么一定会有传动到机械轴上的磁盘IO吗?

其实也不一定,为什么,因为现在的磁盘本身就会带一块缓存。另外现在的服务器都会组建磁盘阵列,在磁盘阵列里的核心硬件Raid卡里也会集成RAM作为缓存。只有所有的缓存都不命中的时候,机械轴带着磁头才会真正工作。

一招修复内存不能read_read文件一个字节实际会发生多大的磁盘IO?相关推荐

  1. shmget 共享内存 同步读写文件一个进程写,多个进程读,读和写同步,边写边读

    首先,看看老大给我的任务:实现一个模块间的内存管理库, 实现以下功能 1.该内存库通讯的数据量不确定, 最大5Mbit/s  2.该内存库用于模块间的数据交互 3.该内存库只允许一个模块写入, 但可多 ...

  2. 计算机存储一个字节数是,一个字节可以存储多大的数字?

    一个字节有8位,每一位两种状态1或者0计算机储存数据是以二进制的方式,有一位为符号位,所以最大数为01111111转化为十进制数为127.若无符号,最大数为11111111转化为十进制为255.二进制 ...

  3. linux write文件,关于linux:write文件一个字节后何时发起写磁盘IO

    在前文<read文件一个字节理论会产生多大的磁盘IO?>写完之后,原本想着偷个懒,只通过读操作来让大家理解下Linux IO栈的各个模块就行了.但很多同学示意再让我写一篇对于写操作的.既然 ...

  4. 计算机u盘驱动坏了如何的修复,U盘损坏怎么恢复?学会这两招,快速恢复里面的文件...

    原标题:U盘损坏怎么恢复?学会这两招,快速恢复里面的文件 U盘损坏怎么恢复?U盘想必大家都不陌生,U盘是我们常用的数据存储设备,但是U盘也很脆弱,使用一段时间过后就会出现连接不上,打不开这些故障,当U ...

  5. 腾讯面试题:服务器内存1G,有一个2G的文件,里面每行存着一个QQ号(5-10位数),怎么最快找出出现过最多次的QQ号。

    腾讯最新面试题:服务器内存1G,有一个2G的文件,里面每行存着一个QQ号(5-10位数),怎么最快找出出现过最多次的QQ号. 以下是个人所建第Algorithms_12群内朋友的聊天记录: 首先你要注 ...

  6. 配置内存中OLTP文件组提高性能

    在今天的文章里,我想谈下使用内存中OLTP的内存优化文件组来获得持久性,还有如何配置它来获得高性能.在进入正题前,我想简单介绍下使用你数据库里这个特定文件组,内存OLTP是如何获得持久性的. 内存中O ...

  7. python 内存中的文件操作 StringIO cStringIO 简介

    StringIO StringIO的行为与file对象非常像,但它不是磁盘上文件,而是一个内存里的"文件",我们可以将操作磁盘文件那样来操作StringIO.一个简单的例子,让你对 ...

  8. 函数 —— strncpy() (内存重叠) memcpy() memmove() 一个字符串拷贝给另一个字符串

    char *strncpy(char *dest, const char *src, size_t n) *strncpy(char *dest, const char *src, size_t n) ...

  9. c语言中读取内存的文件,c++从内存中读取文件内容,内容写到内存 实现文件的内存共享代码实例...

    使用c++代码进行内存共享操作,内存共享可以通过key value的形式来保存内存,后面可以使用key值来直接读取内存,效率会很高/ 函数说明: shmget(key_t key, size_t si ...

最新文章

  1. 我的心愿秀、大家也来秀(show)一下
  2. 把老赵的页面缓存片断改一下,呵呵
  3. python观察日志(part12)--基于类的深拷贝与浅拷贝
  4. laravel关闭crsf
  5. pushbox(1)
  6. html5音乐加大音量,怎么调大音乐声音 mp3音量增大器介绍【图解】
  7. 安装Kubernets管理平台Ratel
  8. 爬虫技术(01)神箭手爬虫初学案例解读
  9. 基辛格等分享: ChatGPT 预示着一场智能革命,而人类还没有准备好
  10. CPU的主频/核心数
  11. 大唐移动android面试题,大唐移动面试经验
  12. android apk可安装成功但无法运行提示dex文件异常
  13. 用python实现成绩录入
  14. html中如何将背景图片模糊效果,【css】背景图片模糊效果
  15. 关于linux重启后磁盘分区消失的情况复现与修复
  16. 20201215记一次502错误
  17. 9月开学季CSDN高校俱乐部专家巡讲讲师招募
  18. 如何考察求职者的「学习能力」?
  19. 高速固态存储卡学习资料第701篇:基于6U VPX XC7V690T的阵列M.2高速固态存储卡
  20. HTML5见缝插针小游戏

热门文章

  1. 防止Stack smash的技术
  2. optee中的thread_vector_table线程向量表
  3. Golang经典面试题下
  4. 22、UPDATE多表关联更新
  5. hdu oj1093题解
  6. ACM入门之【拓扑排序】
  7. ACM入门之【离散化】
  8. 2021夏季每日一题 【week1 未完结】
  9. 高效的判断素数---筛选法
  10. 图元变形lisp源码_AutoLISP入门6---图元资料的取得与活用技巧(一).pdf