HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 系统底层修炼

    • 操作系统核心知识学习指南
    • CPU调度与上下文切换
    • CFS调度器原理与源码
    • 内存管理与虚拟内存
    • PageCache与内存回收
    • 文件系统与IO优化
    • 零拷贝与Direct I/O
    • 网络子系统架构
    • TCP协议深度解析
    • TCP问题排查实战
    • 网络性能优化
    • epoll与IO多路复用
    • 进程与线程管理
    • Go Runtime调度器GMP模型
    • 系统性能分析方法论
    • DPDK与用户态网络栈
    • eBPF与内核可观测性
    • 综合实战案例

文件系统与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性能的影响

步骤:

  1. 编译测试程序:
gcc -o io_test io_test.c
  1. 清理缓存后运行:
# 清理PageCache
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

# 运行测试
./io_test
  1. 观测IO行为:
# 终端1:运行测试程序
./io_test

# 终端2:观测IO统计
iostat -x 1
  1. 分析结果:
  • 比较普通写入和同步写入的性能差异
  • 观察PageCache命中时的读取速度
  • 理解缓存对性能的影响

预期结果:

  • 普通写入速度:500-1000 MB/s
  • 同步写入速度:50-100 MB/s
  • PageCache命中读取:1000+ MB/s

实验2:IO调度器性能对比

目标: 理解不同IO调度器的特点

步骤:

  1. 准备测试脚本:
#!/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
  1. 安装fio工具:
sudo apt install fio
  1. 运行测试:
chmod +x scheduler_test.sh
./scheduler_test.sh
  1. 分析结果:
  • 比较不同调度器的IOPS
  • 观察延迟差异
  • 理解调度器的适用场景

实验3:PageCache命中率分析

目标: 观测和分析PageCache的命中率

步骤:

  1. 安装cachestat工具:
sudo apt install bpfcc-tools
  1. 运行测试程序并观测:
# 终端1:运行IO测试
./io_test

# 终端2:观测PageCache统计
sudo /usr/share/bcc/tools/cachestat 1
  1. 观察输出:
HITS   MISSES  DIRTIES  BUFFERS_MB  CACHED_MB
  1000      50      100         200       1000
  1500      30      120         200       1020
  1. 分析命中率:
  • 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

复习题

选择题

  1. Linux默认使用的文件系统是:

    • A. ext3
    • B. ext4
    • C. xfs
    • D. btrfs
  2. PageCache的主要作用是:

    • A. 加速CPU访问
    • B. 缓存文件内容
    • C. 管理进程内存
    • D. 存储程序代码
  3. 在iostat输出中,await表示:

    • A. CPU等待时间
    • B. IO请求平均等待时间
    • C. 网络延迟
    • D. 进程等待时间
  4. 以下哪个标志会绕过PageCache?

    • A. O_RDONLY
    • B. O_WRONLY
    • C. O_DIRECT
    • D. O_APPEND
  5. 脏页(Dirty Page)是指:

    • A. 被删除的页
    • B. 已修改未写入磁盘的页
    • C. 从磁盘读取的页
    • D. 空闲的页

简答题

  1. 解释文件读取时PageCache的工作流程。

  2. 为什么同步写入(O_SYNC)比普通写入慢很多?

  3. 什么情况下应该使用Direct IO?

  4. 解释IO调度器的作用和常见类型。

  5. 如何诊断系统是否存在IO瓶颈?

实战题

  1. 性能优化题: 一个应用需要频繁读取大量小文件,性能很差。请分析可能的原因并提出优化方案。

  2. IO分析题: 设计一个实验来测量你的磁盘的随机读写IOPS和顺序读写带宽。

  3. 故障排查题: 系统响应变慢,top显示CPU空闲,但wa值很高。请描述你的排查步骤和可能的解决方案。

扩展阅读

推荐资源

  • Linux文件系统源码
  • ext4文件系统文档
  • IO性能分析

深入方向

  • io_uring异步IO框架
  • 文件系统journaling机制
  • SSD优化技术(TRIM、磨损均衡)
  • 分布式文件系统(Ceph、GlusterFS)

下一章预告: 我们将深入探讨零拷贝技术,包括sendfile、splice、mmap等高级IO技术,以及如何使用它们优化文件传输性能。

Prev
PageCache与内存回收
Next
零拷贝与Direct I/O