前言:为什么需要页表清理?
想象一下,当一个进程退出时,它在内存中留下了大量的"足迹"------页表。就像图书馆关门时需要整理所有被翻乱的书籍一样,操作系统需要清理这些页表结构。但直接清理会带来性能问题:每次释放一个页表就刷新TLB(Translation Lookaside Buffer,地址转换缓存),就像每整理一本书就重新摆放整个书架一样低效。
Linux内核通过递归清理 和TLB批量优化 两大机制,巧妙地解决了这个问题。本文将深入解析clear_page_tables和pte_free_tlb这两个关键函数,揭示Linux如何高效优雅地完成这项内存清理工作
核心工作原理:分层清理 + 批量刷新
层次化清理架构
Linux采用多级页表结构,清理时遵循自顶向下的递归策略:
c
// 清理流程示意图
clear_page_tables() // 启动清理
↓
free_one_pgd() // 清理PGD层级
↓
free_one_pmd() // 清理PMD层级
↓
pte_free_tlb() // 最终PTE释放
TLB批量优化机制
TLB是CPU的地址转换缓存,直接清理页表会导致TLB中的缓存项失效。Linux的优化策略是:
SMP系统(多核处理器):
- 批量收集:累积506个待释放页表后再统一处理
- 延迟刷新:减少TLB刷新次数,提升性能
UP系统(单核处理器):
- 立即释放:无需复杂同步,直接释放
- 简单高效:阈值设为1,每次立即处理
c
// SMP vs UP 策略对比
#ifdef CONFIG_SMP
#define FREE_PTE_NR 506 // 批量处理506个
#else
#define FREE_PTE_NR 1 // 立即处理
#endif
技术亮点解析
完整性检查机制
每个层级清理前都进行健壮性检查:
c
if (unlikely(pmd_bad(*dir))) {
pmd_ERROR(*dir); // 报告错误
pmd_clear(dir); // 安全清空
return;
}
精确资源统计
实时更新内存使用统计:
c
dec_page_state(nr_page_table_pages); // 更新系统统计
tlb->mm->nr_ptes--; // 更新进程统计
实际应用场景
这套机制在以下场景中发挥关键作用:
- 进程退出:清理整个进程的地址空间
- 内存回收:释放不再使用的页表内存
- 地址空间调整:修改进程内存映射时清理旧页表
设计哲学启示
Linux页表清理机制体现了优秀软件设计的核心原则:
- 分层抽象:复杂问题分解为简单子问题
- 批量处理:小操作合并为大操作提升效率
- 自适应优化:不同环境采用不同策略
- 健壮性优先:异常情况安全处理
递归清理进程的页表层次结构clear_page_tables
c
static inline void free_one_pmd(struct mmu_gather *tlb, pmd_t * dir)
{
struct page *page;
if (pmd_none(*dir))
return;
if (unlikely(pmd_bad(*dir))) {
pmd_ERROR(*dir);
pmd_clear(dir);
return;
}
page = pmd_page(*dir);
pmd_clear(dir);
dec_page_state(nr_page_table_pages);
tlb->mm->nr_ptes--;
pte_free_tlb(tlb, page);
}
static inline void free_one_pgd(struct mmu_gather *tlb, pgd_t * dir)
{
int j;
pmd_t * pmd;
if (pgd_none(*dir))
return;
if (unlikely(pgd_bad(*dir))) {
pgd_ERROR(*dir);
pgd_clear(dir);
return;
}
pmd = pmd_offset(dir, 0);
pgd_clear(dir);
for (j = 0; j < PTRS_PER_PMD ; j++)
free_one_pmd(tlb, pmd+j);
pmd_free_tlb(tlb, pmd);
}
void clear_page_tables(struct mmu_gather *tlb, unsigned long first, int nr)
{
pgd_t * page_dir = tlb->mm->pgd;
page_dir += first;
do {
free_one_pgd(tlb, page_dir);
page_dir++;
} while (--nr);
}
代码功能概述
这段代码用于清理进程的页表结构
代码逐段解析
free_one_pmd 函数 - 释放PMD页表
c
static inline void free_one_pmd(struct mmu_gather *tlb, pmd_t * dir)
{
struct page *page;
tlb:TLB收集器,用于批量处理页表释放dir:指向PMD(Page Middle Directory)页表项的指针
c
if (pmd_none(*dir))
return;
- 检查PMD是否为空 :
pmd_none(*dir)检查PMD表项是否未使用 - 如果是空的,直接返回,无需处理
c
if (unlikely(pmd_bad(*dir))) {
pmd_ERROR(*dir);
pmd_clear(dir);
return;
}
- 检查损坏的PMD :
pmd_bad(*dir)检测异常的PMD表项 pmd_ERROR(*dir):打印错误信息和堆栈跟踪pmd_clear(dir):清空异常的PMD表项- 返回,不继续处理损坏的表项
c
page = pmd_page(*dir);
pmd_clear(dir);
- 获取物理页 :
pmd_page(*dir)从PMD表项中提取对应的物理页框 - 清空PMD :
pmd_clear(dir)将PMD表项标记为空
c
dec_page_state(nr_page_table_pages);
tlb->mm->nr_ptes--;
- 更新统计信息 :
dec_page_state(nr_page_table_pages):减少系统页表页计数tlb->mm->nr_ptes--:减少进程的页表项计数
c
pte_free_tlb(tlb, page);
}
- 释放PTE页表 :
pte_free_tlb(tlb, page)将PTE页表页添加到TLB收集器,稍后批量释放
free_one_pgd 函数 - 释放PGD页表
c
static inline void free_one_pgd(struct mmu_gather *tlb, pgd_t * dir)
{
int j;
pmd_t * pmd;
tlb:TLB收集器dir:指向PGD(Page Global Directory)页表项的指针
c
if (pgd_none(*dir))
return;
- 检查PGD是否为空:如果PGD表项未使用,直接返回
c
if (unlikely(pgd_bad(*dir))) {
pgd_ERROR(*dir);
pgd_clear(dir);
return;
}
- 检查损坏的PGD:处理异常的PGD表项
- 打印错误、清空表项、返回
c
pmd = pmd_offset(dir, 0);
pgd_clear(dir);
- 获取PMD表 :
pmd_offset(dir, 0)从PGD表项获取对应的PMD表起始地址 - 清空PGD :
pgd_clear(dir)标记PGD表项为空
c
for (j = 0; j < PTRS_PER_PMD ; j++)
free_one_pmd(tlb, pmd+j);
- 遍历释放所有PMD:循环处理PMD表中的每个表项
PTRS_PER_PMD:每个PGD表中的表项数量(通常是512)
c
pmd_free_tlb(tlb, pmd);
}
- 释放PMD页表 :
pmd_free_tlb(tlb, pmd)将PMD页表页添加到TLB收集器
clear_page_tables 函数 - 主清理函数
c
void clear_page_tables(struct mmu_gather *tlb, unsigned long first, int nr)
{
pgd_t * page_dir = tlb->mm->pgd;
tlb:TLB收集器first:起始的PGD索引nr:要清理的PGD表项数量
c
page_dir += first;
- 定位起始PGD:将PGD指针移动到指定的起始位置
c
do {
free_one_pgd(tlb, page_dir);
page_dir++;
} while (--nr);
}
- 循环清理PGD表项 :
- 对每个PGD表项调用
free_one_pgd - 移动指针到下一个PGD表项
- 递减计数器,直到处理完指定数量的表项
- 对每个PGD表项调用
详细技术说明
页表检查宏
c
// 检查表项是否未使用
pgd_none(*dir) // PGD为空
pmd_none(*dir) // PMD为空
// 检查表项是否损坏
pgd_bad(*dir) // PGD异常
pmd_bad(*dir) // PMD异常
// 清空表项
pgd_clear(dir) // 清空PGD
pmd_clear(dir) // 清空PMD
页表提取宏
c
pmd_offset(dir, 0) // 从PGD获取PMD表地址
pmd_page(*dir) // 从PMD获取PTE页表物理页
内存统计管理
c
dec_page_state(nr_page_table_pages) // 系统全局页表页计数
tlb->mm->nr_ptes-- // 进程私有页表项计数
函数功能总结
核心功能:递归清理进程的页表层次结构
主要作用:
- 层次化清理:按照PGD→PMD→PTE的层次递归释放页表
- 完整性检查:检测并处理损坏的页表项
- 资源统计:准确更新页表使用统计信息
- 性能优化:通过TLB收集器批量处理释放操作
页表释放的TLB优化机制pte_free_tlb
c
#define pte_free_tlb(tlb, ptep) \
do { \
tlb->need_flush = 1; \
__pte_free_tlb(tlb, ptep); \
} while (0)
#define __pte_free_tlb(tlb,pte) tlb_remove_page((tlb),(pte))
/* tlb_remove_page
* Must perform the equivalent to __free_pte(pte_get_and_clear(ptep)), while
* handling the additional races in SMP caused by other CPUs caching valid
* mappings in their TLBs.
*/
static inline void tlb_remove_page(struct mmu_gather *tlb, struct page *page)
{
tlb->need_flush = 1;
if (tlb_fast_mode(tlb)) {
free_page_and_swap_cache(page);
return;
}
tlb->pages[tlb->nr++] = page;
if (tlb->nr >= FREE_PTE_NR)
tlb_flush_mmu(tlb, 0, 0);
}
/*
* For UP we don't need to worry about TLB flush
* and page free order so much..
*/
#ifdef CONFIG_SMP
#define FREE_PTE_NR 506
#define tlb_fast_mode(tlb) ((tlb)->nr == ~0U)
#else
#define FREE_PTE_NR 1
#define tlb_fast_mode(tlb) 1
#endif
代码功能概述
这段代码实现了页表释放的TLB优化机制,通过批量处理和延迟刷新来提高内存管理性能
代码逐段解析
pte_free_tlb 宏定义
c
#define pte_free_tlb(tlb, ptep) \
do { \
tlb->need_flush = 1; \
__pte_free_tlb(tlb, ptep); \
} while (0)
do { ... } while (0):创建多语句宏的标准做法,确保在使用时像单个语句一样工作
c
tlb->need_flush = 1;
- 设置刷新标志:标记TLB需要刷新,因为页表即将被释放
- 这确保后续会执行TLB刷新操作
c
__pte_free_tlb(tlb, ptep);
- 调用实际释放函数:转发到真正的释放实现
__pte_free_tlb 宏定义
c
#define __pte_free_tlb(tlb,pte) tlb_remove_page((tlb),(pte))
- 简单转发 :直接将调用转发给
tlb_remove_page函数 - 这里
pte实际上是物理页框(page结构),而不是PTE表项
tlb_remove_page 函数核心实现
c
static inline void tlb_remove_page(struct mmu_gather *tlb, struct page *page)
{
tlb->need_flush = 1;
- 再次设置刷新标志:确保TLB刷新标志被设置
- 这个重复设置是为了代码的健壮性
c
if (tlb_fast_mode(tlb)) {
free_page_and_swap_cache(page);
return;
}
- 快速模式检查 :
tlb_fast_mode(tlb)检查是否处于快速模式 - 立即释放 :如果是快速模式,直接调用
free_page_and_swap_cache(page)释放页面 - 返回:快速模式下立即返回,不进行批量处理
c
tlb->pages[tlb->nr++] = page;
- 添加到批量数组:将页面指针添加到TLB收集器的页面数组中
tlb->nr++:增加计数,使用后递增
c
if (tlb->nr >= FREE_PTE_NR)
tlb_flush_mmu(tlb, 0, 0);
- 批量刷新检查 :如果收集的页面数量达到阈值
FREE_PTE_NR - 执行批量刷新 :调用
tlb_flush_mmu刷新TLB并释放所有收集的页面
SMP和UP的不同配置
c
#ifdef CONFIG_SMP
#define FREE_PTE_NR 506
#define tlb_fast_mode(tlb) ((tlb)->nr == ~0U)
#else
#define FREE_PTE_NR 1
#define tlb_fast_mode(tlb) 1
#endif
SMP(对称多处理)配置:
c
#define FREE_PTE_NR 506
- 批量阈值:在SMP系统中,批量处理506个页面
c
#define tlb_fast_mode(tlb) ((tlb)->nr == ~0U)
- 快速模式判断 :当
tlb->nr == ~0U(最大值)时启用快速模式 ~0U是32位无符号整数的最大值(0xFFFFFFFF)- 在SMP中,快速模式是特殊情况
UP(单处理器)配置:
c
#define FREE_PTE_NR 1
- 立即释放:在UP系统中,阈值设为1,意味着每次立即释放
c
#define tlb_fast_mode(tlb) 1
- 总是快速模式:在UP系统中总是使用快速模式
- 因为单处理器不需要复杂的TLB同步
详细技术说明
TLB刷新必要性
当页表被释放后,对应的虚拟到物理映射就无效了。但CPU的TLB中可能还缓存着这些旧的映射。如果不刷新TLB,可能导致:
- 访问已释放内存:通过旧的TLB项访问已释放的物理页
- 安全漏洞:可能访问到重新分配给其他用途的敏感数据
- 数据损坏:错误的写入操作破坏其他数据
函数功能总结
核心功能:优化页表页面的释放过程,通过TLB收集器实现高效的批量处理
主要作用:
- TLB管理:标记需要刷新的TLB项,确保内存一致性
- 性能优化:通过批量处理减少SMP系统中的同步开销
- 资源释放:安全释放不再使用的页表页面
- 模式自适应:根据SMP/UP配置采用不同的优化策略