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

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

PageCache与内存回收

章节概述

PageCache是Linux内核中至关重要的缓存机制,它显著提升了文件IO性能。本章将深入探讨PageCache的工作原理、脏页回写策略、内存回收机制,以及如何通过调优提升系统性能。

学习目标:

  • 理解PageCache的工作机制和性能影响
  • 掌握脏页回写的触发条件和策略
  • 学会使用工具观测和调优PageCache
  • 能够诊断和解决内存回收相关的性能问题

核心概念

1. PageCache架构

┌─────────────────────────────────────┐
│         用户空间应用                 │
│    read() / write()                 │
└──────────┬──────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│         VFS层                        │
└──────────┬──────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│       PageCache                      │
│  ┌──────────────────────────────┐  │
│  │ Clean Pages (干净页)         │  │
│  │ - 与磁盘内容一致              │  │
│  │ - 可以直接丢弃                │  │
│  └──────────────────────────────┘  │
│  ┌──────────────────────────────┐  │
│  │ Dirty Pages (脏页)           │  │
│  │ - 已修改未写入磁盘            │  │
│  │ - 需要回写后才能释放          │  │
│  └──────────────────────────────┘  │
└──────────┬──────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│      Block Layer / 磁盘              │
└─────────────────────────────────────┘

PageCache的作用:

  • 读缓存:避免重复读取磁盘
  • 写缓存:延迟写入,批量刷盘
  • 预读优化:提前读取后续数据
  • 减少IO次数:合并小IO为大IO

2. 脏页回写机制

触发条件:

1. 定时回写
   - 每隔dirty_writeback_centisecs(默认5秒)
   - 由pdflush/flush线程执行

2. 脏页比例超限
   - dirty_ratio: 脏页占总内存比例上限
   - dirty_background_ratio: 后台回写触发阈值

3. 手动同步
   - sync系统调用
   - fsync/fdatasync

4. 内存压力
   - 内存不足时强制回写

回写策略:

优先级顺序:
1. 最老的脏页优先
2. 同一文件的脏页合并
3. 顺序IO优化
4. 避免写放大

3. 内存回收机制

回收层次:

┌─────────────────────────────────┐
│ 1. 回收PageCache (Clean Pages) │ ← 最容易
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│ 2. 回写并回收Dirty Pages        │
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│ 3. Swap匿名页                   │
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│ 4. OOM Killer (杀进程)          │ ← 最后手段
└─────────────────────────────────┘

kswapd守护进程:

  • 后台回收进程,每个NUMA节点一个
  • 当内存低于low水位时被唤醒
  • 回收到high水位后休眠

4. 内存水位线

┌─────────────────────────────────┐
│         可用内存区域             │ ← high水位
├─────────────────────────────────┤
│                                 │
│                                 │ ← low水位(唤醒kswapd)
├─────────────────────────────────┤
│                                 │ ← min水位(直接回收)
├─────────────────────────────────┤
│         保留内存                 │
└─────────────────────────────────┘

水位线说明:

  • high:kswapd休眠的目标
  • low:kswapd被唤醒的阈值
  • min:触发直接回收,进程阻塞等待

源码解析

1. PageCache读取

关键文件: mm/filemap.c

// 从PageCache读取页
struct page *find_get_page(struct address_space *mapping, pgoff_t index)
{
    struct page *page;
    
    rcu_read_lock();
    page = radix_tree_lookup(&mapping->page_tree, index);
    if (page && !radix_tree_deref_retry(page)) {
        if (!page_cache_get_speculative(page))
            goto repeat;
        
        // PageCache命中
        if (unlikely(page != radix_tree_lookup(&mapping->page_tree, index))) {
            put_page(page);
            goto repeat;
        }
    }
    rcu_read_unlock();
    
    return page;
}

// 添加页到PageCache
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
                          pgoff_t index, gfp_t gfp_mask)
{
    void *shadow = NULL;
    int ret;
    
    __SetPageLocked(page);
    ret = __add_to_page_cache_locked(page, mapping, index, gfp_mask, &shadow);
    if (unlikely(ret))
        __ClearPageLocked(page);
    else {
        // 添加到LRU链表
        lru_cache_add(page);
    }
    
    return ret;
}

2. 脏页标记和回写

关键文件: mm/page-writeback.c

// 标记页为脏
void __set_page_dirty(struct page *page, struct address_space *mapping)
{
    unsigned long flags;
    
    spin_lock_irqsave(&mapping->tree_lock, flags);
    if (page->mapping) {
        // 设置脏标志
        radix_tree_tag_set(&mapping->page_tree, page->index, 
                          PAGECACHE_TAG_DIRTY);
    }
    spin_unlock_irqrestore(&mapping->tree_lock, flags);
    
    // 更新统计信息
    __inc_zone_page_state(page, NR_FILE_DIRTY);
    __inc_bdi_stat(mapping->backing_dev_info, BDI_RECLAIMABLE);
}

// 回写脏页
static int writeback_single_inode(struct inode *inode,
                                  struct writeback_control *wbc)
{
    struct address_space *mapping = inode->i_mapping;
    long nr_to_write = wbc->nr_to_write;
    int ret;
    
    // 跳过没有脏页的inode
    if (!mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
        goto out;
    
    // 回写脏页
    ret = do_writepages(mapping, wbc);
    
    // 同步元数据
    if (wbc->sync_mode == WB_SYNC_ALL)
        write_inode_now(inode, 1);
    
out:
    return ret;
}

// 后台回写线程
static long wb_do_writeback(struct bdi_writeback *wb)
{
    struct wb_writeback_work *work;
    long wrote = 0;
    
    while ((work = get_next_work_item(wb)) != NULL) {
        // 执行回写工作
        wrote += wb_writeback(wb, work);
        
        // 完成工作项
        wb_clear_pending(wb, work);
    }
    
    return wrote;
}

3. 内存回收

关键文件: mm/vmscan.c

// kswapd主循环
static int kswapd(void *p)
{
    pg_data_t *pgdat = (pg_data_t*)p;
    struct task_struct *tsk = current;
    
    for (;;) {
        bool was_frozen;
        
        // 等待唤醒
        wait_event_freezable(pgdat->kswapd_wait,
            kswapd_should_run(pgdat));
        
        // 执行内存回收
        balance_pgdat(pgdat, order, classzone_idx);
    }
    
    return 0;
}

// 回收内存主函数
static unsigned long shrink_zone(struct zone *zone,
                                 struct scan_control *sc)
{
    unsigned long nr_reclaimed = 0;
    
    // 扫描并回收不活跃页
    nr_reclaimed += shrink_inactive_list(lruvec, sc);
    
    // 扫描活跃页,将不活跃的移到不活跃链表
    shrink_active_list(lruvec, sc);
    
    return nr_reclaimed;
}

️ 实用命令

1. PageCache观测

查看系统内存使用:

# 总览
free -h

# 详细的PageCache信息
cat /proc/meminfo | grep -E "Cached|Dirty|Writeback"

# 输出示例:
# Cached:          8192000 kB   # PageCache大小
# Dirty:              1024 kB   # 脏页大小
# Writeback:             0 kB   # 正在回写的页

实时监控脏页:

watch -n 1 'cat /proc/meminfo | grep -E "Dirty|Writeback"'

# 或使用自定义脚本
while true; do
    date
    cat /proc/meminfo | grep -E "Cached|Dirty|Writeback"
    sleep 1
done

2. PageCache统计

使用vmstat:

vmstat 1

# 输出解释:
# bi: 从块设备读取的块数/秒
# bo: 写入块设备的块数/秒
# si: swap in
# so: swap out
# cache: PageCache大小

使用cachestat(eBPF工具):

# 安装bcc-tools
sudo apt install bpfcc-tools

# 实时监控PageCache命中率
sudo /usr/share/bcc/tools/cachestat 1

# 输出示例:
# HITS   MISSES  DIRTIES  BUFFERS_MB  CACHED_MB
# 1000      50      100         200       1000

3. 手动清理PageCache

清理前先同步:

# 同步所有脏页到磁盘
sync

清理PageCache:

# 清理PageCache
echo 1 | sudo tee /proc/sys/vm/drop_caches

# 清理dentries和inodes
echo 2 | sudo tee /proc/sys/vm/drop_caches

# 清理所有(PageCache + dentries + inodes)
echo 3 | sudo tee /proc/sys/vm/drop_caches

注意事项:

  • 清理会导致短暂性能下降
  • 通常不需要手动清理
  • 主要用于性能测试时建立baseline

4. 脏页参数调整

查看当前参数:

sysctl -a | grep dirty

关键参数:

# 脏页占总内存的比例上限(默认20%)
vm.dirty_ratio = 20

# 后台回写触发阈值(默认10%)
vm.dirty_background_ratio = 10

# 回写间隔(默认5秒,单位:百分之一秒)
vm.dirty_writeback_centisecs = 500

# 脏页最长存活时间(默认30秒)
vm.dirty_expire_centisecs = 3000

调整参数:

# 降低脏页比例(适合数据库等场景)
sudo sysctl -w vm.dirty_ratio=10
sudo sysctl -w vm.dirty_background_ratio=5

# 更频繁的回写(提高数据安全性)
sudo sysctl -w vm.dirty_writeback_centisecs=100

# 永久生效(写入/etc/sysctl.conf)
echo "vm.dirty_ratio=10" | sudo tee -a /etc/sysctl.conf

代码示例

1. 观测PageCache效果

#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

double get_time_ms() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}

void create_test_file() {
    int fd;
    char buffer[BUFFER_SIZE];
    
    printf("创建测试文件...\n");
    
    memset(buffer, 'A', BUFFER_SIZE);
    fd = open("/tmp/cache_test.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    
    for (int i = 0; i < FILE_SIZE / BUFFER_SIZE; i++) {
        write(fd, buffer, BUFFER_SIZE);
    }
    
    close(fd);
    printf("测试文件创建完成\n\n");
}

void test_cold_read() {
    int fd;
    char buffer[BUFFER_SIZE];
    double start, end;
    
    printf("=== 冷读测试(PageCache未命中)===\n");
    
    // 清理PageCache
    system("sync && echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null");
    
    start = get_time_ms();
    fd = open("/tmp/cache_test.dat", O_RDONLY);
    
    while (read(fd, buffer, BUFFER_SIZE) > 0);
    
    close(fd);
    end = get_time_ms();
    
    printf("读取100MB耗时: %.2f ms\n", end - start);
    printf("速度: %.2f MB/s\n\n", 100.0 / ((end - start) / 1000.0));
}

void test_hot_read() {
    int fd;
    char buffer[BUFFER_SIZE];
    double start, end;
    
    printf("=== 热读测试(PageCache命中)===\n");
    
    start = get_time_ms();
    fd = open("/tmp/cache_test.dat", O_RDONLY);
    
    while (read(fd, buffer, BUFFER_SIZE) > 0);
    
    close(fd);
    end = get_time_ms();
    
    printf("读取100MB耗时: %.2f ms\n", end - start);
    printf("速度: %.2f MB/s\n\n", 100.0 / ((end - start) / 1000.0));
    printf("提示: 热读比冷读快很多,这就是PageCache的效果!\n\n");
}

int main() {
    printf("PageCache性能测试\n");
    printf("==================\n\n");
    
    create_test_file();
    test_cold_read();
    test_hot_read();
    
    // 清理
    unlink("/tmp/cache_test.dat");
    
    return 0;
}

2. 监控脏页脚本

#!/bin/bash
# dirty_page_monitor.sh - 监控脏页变化

echo "脏页监控工具"
echo "按Ctrl+C停止"
echo ""

while true; do
    # 获取当前时间
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    
    # 获取脏页信息
    DIRTY=$(cat /proc/meminfo | grep "^Dirty:" | awk '{print $2}')
    WRITEBACK=$(cat /proc/meminfo | grep "^Writeback:" | awk '{print $2}')
    CACHED=$(cat /proc/meminfo | grep "^Cached:" | awk '{print $2}')
    
    # 计算脏页比例
    DIRTY_RATIO=$(awk "BEGIN {printf \"%.2f\", ($DIRTY/$CACHED)*100}")
    
    # 输出
    printf "%s | Dirty: %8d KB | Writeback: %8d KB | Cached: %10d KB | Ratio: %5.2f%%\n" \
           "$TIMESTAMP" "$DIRTY" "$WRITEBACK" "$CACHED" "$DIRTY_RATIO"
    
    sleep 1
done

3. Go语言PageCache测试

package main

import (
    "fmt"
    "io"
    "os"
    "os/exec"
    "time"
)

const (
    fileSize   = 100 * 1024 * 1024 // 100MB
    bufferSize = 4096
)

func createTestFile() {
    fmt.Println("创建测试文件...")
    
    file, _ := os.Create("/tmp/cache_test.dat")
    defer file.Close()
    
    buffer := make([]byte, bufferSize)
    for i := range buffer {
        buffer[i] = 'A'
    }
    
    for i := 0; i < fileSize/bufferSize; i++ {
        file.Write(buffer)
    }
    
    fmt.Println("测试文件创建完成\n")
}

func dropPageCache() {
    exec.Command("sync").Run()
    exec.Command("sh", "-c", "echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null").Run()
}

func testColdRead() {
    fmt.Println("=== 冷读测试(PageCache未命中)===")
    
    dropPageCache()
    
    start := time.Now()
    file, _ := os.Open("/tmp/cache_test.dat")
    defer file.Close()
    
    buffer := make([]byte, bufferSize)
    for {
        _, err := file.Read(buffer)
        if err == io.EOF {
            break
        }
    }
    
    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 testHotRead() {
    fmt.Println("=== 热读测试(PageCache命中)===")
    
    start := time.Now()
    file, _ := os.Open("/tmp/cache_test.dat")
    defer file.Close()
    
    buffer := make([]byte, bufferSize)
    for {
        _, err := file.Read(buffer)
        if err == io.EOF {
            break
        }
    }
    
    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 main() {
    fmt.Println("PageCache性能测试")
    fmt.Println("==================\n")
    
    createTestFile()
    testColdRead()
    testHotRead()
    
    os.Remove("/tmp/cache_test.dat")
}

动手实验

实验1:观测PageCache命中率

目标: 理解PageCache对性能的影响

步骤:

  1. 编译测试程序:
gcc -o cache_test cache_test.c
  1. 运行测试:
./cache_test
  1. 使用cachestat观测:
# 终端1:运行测试
./cache_test

# 终端2:观测命中率
sudo /usr/share/bcc/tools/cachestat 1
  1. 分析结果:
    • 冷读:大量MISSES
    • 热读:大量HITS
    • 命中率 = HITS / (HITS + MISSES)

实验2:脏页回写实验

目标: 观测脏页的回写过程

步骤:

  1. 启动脏页监控:
chmod +x dirty_page_monitor.sh
./dirty_page_monitor.sh
  1. 执行大量写入:
dd if=/dev/zero of=/tmp/bigfile bs=1M count=1000
  1. 观察脏页变化:

    • 脏页快速增长
    • 达到阈值后开始回写
    • Writeback列显示正在回写的页
  2. 手动触发同步:

sync
# 观察脏页迅速降为0

实验3:内存回收观测

目标: 观察kswapd的工作过程

步骤:

  1. 查看当前内存水位:
cat /proc/zoneinfo | grep -A 5 "pages free"
  1. 创建内存压力:
# 分配大量内存
stress-ng --vm 1 --vm-bytes 80% --vm-method all --timeout 60s
  1. 观察kswapd活动:
# 查看kswapd CPU使用
top -p $(pgrep kswapd)

# 查看内存回收统计
vmstat 1
# 观察si/so列
  1. 查看回收日志:
dmesg | grep -i "kswapd\|reclaim"

常见问题

Q1: PageCache会占用多少内存?

A: PageCache会使用所有可用的空闲内存。Linux的设计哲学是"用完所有内存",当应用需要内存时,内核会自动释放PageCache。通过free -h命令可以看到:

  • buff/cache: PageCache占用的内存
  • available: 实际可用内存(包括可释放的PageCache)

Q2: 什么时候需要手动清理PageCache?

A: 一般情况下不需要手动清理。只在以下场景考虑:

  • 性能测试时建立baseline
  • 调试内存相关问题
  • 某些特殊的性能测试场景

正常运行时不应该清理,因为PageCache是性能优化的重要手段。

Q3: 如何优化脏页回写性能?

A: 优化策略:

  • 降低dirty_ratio:减少突发写入的影响
  • 降低dirty_background_ratio:更早开始后台回写
  • 缩短dirty_writeback_centisecs:更频繁的回写
  • 使用SSD:更快的写入速度
  • 应用层优化:使用DirectIO或异步IO

Q4: swap和PageCache的关系是什么?

A: 内存回收的优先级:

  1. 先回收PageCache中的clean pages
  2. 再回收dirty pages(需要先回写)
  3. 最后才swap匿名页

可以通过vm.swappiness参数调整:

  • 0: 尽量不swap(推荐服务器)
  • 60: 默认值
  • 100: 积极swap

复习题

选择题

  1. PageCache主要缓存什么内容?

    • A. 进程代码
    • B. 文件数据
    • C. 网络数据包
    • D. CPU指令
  2. 脏页(Dirty Page)是指:

    • A. 从未使用过的页
    • B. 已修改但未写入磁盘的页
    • C. 已损坏的页
    • D. 正在使用的页
  3. kswapd是什么?

    • A. 内核模块
    • B. 系统调用
    • C. 内存回收守护进程
    • D. 文件系统
  4. vm.dirty_ratio参数控制什么?

    • A. 内存碎片比例
    • B. 脏页占总内存的最大比例
    • C. swap使用比例
    • D. CPU使用率
  5. 清理PageCache使用的命令是?

    • A. free -c
    • B. echo 3 > /proc/sys/vm/drop_caches
    • C. clear cache
    • D. rm -rf /cache

简答题

  1. 解释PageCache的读缓存和写缓存机制。

  2. 什么情况下会触发脏页回写?

  3. 描述Linux内存回收的层次和优先级。

  4. 如何诊断系统是否存在PageCache相关的性能问题?

  5. 在什么场景下应该考虑使用Direct IO绕过PageCache?

实战题

  1. 性能分析题: 系统出现周期性的性能抖动,每5秒卡顿一次。请设计分析流程并给出可能的原因和解决方案。

  2. 参数调优题: 一个数据库服务器,内存32GB,经常出现IO写入延迟。请给出PageCache相关参数的调优方案。

  3. 故障排查题: 服务器内存使用率很高,但free显示还有大量buff/cache。应用抱怨内存不足。请分析可能的原因。

扩展阅读

推荐资源

  • Linux PageCache源码
  • Page Cache, the Affair Between Memory and Files
  • Linux内存管理文档

深入方向

  • 文件系统与PageCache的交互
  • 内存回收算法演进
  • NUMA与PageCache
  • 透明大页(THP)对PageCache的影响

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

Prev
内存管理与虚拟内存
Next
文件系统与IO优化