文件系统与IO优化
章节概述
文件系统是操作系统管理持久化数据的核心组件。本章将深入探讨Linux文件系统的IO路径、PageCache机制、IO调度器等核心内容,以及如何通过工具观测和优化文件IO性能。
学习目标:
- 理解Linux文件IO的完整路径
- 掌握PageCache的工作机制
- 学会使用iostat、iotop等工具分析IO性能
- 能够诊断和优化文件IO相关的性能问题
核心概念
1. Linux IO完整路径
读取路径:
用户进程 read()
↓
系统调用层
↓
VFS层(虚拟文件系统)
↓
具体文件系统(ext4/xfs等)
↓
PageCache(缓存层)
↓ ↓
命中 未命中
↓ ↓
返回 Block Layer
↓
IO Scheduler
↓
设备驱动
↓
磁盘硬件
写入路径:
用户进程 write()
↓
系统调用层
↓
VFS层
↓
具体文件系统
↓
PageCache(标记脏页)
↓
后台flush线程
↓
Block Layer
↓
IO Scheduler
↓
设备驱动
↓
磁盘硬件
2. PageCache机制
PageCache架构:
┌─────────────────────────────────┐
│ 用户空间进程 │
└───────────┬─────────────────────┘
│ read/write
↓
┌─────────────────────────────────┐
│ PageCache │
│ ┌──────┬──────┬──────┬──────┐ │
│ │ Page │ Page │ Page │ Page │ │
│ │(Clean)│(Dirty)│(Clean)│(Dirty)│
│ └──────┴──────┴──────┴──────┘ │
└───────────┬─────────────────────┘
│ 未命中时
↓
┌─────────────────────────────────┐
│ 磁盘设备 │
└─────────────────────────────────┘
关键概念:
- Clean Page:与磁盘内容一致的页
- Dirty Page:被修改但未写入磁盘的页
- Page Fault:访问的页不在内存中
- Read-ahead:预读机制,提前读取后续页
3. IO调度器
调度器类型:
调度器 | 特点 | 适用场景 |
---|---|---|
mq-deadline | 多队列,SSD优化 | 普通SSD |
none | 完全不排序 | 高端NVMe |
bfq | 公平调度 | 桌面环境 |
kyber | 延迟优化 | 低延迟场景 |
源码解析
1. VFS层读取
关键文件: fs/read_write.c
// 系统调用read的实现
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
// VFS读取函数
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (!ret) {
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
ret = __vfs_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
}
2. PageCache操作
关键文件: mm/filemap.c
// 从PageCache读取
ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
ssize_t retval = 0;
loff_t *ppos = &iocb->ki_pos;
loff_t pos = *ppos;
// 尝试从PageCache读取
do {
struct page *page;
pgoff_t index;
unsigned long offset;
unsigned long nr, ret;
index = pos >> PAGE_SHIFT;
offset = pos & ~PAGE_MASK;
// 查找页缓存
page = find_get_page(mapping, index);
if (!page) {
// PageCache Miss,需要从磁盘读取
page_cache_sync_readahead(mapping, ra, file, index,
last_index - index);
page = find_get_page(mapping, index);
if (unlikely(!page))
goto no_cached_page;
}
// 从页缓存复制到用户空间
ret = copy_page_to_iter(page, offset, nr, iter);
} while (iov_iter_count(iter) && !ret);
return retval;
}
3. 脏页回写
关键文件: mm/page-writeback.c
// 后台回写线程
static int wb_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
unsigned long wb_start = jiffies;
long nr_pages = work->nr_pages;
struct inode *inode;
struct writeback_control wbc = {
.sync_mode = work->sync_mode,
.tagged_writepages = work->tagged_writepages,
.for_kupdate = work->for_kupdate,
.for_background = work->for_background,
.range_cyclic = work->range_cyclic,
};
while (!list_empty(&wb->b_io)) {
inode = wb_inode(wb->b_io.prev);
// 回写inode的脏页
__writeback_single_inode(inode, &wbc);
}
return nr_pages - wbc.nr_to_write;
}
️ 实用命令
1. IO性能观测
iostat - IO统计信息
# 每秒显示一次IO统计
iostat -x 1
# 输出解释
# Device: 设备名称
# r/s: 每秒读请求数
# w/s: 每秒写请求数
# rkB/s: 每秒读取的KB数
# wkB/s: 每秒写入的KB数
# await: IO请求的平均等待时间(毫秒)
# %util: 设备利用率
示例输出:
Device r/s w/s rkB/s wkB/s await %util
sda 50 100 2000 4000 5.2 45.0
nvme0n1 200 300 8000 12000 1.2 80.0
2. 进程级IO观测
iotop - 实时IO监控
# 显示IO使用最高的进程
sudo iotop -o
# 仅显示进程(不显示线程)
sudo iotop -oP
# 输出解释
# TID: 线程ID
# DISK READ: 磁盘读取速度
# DISK WRITE: 磁盘写入速度
# IO: IO百分比
# COMMAND: 命令名称
pidstat - 进程IO统计
# 显示进程IO统计
pidstat -d 1
# 输出解释
# kB_rd/s: 每秒读取的KB数
# kB_wr/s: 每秒写入的KB数
# kB_ccwr/s: 每秒取消的写入KB数
3. PageCache观测
查看系统缓存使用:
# 查看内存使用情况(包括缓存)
free -h
# 查看缓存的详细信息
cat /proc/meminfo | grep -i cache
# 查看脏页统计
cat /proc/meminfo | grep Dirty
清理缓存(谨慎使用):
# 同步脏页到磁盘
sync
# 清理PageCache
echo 1 | sudo tee /proc/sys/vm/drop_caches
# 清理dentries和inodes
echo 2 | sudo tee /proc/sys/vm/drop_caches
# 清理所有缓存
echo 3 | sudo tee /proc/sys/vm/drop_caches
4. IO调度器管理
查看当前调度器:
cat /sys/block/sda/queue/scheduler
# 输出: [mq-deadline] kyber none
切换调度器:
# 临时切换
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler
# 永久生效(修改grub)
sudo vim /etc/default/grub
# 添加: GRUB_CMDLINE_LINUX="elevator=none"
sudo update-grub
代码示例
1. 文件读写性能测试
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#include <string.h>
#define FILE_SIZE (100 * 1024 * 1024) // 100MB
#define BUFFER_SIZE 4096
// 计算时间差(毫秒)
long time_diff_ms(struct timeval *start, struct timeval *end) {
return (end->tv_sec - start->tv_sec) * 1000 +
(end->tv_usec - start->tv_usec) / 1000;
}
// 测试普通写入
void test_normal_write() {
int fd;
char buffer[BUFFER_SIZE];
struct timeval start, end;
memset(buffer, 'A', BUFFER_SIZE);
printf("=== 普通写入测试 ===\n");
gettimeofday(&start, NULL);
fd = open("/tmp/test_normal.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return;
}
for (int i = 0; i < FILE_SIZE / BUFFER_SIZE; i++) {
write(fd, buffer, BUFFER_SIZE);
}
close(fd);
gettimeofday(&end, NULL);
printf("写入100MB耗时: %ld ms\n", time_diff_ms(&start, &end));
printf("写入速度: %.2f MB/s\n\n",
100.0 / (time_diff_ms(&start, &end) / 1000.0));
}
// 测试同步写入
void test_sync_write() {
int fd;
char buffer[BUFFER_SIZE];
struct timeval start, end;
memset(buffer, 'A', BUFFER_SIZE);
printf("=== 同步写入测试 (O_SYNC) ===\n");
gettimeofday(&start, NULL);
fd = open("/tmp/test_sync.dat", O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, 0644);
if (fd < 0) {
perror("open");
return;
}
for (int i = 0; i < FILE_SIZE / BUFFER_SIZE; i++) {
write(fd, buffer, BUFFER_SIZE);
}
close(fd);
gettimeofday(&end, NULL);
printf("写入100MB耗时: %ld ms\n", time_diff_ms(&start, &end));
printf("写入速度: %.2f MB/s\n\n",
100.0 / (time_diff_ms(&start, &end) / 1000.0));
}
// 测试读取(PageCache命中)
void test_cached_read() {
int fd;
char buffer[BUFFER_SIZE];
struct timeval start, end;
printf("=== PageCache命中读取测试 ===\n");
// 先读一次,填充PageCache
fd = open("/tmp/test_normal.dat", O_RDONLY);
if (fd < 0) {
perror("open");
return;
}
while (read(fd, buffer, BUFFER_SIZE) > 0);
close(fd);
// 再次读取,应该全部命中PageCache
gettimeofday(&start, NULL);
fd = open("/tmp/test_normal.dat", O_RDONLY);
while (read(fd, buffer, BUFFER_SIZE) > 0);
close(fd);
gettimeofday(&end, NULL);
printf("读取100MB耗时: %ld ms\n", time_diff_ms(&start, &end));
printf("读取速度: %.2f MB/s\n\n",
100.0 / (time_diff_ms(&start, &end) / 1000.0));
}
int main() {
test_normal_write();
test_sync_write();
test_cached_read();
// 清理测试文件
unlink("/tmp/test_normal.dat");
unlink("/tmp/test_sync.dat");
return 0;
}
2. IO观测工具
#!/bin/bash
# io_monitor.sh - IO性能监控脚本
echo "===== IO性能监控工具 ====="
echo ""
# 检查是否为root用户
if [ "$EUID" -ne 0 ]; then
echo "请使用root权限运行此脚本"
exit 1
fi
# 1. 显示系统IO统计
echo "1. 系统IO统计 (5秒采样):"
iostat -x 1 5 | tail -n +4
echo ""
echo "2. 当前IO最高的进程:"
iotop -b -n 1 -o | head -n 10
echo ""
echo "3. PageCache使用情况:"
free -h | grep -E "Mem|Swap|buff"
echo ""
echo "4. 脏页统计:"
cat /proc/meminfo | grep -E "Dirty|Writeback"
echo ""
echo "5. IO调度器配置:"
for disk in /sys/block/sd*/queue/scheduler /sys/block/nvme*/queue/scheduler; do
if [ -f "$disk" ]; then
echo "$(basename $(dirname $(dirname $disk))): $(cat $disk)"
fi
done
echo ""
echo "监控完成"
3. Go语言文件IO示例
package main
import (
"fmt"
"io"
"os"
"time"
)
const (
fileSize = 100 * 1024 * 1024 // 100MB
bufferSize = 4096
)
// 测试缓冲写入
func testBufferedWrite() {
fmt.Println("=== 缓冲写入测试 ===")
file, err := os.Create("/tmp/test_buffered.dat")
if err != nil {
panic(err)
}
defer file.Close()
buffer := make([]byte, bufferSize)
for i := range buffer {
buffer[i] = 'A'
}
start := time.Now()
written := 0
for written < fileSize {
n, err := file.Write(buffer)
if err != nil {
panic(err)
}
written += n
}
elapsed := time.Since(start)
speed := float64(fileSize) / (1024 * 1024) / elapsed.Seconds()
fmt.Printf("写入100MB耗时: %v\n", elapsed)
fmt.Printf("写入速度: %.2f MB/s\n\n", speed)
}
// 测试直接读取
func testDirectRead() {
fmt.Println("=== 直接读取测试 ===")
file, err := os.Open("/tmp/test_buffered.dat")
if err != nil {
panic(err)
}
defer file.Close()
buffer := make([]byte, bufferSize)
start := time.Now()
totalRead := 0
for {
n, err := file.Read(buffer)
totalRead += n
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
}
elapsed := time.Since(start)
speed := float64(totalRead) / (1024 * 1024) / elapsed.Seconds()
fmt.Printf("读取%dMB耗时: %v\n", totalRead/(1024*1024), elapsed)
fmt.Printf("读取速度: %.2f MB/s\n\n", speed)
}
func main() {
testBufferedWrite()
testDirectRead()
// 清理测试文件
os.Remove("/tmp/test_buffered.dat")
}
动手实验
实验1:对比不同IO模式性能
目标: 理解PageCache对IO性能的影响
步骤:
- 编译测试程序:
gcc -o io_test io_test.c
- 清理缓存后运行:
# 清理PageCache
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
# 运行测试
./io_test
- 观测IO行为:
# 终端1:运行测试程序
./io_test
# 终端2:观测IO统计
iostat -x 1
- 分析结果:
- 比较普通写入和同步写入的性能差异
- 观察PageCache命中时的读取速度
- 理解缓存对性能的影响
预期结果:
- 普通写入速度:500-1000 MB/s
- 同步写入速度:50-100 MB/s
- PageCache命中读取:1000+ MB/s
实验2:IO调度器性能对比
目标: 理解不同IO调度器的特点
步骤:
- 准备测试脚本:
#!/bin/bash
# scheduler_test.sh
DEVICE="sda" # 修改为你的设备名
SCHEDULERS=("mq-deadline" "none" "kyber")
for scheduler in "${SCHEDULERS[@]}"; do
echo "测试调度器: $scheduler"
echo $scheduler | sudo tee /sys/block/$DEVICE/queue/scheduler
# 清理缓存
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
# 运行fio测试
sudo fio --name=test --filename=/tmp/fio_test \
--size=1G --rw=randread --bs=4k \
--numjobs=4 --time_based --runtime=30 \
--group_reporting
echo "---"
done
- 安装fio工具:
sudo apt install fio
- 运行测试:
chmod +x scheduler_test.sh
./scheduler_test.sh
- 分析结果:
- 比较不同调度器的IOPS
- 观察延迟差异
- 理解调度器的适用场景
实验3:PageCache命中率分析
目标: 观测和分析PageCache的命中率
步骤:
- 安装cachestat工具:
sudo apt install bpfcc-tools
- 运行测试程序并观测:
# 终端1:运行IO测试
./io_test
# 终端2:观测PageCache统计
sudo /usr/share/bcc/tools/cachestat 1
- 观察输出:
HITS MISSES DIRTIES BUFFERS_MB CACHED_MB
1000 50 100 200 1000
1500 30 120 200 1020
- 分析命中率:
- HITS: PageCache命中次数
- MISSES: PageCache未命中次数
- 命中率 = HITS / (HITS + MISSES)
常见问题
Q1: PageCache会占用多少内存?
A: PageCache会动态使用系统的空闲内存。Linux采用"用完所有空闲内存"的策略,当应用需要内存时会自动释放PageCache。通过free -h
命令的buff/cache
列可以看到PageCache使用量。
Q2: 如何判断IO是否成为瓶颈?
A: 观察以下指标:
iostat
中的%util
接近100%await
时间过长(>10ms为慢)vmstat
中的wa
(IO等待)值高- 应用响应慢但CPU利用率低
Q3: SSD是否需要IO调度器?
A:
- 普通SATA SSD:使用
mq-deadline
- 高端NVMe SSD:使用
none
(无调度) - 桌面环境:考虑使用
bfq
Q4: 如何优化文件IO性能?
A: 优化策略:
- 使用合适的缓冲区大小(通常4KB-1MB)
- 减少系统调用次数(批量IO)
- 利用PageCache(顺序读写)
- 使用异步IO(aio/io_uring)
- 对延迟敏感的场景使用Direct IO
复习题
选择题
Linux默认使用的文件系统是:
- A. ext3
- B. ext4
- C. xfs
- D. btrfs
PageCache的主要作用是:
- A. 加速CPU访问
- B. 缓存文件内容
- C. 管理进程内存
- D. 存储程序代码
在
iostat
输出中,await
表示:- A. CPU等待时间
- B. IO请求平均等待时间
- C. 网络延迟
- D. 进程等待时间
以下哪个标志会绕过PageCache?
- A. O_RDONLY
- B. O_WRONLY
- C. O_DIRECT
- D. O_APPEND
脏页(Dirty Page)是指:
- A. 被删除的页
- B. 已修改未写入磁盘的页
- C. 从磁盘读取的页
- D. 空闲的页
简答题
解释文件读取时PageCache的工作流程。
为什么同步写入(O_SYNC)比普通写入慢很多?
什么情况下应该使用Direct IO?
解释IO调度器的作用和常见类型。
如何诊断系统是否存在IO瓶颈?
实战题
性能优化题: 一个应用需要频繁读取大量小文件,性能很差。请分析可能的原因并提出优化方案。
IO分析题: 设计一个实验来测量你的磁盘的随机读写IOPS和顺序读写带宽。
故障排查题: 系统响应变慢,
top
显示CPU空闲,但wa
值很高。请描述你的排查步骤和可能的解决方案。
扩展阅读
推荐资源
深入方向
- io_uring异步IO框架
- 文件系统journaling机制
- SSD优化技术(TRIM、磨损均衡)
- 分布式文件系统(Ceph、GlusterFS)
下一章预告: 我们将深入探讨零拷贝技术,包括sendfile、splice、mmap等高级IO技术,以及如何使用它们优化文件传输性能。