30 | 文件缓存:常用文档应该放在触手可得的地方
下载APP
关闭
渠道合作
推荐作者
30 | 文件缓存:常用文档应该放在触手可得的地方
2019-06-05 刘超 来自北京
《趣谈Linux操作系统》
课程介绍
讲述:刘超
时长14:44大小13.46M
上一节,我们讲了文件系统的挂载和文件的打开,并通过打开文件的过程,构建了一个文件管理的整套数据结构体系。其实到这里,我们还没有对文件进行读写,还属于对于元数据的操作。那这一节,我们就重点关注读写。
系统调用层和虚拟文件系统层
文件系统的读写,其实就是调用系统函数 read 和 write。由于读和写的很多逻辑是相似的,这里我们一起来看一下这个过程。
下面的代码就是 read 和 write 的系统调用,在内核里面的定义。
对于 read 来讲,里面调用 vfs_read->__vfs_read。对于 write 来讲,里面调用 vfs_write->__vfs_write。
下面是 __vfs_read 和 __vfs_write 的代码。
上一节,我们讲了,每一个打开的文件,都有一个 struct file 结构。这里面有一个 struct file_operations f_op,用于定义对这个文件做的操作。__vfs_read 会调用相应文件系统的 file_operations 里面的 read 操作,__vfs_write 会调用相应文件系统 file_operations 里的 write 操作。
ext4 文件系统层
对于 ext4 文件系统来讲,内核定义了一个 ext4_file_operations。
由于 ext4 没有定义 read 和 write 函数,于是会调用 ext4_file_read_iter 和 ext4_file_write_iter。
ext4_file_read_iter 会调用 generic_file_read_iter,ext4_file_write_iter 会调用 __generic_file_write_iter。
generic_file_read_iter 和 __generic_file_write_iter 有相似的逻辑,就是要区分是否用缓存。
缓存其实就是内存中的一块空间。因为内存比硬盘快得多,Linux 为了改进性能,有时候会选择不直接操作硬盘,而是读写都在内存中,然后批量读取或者写入硬盘。一旦能够命中内存,读写效率就会大幅度提高。
因此,根据是否使用内存做缓存,我们可以把文件的 I/O 操作分为两种类型。
第一种类型是缓存 I/O。大多数文件系统的默认 I/O 操作都是缓存 I/O。对于读操作来讲,操作系统会先检查,内核的缓冲区有没有需要的数据。如果已经缓存了,那就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。对于写操作来讲,操作系统会先将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说,写操作就已经完成。至于什么时候再写到磁盘中由操作系统决定,除非显式地调用了 sync 同步命令。
第二种类型是直接 IO,就是应用程序直接访问磁盘数据,而不经过内核缓冲区,从而减少了在内核缓存和用户程序之间数据复制。
如果在读的逻辑 generic_file_read_iter 里面,发现设置了 IOCB_DIRECT,则会调用 address_space 的 direct_IO 的函数,将数据直接读取硬盘。我们在 mmap 映射文件到内存的时候讲过 address_space,它主要用于在内存映射的时候将文件和内存页产生关联。
同样,对于缓存来讲,也需要文件和内存页进行关联,这就要用到 address_space。address_space 的相关操作定义在 struct address_space_operations 结构中。对于 ext4 文件系统来讲, address_space 的操作定义在 ext4_aops,direct_IO 对应的函数是 ext4_direct_IO。
如果在写的逻辑 __generic_file_write_iter 里面,发现设置了 IOCB_DIRECT,则调用 generic_file_direct_write,里面同样会调用 address_space 的 direct_IO 的函数,将数据直接写入硬盘。
ext4_direct_IO 最终会调用到 __blockdev_direct_IO->do_blockdev_direct_IO,这就跨过了缓存层,到了通用块层,最终到了文件系统的设备驱动层。由于文件系统是块设备,所以这个调用的是 blockdev 相关的函数,有关块设备驱动程序的原理我们下一章详细讲,这一节我们就讲到文件系统到块设备的分界线部分。
接下来,我们重点看带缓存的部分如果进行读写。
带缓存的写入操作
我们先来看带缓存写入的函数 generic_perform_write。
这个函数里,是一个 while 循环。我们需要找出这次写入影响的所有的页,然后依次写入。对于每一个循环,主要做四件事情:
对于每一页,先调用 address_space 的 write_begin 做一些准备;
调用 iov_iter_copy_from_user_atomic,将写入的内容从用户态拷贝到内核态的页中;
调用 address_space 的 write_end 完成写操作;
调用 balance_dirty_pages_ratelimited,看脏页是否太多,需要写回硬盘。所谓脏页,就是写入到缓存,但是还没有写入到硬盘的页面。
我们依次来看这四个步骤。
第一步,对于 ext4 来讲,调用的是 ext4_write_begin。
ext4 是一种日志文件系统,是为了防止突然断电的时候的数据丢失,引入了日志**(Journal)**模式。日志文件系统比非日志文件系统多了一个 Journal 区域。文件在 ext4 中分两部分存储,一部分是文件的元数据,另一部分是数据。元数据和数据的操作日志 Journal 也是分开管理的。你可以在挂载 ext4 的时候,选择 Journal 模式。这种模式在将数据写入文件系统前,必须等待元数据和数据的日志已经落盘才能发挥作用。这样性能比较差,但是最安全。
另一种模式是 order 模式。这个模式不记录数据的日志,只记录元数据的日志,但是在写元数据的日志前,必须先确保数据已经落盘。这个折中,是默认模式。
还有一种模式是 writeback,不记录数据的日志,仅记录元数据的日志,并且不保证数据比元数据先落盘。这个性能最好,但是最不安全。
在 ext4_write_begin,我们能看到对于 ext4_journal_start 的调用,就是在做日志相关的工作。
在 ext4_write_begin 中,还做了另外一件重要的事情,就是调用 grab_cache_page_write_begin,来得到应该写入的缓存页。
在内核中,缓存以页为单位放在内存里面,那我们如何知道,一个文件的哪些数据已经被放到缓存中了呢?每一个打开的文件都有一个 struct file 结构,每个 struct file 结构都有一个 struct address_space 用于关联文件和内存,就是在这个结构里面,有一棵树,用于保存所有与这个文件相关的的缓存页。
我们查找的时候,往往需要根据文件中的偏移量找出相应的页面,而基数树 radix tree 这种数据结构能够快速根据一个长整型查找到其相应的对象,因而这里缓存页就放在 radix 基数树里面。
pagecache_get_page 就是根据 pgoff_t index 这个长整型,在这棵树里面查找缓存页,如果找不到就会创建一个缓存页。
第二步,调用 iov_iter_copy_from_user_atomic。先将分配好的页面调用 kmap_atomic 映射到内核里面的一个虚拟地址,然后将用户态的数据拷贝到内核态的页面的虚拟地址中,调用 kunmap_atomic 把内核里面的映射删除。
第三步,调用 ext4_write_end 完成写入。这里面会调用 ext4_journal_stop 完成日志的写入,会调用 block_write_end->__block_commit_write->mark_buffer_dirty,将修改过的缓存标记为脏页。可以看出,其实所谓的完成写入,并没有真正写入硬盘,仅仅是写入缓存后,标记为脏页。
但是这里有一个问题,数据很危险,一旦宕机就没有了,所以需要一种机制,将写入的页面真正写到硬盘中,我们称为回写(Write Back)。
第四步,调用 balance_dirty_pages_ratelimited,是回写脏页的一个很好的时机。
在 balance_dirty_pages_ratelimited 里面,发现脏页的数目超过了规定的数目,就调用 balance_dirty_pages->wb_start_background_writeback,启动一个背后线程开始回写。
通过上面的代码,我们可以看出,bdi_wq 是一个全局变量,所有回写的任务都挂在这个队列上。mod_delayed_work 函数负责将一个回写任务 bdi_writeback 挂在这个队列上。bdi_writeback 有个成员变量 struct delayed_work dwork,bdi_writeback 就是以 delayed_work 的身份挂到队列上的,并且把 delay 设置为 0,意思就是一刻不等,马上执行。
那具体这个任务由谁来执行呢?这里的 bdi 的意思是 backing device info,用于描述后端存储相关的信息。每个块设备都会有这样一个结构,并且在初始化块设备的时候,调用 bdi_init 初始化这个结构,在初始化 bdi 的时候,也会调用 wb_init 初始化 bdi_writeback。
这里面最重要的是 INIT_DELAYED_WORK。其实就是初始化一个 timer,也即定时器,到时候我们就执行 wb_workfn 这个函数。
接下来的调用链为:wb_workfn->wb_do_writeback->wb_writeback->writeback_sb_inodes->__writeback_single_inode->do_writepages,写入页面到硬盘。
在调用 write 的最后,当发现缓存的数据太多的时候,会触发回写,这仅仅是回写的一种场景。另外还有几种场景也会触发回写:
用户主动调用 sync,将缓存刷到硬盘上去,最终会调用 wakeup_flusher_threads,同步脏页;
当内存十分紧张,以至于无法分配页面的时候,会调用 free_more_memory,最终会调用 wakeup_flusher_threads,释放脏页;
脏页已经更新了较长时间,时间上超过了 timer,需要及时回写,保持内存和磁盘上数据一致性。
带缓存的读操作
带缓存的写分析完了,接下来,我们看带缓存的读,对应的是函数 generic_file_buffered_read。
读取比写入总体而言简单一些,主要涉及预读的问题。
在 generic_file_buffered_read 函数中,我们需要先找到 page cache 里面是否有缓存页。如果没有找到,不但读取这一页,还要进行预读,这需要在 page_cache_sync_readahead 函数中实现。预读完了以后,再试一把查找缓存页,应该能找到了。
如果第一次找缓存页就找到了,我们还是要判断,是不是应该继续预读;如果需要,就调用 page_cache_async_readahead 发起一个异步预读。
最后,copy_page_to_iter 会将内容从内核缓存页拷贝到用户内存空间。
总结时刻
这一节对于读取和写入的分析就到这里了。我们发现这个过程还是很复杂的,我这里画了一张调用图,你可以看到调用过程。
在系统调用层我们需要仔细学习 read 和 write。在 VFS 层调用的是 vfs_read 和 vfs_write 并且调用 file_operation。在 ext4 层调用的是 ext4_file_read_iter 和 ext4_file_write_iter。
接下来就是分叉。你需要知道缓存 I/O 和直接 I/O。直接 I/O 读写的流程是一样的,调用 ext4_direct_IO,再往下就调用块设备层了。缓存 I/O 读写的流程不一样。对于读,从块设备读取到缓存中,然后从缓存中拷贝到用户态。对于写,从用户态拷贝到缓存,设置缓存页为脏,然后启动一个线程写入块设备。
课堂练习
你知道如何查询和清除文件系统缓存吗?
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 16
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
29 | 虚拟文件系统:文件多了就需要档案管理系统
下一篇
31 | 输入与输出:如何建立售前售后生态体系?
精选留言(23)
- 石维康2019-06-05查看文件缓存:通过free命令中的buff/cache一栏的信息即可看到文件缓存的用量。 清除缓存:sync; echo 1 > /proc/sys/vm/drop_caches
作者回复: 赞
38 - why2019-06-07- 系统调用层和虚拟文件系统层 - 调用 read/write 进行读写 → vfs_read/write → __vfs_read/write - 打开文件时创建 struct file, 其中有 file_operations, 虚拟文件系统调用 operations 中的 read/write - ext4 文件系统层 - 调用到 generic_file_read/write_iter, 其中判断是否需要使用缓存 - 缓存, 即内存中一块空间, 可分为两类 I/O - 缓存 I/O: 默认模式, 读操作先检测缓存区中是否有, 若无则从文件系统读取并缓存; 写操作直接从用户空间赋值到内核缓存中, 再由 OS 决定或用户调用 sync 写回磁盘 - 直接 I/O: 程序直接访问磁盘, 不经过缓存 - 直接 I/O 过程: - 读: 若设置了 IOCB_DIRECT, 调用 address_space 的 direct_io 直接读取硬盘( 文件与内存页映射) ; 若使用缓存也要调用 address_sapce 进行文件与内存页的映射 - 写: 若设置了 IOCB_DIRECT, 调用块设备驱动直接写入磁盘 - 带缓存写过程 - 在 while 循环中, 找出写入影响的页, 并依次写入, 完成以下四步 - 每一页调用 write_begin 做准备 - 将写入内容从用户态拷贝到内核态 - 调用 write_end 完成写入 - 查看脏页 (未写入磁盘的缓存) 是否过多, 是否需要写回磁盘 - write_begin 做准备 - ext4 是日志文件系统, 通过日志避免断电数据丢失 - 文件分为元数据和数据, 其操作日志页分开维护 - Journal 模式下: 写入数据前, 元数据及数据日志必须落盘, 安全但性能差 - Order 模式下: 只记录元数据日志, 写日志前, 数据必须落盘, 折中 - Writeback 模式下: 仅记录元数据日志, 数据不用先落盘 - write_begin 准备日志, 并得到应该写入的缓存页 - 内核中缓存以页为单位, 打开文件的 file 结构中用 radix tree 维护文件的缓存页 - iov_iter_copy_from_user_atomic 拷贝内容, kmap_atomic 将缓存页映射到内核虚拟地址; 将拥护他数据拷贝到内核态; kunmap_aotmic 解映射 - write_end, 先完成日志写入 并将缓存设置为脏页 - 调用 balance_dirty_pages_ratelimited 若发先脏页超额, 启动一个线程执行回写. - 回写任务 delayed_work 挂在 bdi_wq 队列, 若delay 设为 0, 马上执行回写 - bdi = backing device info 描述块设备信息, 初始化块设备时回初始化 timer, 到时会执行写回函数 - 另外其他情况也会回写 - 用户调用 sync 或内存紧张时, 回调用 wakeup_flusher_threads 刷回脏页 - 脏页时间超过 timer, 及时回写 - 带缓存读 - generic_file_buffered_read 从 page cache 中判断是否由缓存页 - 若没则从文件系统读取并预读并缓存, 再次查找缓存页 - 若有, 还需判断是否需要预读, 若需要调用 page_cache_async_readahead - 最后调用 copy_page_to_user 从内核拷贝到用户空间展开共 2 条评论18
- 莫名2019-07-22“ext4_direct_IO 最终会调用到 __blockdev_direct_IO->do_blockdev_direct_IO,这就跨过了缓存层,直接到了文件系统的设备驱动层。” 觉得这个说法并不准确,绕过缓存,但并没有直接到达设备驱动层,而是通用块层,主要用于io合并之类操作,然后才是设备驱动层。
作者回复: 是的。赞,谢谢指正
14 - 刘桢2019-06-05打卡,今年12月冲北邮!
作者回复: 加油
共 3 条评论11 - 马媛媛2019-06-06请问 ext4的Journal 模式有什么优势呢,有日志逐条落盘的这个开销,为啥write不直接落盘呢?
作者回复: 写入日志由于是顺序的,写入速度快很多
9 - 啦啦啦2019-07-25老师,我想问下,在学习mysql实战45讲这个课程里面,讲了数据库也有脏页和干净页,以及如何将脏页刷回磁盘的几个时机,请问这个机制是和本节课讲的操作系统的机制是一回事吗?谢谢老师
作者回复: 不一样,那是数据库层次的,不是操作系统层次的。
共 3 条评论7 - sugar2019-06-14看完了老师讲的文件系统的几节,收获颇丰。但如果想要自己去实践一下,很想知道有没有像wireshark那样的网络抓包工具一样底层的 可以针对文件系统,磁盘物理结构进行监控 分析的工具呢?google了一番没找到...共 1 条评论4
- 玉剑冰锋2019-06-12请教老师个问题1.系统默认脏页多长时间或者数量是多少的时候触发事件?2.如果脏页在回写过程中出现故障如何保证数据完整性?3.这里只是提到ext4,其他文件系统跟ext4相比原理一样吗?比如xfs?
作者回复: vm.dirty_background_bytes = 0 vm.dirty_background_ratio = 10 vm.dirty_bytes = 0 vm.dirty_expire_centisecs = 3000 vm.dirty_ratio = 30 vm.dirty_writeback_centisecs = 500 每个文件系统各自有各自的格式
4 - garlic2021-04-13free 查看Cache分配使用情况,其中 page cache是针对 file systems , buffer是针对 block devices 两者是在不同时期不同场景下涉及的缓存机制,kernel2.4版本之前是分开的,并存的。之后版本进行了融合, 清除缓存可以操作 /proc/sys/vm/drop_caches, 学习笔记https://garlicspace.com/2021/03/30/%e6%9f%a5%e8%af%a2%e5%92%8c%e6%b8%85%e9%99%a4%e6%96%87%e4%bb%b6%e7%b3%bb%e7%bb%9f%e7%bc%93%e5%ad%98/展开3
- 安排2019-06-05打卡,每天课程发出后及时看完3
- 响雨2020-12-03缓存利用局部性原理提高数据的读写速度,同时日志系统能够使随机读写变为顺序读写,也能提高速度。2
- 核桃2021-05-03这里建议作者明确说一下bio的概念,不管是直接io还是走缓存,最后都是会封装成一个bio请求到block层的。 另外,这里有一句说法,所有的异步IO 都是直接IO,这点可以关联起来看1
- lfn2019-12-142019-12-14,打卡。1
- djfhchdh2019-06-20free命令查看缓存
作者回复: 赞
1 - 周平2019-06-10老师讲的很清晰,方便以后查看和复习1
- 微秒2019-06-06打卡,慢慢看,虽然不是很懂。1
- 小鳄鱼2022-05-17第二遍: 与CPU的回写(高速缓存)策略不同的是,CPU是第二次使用脏Cache Line时立即回写。而这里,要达到一定数量的脏页才回写。为此,还需要配合更多的触发回写:长时间未回写,缓存空间紧张,用户主动sync 此外,回写采用的是异步线程,也可能导致数据丢失。因此还提供了日志。默认策略是:order。 这一切都是为了性能服务的。但这个日志策略,还考虑了数据安全跟机制性能,没有把路封死。让用户自行选择!在开发基础设施的过程中,就应该同时考虑多种场景,不帮用户选择,让他自己选择?展开
- 李2020-10-24不知道老师还会解答不?我有个疑问 mmap 和write 写的区别,按原理,mmap少了一次用户到内核的数据拷贝,应该快一些。 但我发现写数据比较大的情况下, mmap比write反而慢了。 不知道这个是什么原因?展开
- Q罗2020-06-20元数据和数据有什么区别?
- brian2020-05-28缓存I/O 内核缓存区 等于 内核缓冲区么 ? 缓存,缓冲含义不是不同的吗?
作者回复: 是一个意思,用词有点随意了