文件从打开到读写都经历了什么?

1,400 阅读7分钟

文件的打开

linux中,进程要打开一个文件会使用fopen()这个系统调用,用于根据文件名打开一个文件,返回该文件的文件描述符,文件打开后进程便可以根据文件描述符fd进行其他操作,比如:读,写,关闭等操作。 还不知道文件描述符是什么的可以阅读一下前文。 先贴出fopen系统调用的主要流程图,我们一个个来看这些主要流程: image.png

  • PCB大家都清楚,是描述进程的一个数据结构
  • 进程文件打开表阅读前文之后就知道,他其实是一个存放file结构体(代表一个打开的文件)的数组,这个数组的索引就是我们常听到的文件描述符
  • 系统文件打开表是所有进程共享的,代表整个系统所打开的全部文件,注意区别前面的进程文件打开表是进程私有的
  • 其中FCB指的是linux中的inode结构体(索引节点,代表一个文件),active inode就是inode在内存中的缓存

上图不难看出:

在一个进程中调用fopen,首先会查找系统文件打开表,会出现两种情况:

  1. 如果文件a已经打开:则在进程文件打开表中为文件a分配一个表项,然后将该表项的指针指向系统文件打开表中和文件a对应的一项;然后再PCB中为文件分配一个文件描述符fd,作为进程文件打开表项的指针,文件打开完成
  2. 如果文件a没有打开,其打开过程:
  • 查看含有文件a信息的目录项是否在内存中,如果不在,将目录表装入到内存中,作为cache
  • 根据目录项中的d_inode字段找到inode在磁盘中的位置
  • 将文件a的inode装入到内存中的Active inode中
  • 然后在系统文件打开表中为文件a增加新的一个表项,将表项的指针指向Active Inode中文件a的inode
  • 然后在进程的文件打开表中分配新的一项,将该表项的指针指向系统文件打开表中文件a对应的表项,返回表项(数组)的索引作为文件描述符fd,文件打开完成

对于dentry和inode还不了解的可以阅读:juejin.cn/post/716240…

也就是说,调用fopen系统调用并成功拿到文件描述符之后,就代表这个文件在当前进程被打开了,而这个文件描述符fd就是读写文件的必备参数下面介绍的read和write系统调用都需要传入文件描述符作为参数

文件的读写

在阅读本段落之前,需要了解Page Cache和Buffer Cache的基本概念,参考文章:juejin.cn/post/716240…

读取文件

读取文件过程:

  1. 进程调用库函数向内核发起读文件请求
  2. 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项
  3. 调用该文件可用的系统调用函数read(),read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode
  4. 在inode中,通过文件内容偏移量计算出要读取的页
  5. 通过inode找到文件对应的address_space(连接磁盘inode和内存page的桥梁)
  6. 在address_space中访问该文件的页缓存树(文件在Page Cache中以基数树形式组织),查找对应的页缓存结点:
  • a、如果页缓存命中,那么直接返回文件内容
  • b、如果页缓存缺失,那么产生一个页缺失中断:创建一个页缓存页,同时通过inode找到文件 该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;
  1. 内核将读取的数据拷贝到用户空间,文件内容读取成功

了解了文件的读取过程之后,来思考一个问题。读取1字节的文件实际会发生多大的磁盘IO? 这个问题,更加明确的可以分为三个问题:

第一个问题:读取1 个字节的文件是否会导致磁盘 IO ?

根据上述读取文件的流程可知,如果 Page Cache 命中的话,根本就没有磁盘 IO 产生。

第二个问题:假如 Page Cache 没有命中,那么一定会有传动到机械轴上进行磁盘 IO 吗?

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

第三个问题:如果这些缓存都未命中,发生了磁盘 IO,那发生的是多大的 IO 呢?

整个 IO 过程中涉及到了好几个内核组件,而每个组件之间都是采用不同长度的块来管理磁盘数据的。

  • Page Cache 是以页为单位的,Linux 页大小一般是 4KB
  • 文件系统是以块(block)为单位来管理的,使用 dumpe2fs 可以查看,一般一个块默认是 4KB
  • 通用块层是以段为单位来处理磁盘 IO 请求的,一个段为一个页或者是页的一部分
  • IO 调度程序通过 DMA 方式传输 N 个扇区到内存,扇区一般为 512 字节
  • 硬盘也是采用“扇区”的管理和传输数据的

可以看到,虽然从用户角度确实是只读了 1 个字节,但是在整个内核工作流中,最小的工作单位是磁盘的扇区,为512字节,比1个字节要大的多。另外 block、page cache 等高层组件工作单位更大。其中 Page Cache 的大小是一个内存页 4KB。所以一般一次磁盘IO是多个扇区(512字节)一起进行的。假设通用块层 IO 的段就是一个内存页的话,一次磁盘 IO 就是 4 KB(8 个 512 字节的扇区)一起进行读取。

写入文件

写文件过程:

  1. 进程调用库函数向内核发起读文件请求
  2. 内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项
  3. 调用该文件可用的系统调用函数write(),write()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode
  4. 在inode中,通过文件内容偏移量计算出要读取的页
  5. 通过inode找到文件对应的address_space
  6. 在address_space中访问该文件的页缓存树,查找对应的页缓存结点:
  • a、如果页缓存命中,直接把文件内容修改更新在页缓存的页中,写文件就结束了,函数直接返回。这时候文件修改位于页缓存,并没有写回到磁盘文件中去
  • b、如果页缓存缺失,那么产生一个页缺失中断:创建一个页缓存页,同时通过inode找到文件 该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;
  1. 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块,有两种方式可以把脏页写回磁盘:
  • a、手动调用sync()或者fsync()系统调用把脏页写回
  • b、pdflush进程会定时把脏页写回到磁盘

注意:

脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。