一、前言
首先,通过 Linux 文件系统数据写入流程来大致了解一下IO的底层原理。底层流程涉及用户空间与内核空间的交互、内存缓存(PageCache)管理、磁盘 I/O 调度等核心机制。以下是其核心流程的详细解析:
1、 用户空间触发写入
1.1 系统调用入口
用户程序通过 write() 或 pwrite() 系统调用发起写入请求。系统调用通过软中断(如 x86 的 int 0x80 或 syscall 指令)进入内核态。
1.2 参数解析与权限检查
内核在 write() 函数中验证文件描述符的有效性,检查文件是否可写,并根据 fd 找到对应的 file 结构体(关联文件的 inode)。
1.3 写入位置定位
根据 file 结构体中的 f_pos(文件偏移量)确定写入起始位置。若使用 pwrite(),则直接使用指定的偏移量。
2、内核空间数据处理
2.1 数据写入 Page Cache
- 内存映射优化
若文件通过 mmap() 映射到内存,用户直接修改映射区域会触发 写时复制(Copy-on-Write):- 首次写入时,内核为进程分配新页,复制原页数据到新页,标记为脏页。
- 后续修改直接作用于新页,最终通过脏页回写持久化。
- 非映射写入
通过 write() 写入时,内核调用 copy_from_user() 将用户空间数据复制到内核缓冲区(Page Cache)。
2.2 页管理与缺页中断
-
页存在性检查
内核通过 address_space 结构体(管理文件对应的 Page Cache)查找目标页:
-
页存在:直接写入并标记为脏页。
-
页不存在:触发 缺页中断(Page Fault):
- 硬件检测到虚拟地址未映射,触发异常。
- 内核检查地址合法性,调用 alloc_page() 分配物理页框。
- 若页在磁盘(如被换出),通过 do_readpage() 从磁盘加载数据。
- 更新页表,恢复程序执行。
-
2.3 脏页标记与回写队列
-
写入后的页被标记为 脏页(Dirty Page),加入 address_space 的脏页链表(如 dirty_list)。
-
内核通过 回写线程(如 bdi_writeback) 异步将脏页刷盘,或通过 fsync() 等同步机制强制立即刷盘。
3、磁盘 I/O 调度与持久化
3.1 I/O 调度队列
脏页回写请求通过 submit_bio() 提交到块设备层,加入电梯调度队列(如 CFQ、Deadline 算法),优化 I/O 顺序。
3.2 物理磁盘写入
数据最终通过驱动层(如 sd_dispatch_io())写入物理磁盘,完成持久化。
4、关键机制与优化
4.1 延迟写(Delayed Write)
内核默认采用异步刷盘策略,减少频繁 I/O。刷盘时机包括:
- 脏页比例超过阈值(如 vm.dirty_ratio)。
- 文件关闭时(fput() 触发 sync_file())。
- 系统内存不足时(触发 kswapd 回收内存)。
4.2 直接 I/O(Direct I/O)
通过 O_DIRECT 标志绕过 Page Cache,直接访问磁盘。适用于数据库等需要精细控制缓存的场景,但需自行管理缓存一致性。
4.3 预分配与零拷贝
- 预分配(fallocate())避免写入时动态扩展文件,减少碎片。
- sendfile() 等零拷贝技术减少数据在内核与用户空间的复制。
5、核心数据结构
- inode:存储文件元数据(大小、权限、块指针)。
- address_space:管理文件对应的 Page Cache 页。
- 页表:虚拟地址到物理地址的映射表,由 MMU 硬件维护。
- bio 结构体:封装 I/O 请求,包含数据、目标扇区等信息。
二、文件描述符 FD(File Descriptor)
1、定义
在 Linux 中,fd 全称为 “File descriptor”,中文为 “文件描述符”。它本质是一个非负整数,是进程访问文件或设备的索引值。
2、一切皆文件
涵盖范围:
设计意义:
Linux 内核将每一个文件对应一个索引,程序通过文件描述符操作文件,屏蔽底层细节,实现统一的 I/O 操作接口。
3、详细描述
3.1 文件描述符作用
文件描述符(File descriptor)是系统为管理进程已打开或新创建的文件所分配的非负整数(取值最小为 0),程序通过它执行文件操作,所有 I/O 操作的系统调用都需通过文件描述符实现。
3.2 系统预设的描述符
标准输入 stdin(0):进程默认输入来源。
标准输出 stdout(1):进程默认输出目标。
标准错误 stderr(2):进程错误信息输出目标。
3.3 进程的文件描述符表
进程通过该表记录自身使用的文件描述符信息,进程间相互独立(一个进程使用某文件描述符,另一进程无法共用)。
关联操作:
- 打开文件时分配(如 open() 的返回值)。
- 复制文件描述符(通过 dup()、dup2() 或 fcntl() 实现)。
3.4 系统级的打开文件描述符表
- 记录系统中所有打开文件的描述符,包含读写模式、偏移量等元数据。
- 对文件读写操作的支撑:如读(read)、写(write)需依据表中信息执行。
4、三个数据结构
4.1 进程的文件描述符表
记录进程打开的文件描述符,包含描述符与文件的映射关系。
4.2 系统级的打开文件描述符表
存储系统中所有打开文件的详细信息,如文件读写状态、操作函数指针等。
4.3 文件系统的 inode 表
作用:
记录文件元数据(如文件大小、权限、磁盘块指针)。
关联文件的物理存储位置,是文件系统管理文件的核心数据结构。
5、Linux 配置系统最大打开文件描述符个数
5.1 系统级限制
- 临时修改:通过 sysctl -w fs.file-max=xxx 命令设置(xxx 为目标数值)。
- 永久修改:编辑 /etc/sysctl.conf,添加 fs.file-max = xxx,保存后执行 sysctl -p 生效。
5.2 用户级限制
- 临时修改:使用 ulimit -n xxx 命令设置(xxx 为目标数值)。
- 永久修改:编辑 /etc/security/limits.conf,添加 * hard nofile xxx 和 * soft nofile xxx(xxx 为目标数值),保存后重启生效。
三、PageCache
1、定义
1.1 基础概念
Page Cache 是文件在内存中的缓存结构,属于虚拟文件系统层核心组件。文件读取时优先从 Page Cache 加载数据;写入时数据先存入 Page Cache,再通过回写机制持久化到磁盘。以页为单位存储(常见 4KB),匹配系统内存管理页大小,通过管理文件缓存提升系统 I/O 性能,减少磁盘直接访问。
1.2 核心作用
- 读优化:数据命中 Page Cache 时直接内存读取,规避磁盘 I/O 延迟。
- 写优化:数据先写入 Page Cache 延迟落盘,合并多次写操作,降低磁盘 I/O 压力。
2、内核维护中间层
2.1 硬件关联机制
借助 CPU 硬件缓存加速内存访问,提升 Page Cache 读取效率。
作为内存与磁盘的 “速度缓冲带”,缓解性能差异,降低磁盘读写负担。
2.2 内核维护场景
内存动态管理:根据内存使用情况调整 Page Cache 占用,内存紧张时回收空间。
多进程交互:多进程通过 Page Cache 共享数据,避免重复读盘。
磁盘 I/O 优化:顺序读预读、随机读缓存热点,减少磁盘寻道时间。
数据一致性保障:通过回写机制确保 “脏页” 数据持久化到磁盘。
3、引入 Page Cache 的原因
3.1 读操作
命中时直接内存读取;未命中时从磁盘读取并存入 Page Cache。
3.2 写操作
数据先写入 Page Cache 标记 “脏页”,累积后批量回写磁盘,减少随机写次数。
3.3 系统优化
减少磁盘访问,延长磁盘寿命,释放 CPU 资源。
4 Page Cache 工作流程
4.1 读流程
内核检查 Page Cache 是否存在目标页。
命中直接返回数据;未命中则从磁盘读取,载入后返回。
4.2 写流程
数据写入 Page Cache 并标记为 “脏页”。
满足回写条件时,内核回写线程将脏页数据写入磁盘。
5、Page Cache 写策略
5.1 Write Through(直写策略)
数据写入 Page Cache 同时写入磁盘,保障强一致性,适用于高一致性场景,性能受磁盘限制。
5.2 Write Back(回写策略)
数据先写入 Page Cache 标记脏页,后续批量回写磁盘,减少 I/O 次数,通过内核线程管理回写。
6、Page Cache 可调参数
vm.dirty_background_ratio:触发后台回写的脏页比例阈值(默认 10%)。
vm.dirty_ratio:阻塞进程写操作的脏页比例阈值(默认 20%)。
其他参数:如控制脏页停留时间(vm.dirty_expire_centisecs)、回写间隔(vm.dirty_writeback_centisecs)等。
7、手动释放 Page Cache
命令:
sync:确保脏页写入磁盘。
echo 1/2/3 > /proc/sys/vm/drop_caches:分别释放页缓存、目录项 / 索引节点缓存、全部相关缓存。
注意:释放操作影响业务性能,需谨慎执行。
四、总结
本文先介绍了一下Linux 文件系统数据写入流程,然后着重讲了两个重要的概念,FD和PageCache,了解这两个概念可以更好的帮助我们理解Linux文件系统IO。接下面一篇文章,我将继续补充一些概念。