文件系统
Linux "一切皆文件"的设计哲学通过 VFS(虚拟文件系统)抽象层实现。理解 inode/dentry 结构、Page Cache 工作原理以及各种 I/O 模式,是优化 Java/Go 服务磁盘 I/O 性能的前提。
目录
| 章节 | 说明 |
|---|---|
| 文件系统层次结构 | VFS → 具体文件系统 → 块设备 |
| inode 与 dentry | 文件元数据与目录结构 |
| 软链接 vs 硬链接 | 本质区别与使用场景 |
| 文件 I/O 分类 | 缓冲/直接/阻塞/异步 |
| Page Cache 工作原理 | 读写缓存机制 |
| 常用文件操作命令 | find/grep/awk/sed 速查 |
文件系统层次结构
应用程序(open/read/write 系统调用)
↓
VFS(虚拟文件系统)
统一接口:inode/dentry/file/super_block
↓
┌──────┬──────┬──────┬──────┐
│ ext4 │ xfs │ NFS │/proc │ 具体文件系统
└──────┴──────┴──────┴──────┘
↓
通用块层(Block Layer)
I/O 调度器(CFQ/Deadline/NOOP)
↓
块设备驱动(SCSI/NVMe)
↓
物理磁盘
文件系统三大类:
| 类型 | 代表 | 说明 |
|---|---|---|
| 基于磁盘 | ext4、xfs、btrfs | 数据持久化到本地磁盘 |
| 基于内存 | /proc、/sys、tmpfs | 不占磁盘,内核动态构建 |
| 网络文件系统 | NFS、CIFS | 访问远程机器数据 |
# 查看已挂载的文件系统
df -hT
mount | column -t
# 查看磁盘空间使用
df -h
# 查看 inode 使用情况(inode 耗尽也会导致"磁盘满")
df -i
inode 与 dentry
inode(索引节点)
每个文件对应一个 inode,记录文件的元数据(不含文件名):
| 字段 | 内容 |
|---|---|
| inode 编号 | 文件系统内唯一标识 |
| 文件大小 | 字节数 |
| 权限 | rwxrwxrwx + 特殊位 |
| 时间戳 | atime/mtime/ctime |
| 硬链接计数 | 指向该 inode 的目录项数量 |
| 数据块指针 | 指向实际存储数据的磁盘块 |
# 查看文件的 inode 信息
stat filename
ls -i filename # 显示 inode 编号
# 根据 inode 编号查找文件
find /path -inum <inode_number>
dentry(目录项)
记录文件名与 inode 的映射关系,由内核在内存中维护(目录项缓存)。
关系:一个 inode 可对应多个 dentry(硬链接),一个 dentry 只指向一个 inode。
软链接 vs 硬链接
| 维度 | 硬链接 | 软链接(符号链接) |
|---|---|---|
| 本质 | 新的目录项指向同一 inode | 新的文件,内容是目标路径字符串 |
| inode | 相同 | 不同 |
| 跨文件系统 | 不支持 | 支持 |
| 目录 | 不支持(防止循环) | 支持 |
| 原文件删除后 | 仍可访问(inode 引用计数 > 0) | 变成悬空链接(dangling link) |
| 命令 | ln src dst |
ln -s src dst |
# 创建硬链接
ln original.txt hardlink.txt
ls -li original.txt hardlink.txt # inode 编号相同
# 创建软链接
ln -s /etc/nginx/nginx.conf nginx.conf
ls -la nginx.conf # 显示 -> 目标路径
# 查找所有悬空软链接
find /path -type l -xtype l
文件 I/O 分类
四个维度
维度 1:是否使用标准库缓冲
| 类型 | 说明 | 场景 |
|---|---|---|
| 缓冲 I/O | 通过 glibc 的用户态缓冲区(fread/fwrite) | 大多数场景,减少系统调用次数 |
| 非缓冲 I/O | 直接系统调用(read/write) | 需要精确控制数据时机 |
维度 2:是否绕过 Page Cache
| 类型 | 说明 | 场景 |
|---|---|---|
| 非直接 I/O(默认) | 经过 Page Cache | 普通文件读写,利用缓存加速 |
| 直接 I/O(O_DIRECT) | 绕过 Page Cache | 数据库(MySQL InnoDB)自管缓存 |
维度 3:是否阻塞调用线程
| 类型 | 说明 |
|---|---|
| 阻塞 I/O | 等待 I/O 完成才返回,线程挂起 |
| 非阻塞 I/O(O_NONBLOCK) | 立即返回,未就绪时返回 EAGAIN |
维度 4:通知方式
| 类型 | 说明 |
|---|---|
| 同步 I/O | 调用方等待 I/O 完成 |
| 异步 I/O(AIO) | 提交请求后立即返回,完成后通过事件/回调通知 |
阻塞 I/O: 应用 ──────────────────────→ 返回
等待数据 + 等待复制
非阻塞 I/O: 应用 → 查询 → EAGAIN(未就绪)
→ 查询 → EAGAIN
→ 查询 → 数据就绪 → 复制 → 返回
I/O 多路复用: 应用 → select/poll/epoll 监听多个 fd
→ 有 fd 就绪 → read → 返回
异步 I/O: 应用 → aio_read() → 立即返回
→ 内核完成后发信号/回调通知
Page Cache 工作原理
Page Cache 是 Linux 内核用内存缓存磁盘文件内容的机制,大幅减少磁盘 I/O。
读流程
read() 系统调用
↓
检查 Page Cache 是否有对应页
├── 命中(Cache Hit)→ 直接从内存返回
└── 未命中(Cache Miss)→ 从磁盘读入内存 → 更新 Page Cache → 返回
写流程(默认:写回模式 Write-Back)
write() 系统调用
↓
写入 Page Cache(内存),标记为"脏页"(dirty)
↓
立即返回(不等待磁盘写入)
↓
内核后台 pdflush/kworker 线程定期将脏页刷入磁盘
强制刷盘:
sync # 刷所有脏页
fsync(fd) # 刷指定文件(含元数据)
fdatasync(fd) # 刷数据,不刷元数据(更快)
查看 Page Cache
# 查看 buff/cache 大小
free -h
# 查看 Page Cache 详情
cat /proc/meminfo | grep -E "Cached|Buffers|Dirty|Writeback"
# 手动清理 Page Cache(生产环境谨慎)
echo 1 > /proc/sys/vm/drop_caches # 清理 Page Cache
echo 2 > /proc/sys/vm/drop_caches # 清理 slab(dentry/inode 缓存)
echo 3 > /proc/sys/vm/drop_caches # 清理 Page Cache + slab
Buffer vs Cache:
| 类型 | 用途 |
|---|---|
| Buffer | 块设备的原始数据缓存(写操作) |
| Cache | 文件系统的页缓存(读操作为主) |
常用文件操作命令
find — 文件查找
# 按名称查找
find /var/log -name "*.log"
# 按修改时间(最近 7 天修改的)
find /home -mtime -7
# 按大小(大于 100MB)
find /data -size +100M
# 按类型(只找目录)
find /etc -type d -name "nginx*"
# 找到后执行操作(删除 30 天前的日志)
find /var/log -name "*.log" -mtime +30 -exec rm {} \;
# 排除目录
find /data -path /data/backup -prune -o -name "*.txt" -print
grep — 内容搜索
# 递归搜索(常用)
grep -r "OutOfMemoryError" /var/log/
# 显示行号、前后上下文
grep -n -A 3 -B 3 "ERROR" app.log
# 忽略大小写
grep -i "exception" app.log
# 统计匹配行数
grep -c "ERROR" app.log
# 反向匹配(不含 DEBUG 的行)
grep -v "DEBUG" app.log
# 多模式匹配
grep -E "ERROR|WARN|FATAL" app.log
awk — 结构化文本处理
# 打印第 2 列(空格分隔)
awk '{print $2}' file.txt
# 条件过滤:第 3 列大于 100
awk '$3 > 100 {print $0}' file.txt
# 统计 HTTP 状态码分布
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 计算平均响应时间(第 7 列)
awk '{sum+=$7; count++} END {print "avg:", sum/count "ms"}' access.log
# 自定义分隔符(处理 CSV)
awk -F',' '{print $1, $3}' data.csv
sed — 流式文本编辑
# 替换(g 表示全局)
sed 's/old/new/g' file.txt
# 原地修改(-i)
sed -i 's/localhost/127.0.0.1/g' config.properties
# 删除空行
sed '/^$/d' file.txt
# 打印特定行范围
sed -n '10,20p' file.txt
# 在匹配行后插入内容
sed '/\[server\]/a host=127.0.0.1' config.ini
磁盘使用分析
# 按大小排序目录
du -sh /var/* | sort -rh | head -20
# 查找大文件
find / -type f -size +1G 2>/dev/null | xargs ls -lh
# 查看 inode 使用("磁盘满"但 df -h 显示有空间时检查)
df -i
# 找出占用 inode 最多的目录
find /var -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head
参考资料
- 《趣谈 Linux 操作系统》— 27-30 文件系统系列(刘超,极客时间)
- 《Linux 性能优化实战》— 22-32 文件系统与磁盘 I/O 模块(倪朋飞,极客时间)
man 2 open、man 2 read、man 2 mmap
评论 (0)
发表评论