Linux系统编程——文件IO——内核内幕
1. 引言
设计IO的内核实现,主要是三个子系统:虚拟文件系统,页面缓存,页面写回。
2. 虚拟文件系统
虚拟文件系统,也称为 虚拟文件切换系统(virtual file switch),让linux内核在调用文件系统函数时,不需要知道所使用的具体文件系统。
VFS的好处很多:单一系统调用可以读取任何存储媒介上的数据,单一实例可以从一个文件系统复制到任何其它系统。
VFS的实现原理是,公共文件模型,公共文件模型为linux内核的文件系统提供了一个必须遵循的框架。此框架以面向对象思想,提供了 回调函数 来支持 读写等功能,每个文件系统还可以注册其特有的函数。
此做法让各个文件系统有一定程度的共同性,如VFS会用到 inode, superblock, directory entry等对象。若一个非unix文件系统可能会欠缺unix-like概念(如inode),就必须克服,当然linux克服了,所以linux支持FAT NTFS之类的文件系统。
当应用程序调用read,C链接库会在编译时将其转换为trap语句,当用户空间进程陷入内核后,控制权会通过系统调用处理程序交给read系统调用,于是内核可以找出支持所指定的文件描述符的是何种对象,然后内核调用与背后对象相应的读取函数。对文件系统而言,此函数是文件系统程序代码的一部分,他会做该做的事(如,从文件读数据),并将数据返回给用户空间的read()调用。于是从系统调用处理程序返回后,系统调用处理程序会将数据复制回用户空间,接着从用户空间read()系统调用返回,然后进程继续执行
对于系统编程人员,VFS的细节很重要,VFS让编程人员通过统一的IO接口(read,write)操作不同的文件系统,且不用担心文件系统的类型或文件位于哪种存储媒介上。
3. 页面缓存
页面缓存是一块内存存储区,用来临时存放最近从磁盘文件系统上访问的数据。
页面缓存利用时间局部性,即当一个资源被访问后,将来被再次访问的概率很高,缓存第一次访问到的数据,以避免日后高昂的磁盘访问。
查找文件系统时,内核先从页面缓存开始。只有当他不存在于缓存时,内核才会调用内存子系统从磁盘读取数据。因此首次读取数据时,数据从磁盘传送到页面缓存,再从页面缓存返回到应用程序,以后再读该数据,数据直接从缓存返回。
页面缓存区的大小可以变动,当IO操作将越来越多的数据存入内存,页面缓存会越来越大 ,导致耗尽内存,所以页面缓存需要修剪,释放其最少被用到的页面。修剪动作无隙自动进行。由于页面缓存大小可以变动,所以缓存区可以使用linux系统中所有的内存,并缓存尽可能多的数据。
然后,相较于修剪页面缓存中常被用到的部分,把很少被用到的数据块(是进程数据不是文件缓存)交换到磁盘更有意义。交换功能让内核可以将数据存储到磁盘上,因此可以使用比系统的RAM还大的内存空间。
内核实现了试探性算法让数据交换和页面缓存达到平衡,试探性算法决定是否可以换出数据到磁盘以取代修剪页面缓存。
交换和缓存的平衡可用/proc/sys/vm/swappiness来调整。此虚拟文件的值的范围是0-100,默认是60,越高意味着越偏向于交换,越低越偏向于修剪。
顺序局部性指,数据往往是顺序地被引用,所以内核还实现了页面缓存的预读功能。预读指随着每个读请求,将额外的数据从磁盘读进页面缓存的行为。
和页面缓存一样,内核也是采用动态方式管理预读功能,如果内核注意到进程始终使用预读功能读取数据,则会扩大先读范围,因而预先读进越来越多的数据,如预读范围可为16KB,也可扩大到128KB,反过来,若内核注意到预读无法提供有用数据,内核可能完全停用预读功能
页缓存是透明的,对于系统程序设计者,无法控制页面缓存,但是有效率的程序需要善用页面缓存,利用预读功能是可能的,顺序IO总是优于随机访问。
4. 页面写回
如前面提到write行为,内核的延后写入功能是通过缓存区完成的。
当一个进程送出写入请求,数据会被复制到一个缓存区,该缓存区会被标记为已被改变(dirty),接着写入请求返回,如果另一个写入请求针对同一个文件的同一个数据块,则缓存区会被新数据锁改变,如果写入请求针对相同文件的其它地方,则会产生新的缓存区。
最后被改变的缓存区需要提交给磁盘,使内存数据和磁盘数据同步。这就是 写回 。
通常情况会这么做:
- 若可用内存空间已经减少到可设置的阈值,则被改变的缓存区会被写回磁盘,并释放相关内存空间。
- 若被改变缓存区保存时间已经超过可设置的阈值,则缓存区写回到磁盘。但不释放相关内存,只将缓存区状态去除dirty,以避免数据永远维持在被改变状态(dirty)
写回操作是通过一群名为pdflush的内核线程进行的。当上面两种情况有一个符号时,pdflush线程被唤醒,开始写回操作,直到两种情况都不符合。
缓存区在内核被表示成 buffer_head 数据结构,其中记录了与缓存区相关的各种元数据,如缓存区是否被改变,其中还包含一个指针,用于指向实际的数据,此数据放在页面缓存区,也是缓存区子系统与页面缓存可以被整合在一起。
linux 延后写入功能和缓存区提供了快速写入功能,代价是断电时有数据漏失的风险。为了避免此风险,可以使用同步IO
一般的写(包括用mmap)都是将数据写到内存中的文件(页缓存),当写回时,脏页会加入 BIO队列,pdflush会按某种算法(如电梯算法)将块写入磁盘中的文件
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?