Linux内存映射解除:页表清理与资源回收的深度剖析 - 详解

前言

在Linux的虚拟内存管理中,内存映射的解除就像是一场精密的"拆除工程"——不仅要移除表面的页表映射,还要深入清理各级页表结构,更要彻底回收所有相关资源。想象一下,当进程调用munmap或内存区域被调整时,内核需要执行一场复杂的内存"外科手术":

  • 精准定位:在多级页表层次中导航,找到需要解除的映射
  • 分层拆除:从PTE到PMD再到PGD,逐级清理页表结构
  • 资源回收:释放物理页面、清理TLB缓存、更新内存统计
  • 智能优化:区分普通页与大页,批量处理提升性能

这个过程涉及页表遍历、TLB管理、内存统计、资源释放等多个精密环节,任何疏漏都可能导致内存泄漏、性能下降甚至系统崩溃。本文将深入揭示Linux内核如何通过分层式的解除映射机制,在保证系统稳定性的同时实现高效的内存资源回收

核心架构:四级解除映射体系

1. 区域解除层 - unmap_region

// 完整的区域解除映射流程
unmap_region()
├── 准备阶段: lru_add_drain(), tlb_gather_mmu()
├── 核心解除: unmap_vmas() - 遍历VMA清除页表
├── 资源记账: vm_unacct_memory() - 更新内存统计
├── 页表释放: free_pgtables() - 回收空闲页表结构
└── 清理阶段: tlb_finish_mmu() - 批量TLB刷新

核心职责:协调整个页表解除映射流程,管理TLB批处理机制

2. VMA遍历层 - unmap_vmas

// 智能分块解除算法
for (每个VMA) {
while (VMA内分块处理) {
if (是大页) unmap_hugepage_range();  // 大页专用处理
else       unmap_page_range();      // 普通页分层处理
if (需要调度) {
tlb_finish_mmu();    // 刷新当前TLB
cond_resched_lock(); // 让出CPU,避免饥饿
tlb_gather_mmu();    // 重新开始TLB收集
}
}
}

设计亮点

  • 分块处理ZAP_BLOCK_SIZE控制锁持有时间
  • 调度友好:长时间操作中主动让出CPU
  • 类型区分:大页与普通页采用不同处理路径

3. 页表遍历层 - 四级页表解除

// 页表层次遍历解除
unmap_page_range()           // PGD粒度遍历zap_pmd_range()              // PMD粒度处理  zap_pte_range()              // PTE粒度清除ptep_get_and_clear()         // 原子性PTE操作

页表层次

  • PGD - 页全局目录:管理整个进程的地址空间
  • PMD - 页中间目录:管理大内存区域
  • PTE - 页表项:管理单个页面映射
  • TLB - 转换后备缓冲区:CPU映射缓存

4. 资源回收层 - 结构化清理

// 完整的VMA资源回收链
remove_vm_struct()
├── 文件映射清理: __remove_shared_vm_struct()
├── 驱动回调执行: vma->vm_ops->close()
├── 引用计数管理: fput(file)
├── 匿名映射清理: anon_vma_unlink()
├── 内存策略释放: mpol_free()
└── 结构内存回收: kmem_cache_free()

完整地解除指定地址区域的页表映射unmap_region

static void unmap_region(struct mm_struct *mm,
struct vm_area_struct *vma,
struct vm_area_struct *prev,
unsigned long start,
unsigned long end)
{
struct mmu_gather *tlb;
unsigned long nr_accounted = 0;
lru_add_drain();
tlb = tlb_gather_mmu(mm, 0);
unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);
vm_unacct_memory(nr_accounted);
if (is_hugepage_only_range(start, end - start))
hugetlb_free_pgtables(tlb, prev, start, end);
else
free_pgtables(tlb, prev, start, end);
tlb_finish_mmu(tlb, start, end);
}

函数功能概述

这个函数用于解除指定地址区域的页表映射,并释放相关的页表结构,是内存解除映射的核心操作

代码逐段解析

函数声明和变量定义

static void unmap_region(struct mm_struct *mm,
struct vm_area_struct *vma,
struct vm_area_struct *prev,
unsigned long start,
unsigned long end)
{
struct mmu_gather *tlb;
unsigned long nr_accounted = 0;
  • mm: 进程的内存描述符
  • vma: 要解除映射的虚拟内存区域
  • prev: 在VMA链表中位于vma之前的VMA
  • start/end: 要解除映射的地址范围
  • tlb: TLB(转换后备缓冲区)收集器,用于批量处理TLB刷新
  • nr_accounted: 已记账的内存页数,用于资源统计

准备阶段

lru_add_drain();
tlb = tlb_gather_mmu(mm, 0);
  1. lru_add_drain();

    • 排空LRU(最近最少使用)缓存
    • 将待处理的页面添加到LRU列表中,确保后续操作能看到一致的页面状态
    • 防止在解除映射过程中有页面滞留在per-CPU缓存中
  2. tlb = tlb_gather_mmu(mm, 0);

    • 初始化TLB收集器结构
    • mm: 目标内存描述符
    • 0: 标志位,表示不需要延迟TLB刷新
    • 准备批量收集需要刷新的TLB条目,提高效率

核心解除映射操作

unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);
vm_unacct_memory(nr_accounted);
  1. unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);

    • 核心函数:实际执行虚拟内存区域的解除映射
    • 遍历指定范围内的所有VMA,清除页表项
    • 收集需要刷新的TLB条目到tlb结构中
    • 统计已解除映射的页面数量到nr_accounted
    • NULL参数表示不需要详细控制结构
  2. vm_unacct_memory(nr_accounted);

    • 解除内存记账
    • 减少内核的内存使用计数,释放相应的资源配额

页表释放

if (is_hugepage_only_range(start, end - start))
hugetlb_free_pgtables(tlb, prev, start, end);
else
free_pgtables(tlb, prev, start, end);
  1. is_hugepage_only_range(start, end - start))

    • 检查指定地址范围是否只包含大页(Hugepage
    • 大页有特殊的内存管理机制,需要特殊处理
  2. hugetlb_free_pgtables(tlb, prev, start, end);

    • 如果是大页区域,调用大页专用的页表释放函数
    • 处理大页特有的页表结构和映射关系
  3. free_pgtables(tlb, prev, start, end);

    • 标准情况:释放普通页的页表结构
    • 递归释放页目录、页表等各级页表结构
    • 利用TLB收集器批量处理TLB刷新

清理阶段

tlb_finish_mmu(tlb, start, end);
  • tlb_finish_mmu(tlb, start, end);
    • 完成TLB处理,执行批量TLB刷新
    • 释放TLB收集器相关的资源
    • 确保所有解除映射操作在CPU架构层面生效
    • start, end参数用于可能的架构特定优化

函数功能总结

主要功能:完整地解除指定地址区域的页表映射,并释放相关的页表数据结构

具体作用

  1. 准备阶段:确保系统状态一致,初始化TLB批处理机制
  2. 核心操作:清除页表映射,更新内存记账信息
  3. 资源回收:根据页类型释放相应的页表结构
  4. 清理阶段:批量刷新TLB,确保操作生效

解除一段连续虚拟地址空间的页表映射unmap_vmas

int unmap_vmas(struct mmu_gather **tlbp, struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long start_addr,
unsigned long end_addr, unsigned long *nr_accounted,
struct zap_details *details)
{
unsigned long zap_bytes = ZAP_BLOCK_SIZE;
unsigned long tlb_start = 0;	/* For tlb_finish_mmu */
int tlb_start_valid = 0;
int ret = 0;
int atomic = details && details->atomic;
for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next) {
  unsigned long start;
  unsigned long end;
  start = max(vma->vm_start, start_addr);
  if (start >= vma->vm_end)
  continue;
  end = min(vma->vm_end, end_addr);
  if (end <= vma->vm_start)
    continue;
    if (vma->vm_flags & VM_ACCOUNT)
    *nr_accounted += (end - start) >> PAGE_SHIFT;
    ret++;
    while (start != end) {
    unsigned long block;
    if (!tlb_start_valid) {
    tlb_start = start;
    tlb_start_valid = 1;
    }
    if (is_vm_hugetlb_page(vma)) {
    block = end - start;
    unmap_hugepage_range(vma, start, end);
    } else {
    block = min(zap_bytes, end - start);
    unmap_page_range(*tlbp, vma, start,
    start + block, details);
    }
    start += block;
    zap_bytes -= block;
    if ((long)zap_bytes > 0)
    continue;
    if (!atomic && need_resched()) {
    int fullmm = tlb_is_full_mm(*tlbp);
    tlb_finish_mmu(*tlbp, tlb_start, start);
    cond_resched_lock(&mm->page_table_lock);
    *tlbp = tlb_gather_mmu(mm, fullmm);
    tlb_start_valid = 0;
    }
    zap_bytes = ZAP_BLOCK_SIZE;
    }
    }
    return ret;
    }

函数功能概述

这个函数是Linux内存管理的核心函数,用于解除一段连续虚拟地址空间的页表映射,支持大页处理、调度延迟优化和资源统计

代码逐段解析

函数声明和变量初始化

int unmap_vmas(struct mmu_gather **tlbp, struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long start_addr,
unsigned long end_addr, unsigned long *nr_accounted,
struct zap_details *details)
{
unsigned long zap_bytes = ZAP_BLOCK_SIZE;
unsigned long tlb_start = 0;	/* For tlb_finish_mmu */
int tlb_start_valid = 0;
int ret = 0;
int atomic = details && details->atomic;
  • tlbp: 指向TLB收集器指针的指针,用于批量TLB刷新

  • mm: 内存描述符,包含进程的所有内存信息

  • vma: 起始的VMA(虚拟内存区域)

  • start_addr/end_addr: 要解除映射的地址范围

  • nr_accounted: 输出参数,记录已记账的页面数量

  • details: 详细信息,如是否原子操作等

  • zap_bytes = ZAP_BLOCK_SIZE: 每次解除映射的块大小,用于控制锁持有时间

  • tlb_start = 0: 记录当前TLB批处理的起始地址

  • tlb_start_valid = 0: 标记tlb_start是否有效

  • ret = 0: 返回值,记录处理的VMA数量

  • atomic: 标记是否为原子操作(不允许调度)

VMA遍历循环

for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next) {
  unsigned long start;
  unsigned long end;
  start = max(vma->vm_start, start_addr);
  if (start >= vma->vm_end)
  continue;
  end = min(vma->vm_end, end_addr);
  if (end <= vma->vm_start)
    continue;
  • 遍历VMA链表,直到VMA为空或VMA起始地址超过结束地址

  • vma = vma->vm_next: 移动到下一个VMA

  • start = max(vma->vm_start, start_addr): 取VMA起始地址和请求起始地址的较大值

  • end = min(vma->vm_end, end_addr): 取VMA结束地址和请求结束地址的较小值

  • 两个continue条件确保只在有效范围内操作

资源记账

if (vma->vm_flags & VM_ACCOUNT)
*nr_accounted += (end - start) >> PAGE_SHIFT;
ret++;
  • 如果VMA设置了VM_ACCOUNT标志,表示该内存需要资源记账

  • (end - start) >> PAGE_SHIFT: 计算页面数量(字节数除以页大小)

  • 累加到nr_accounted中,用于后续资源释放

  • ret++: 统计处理的VMA数量

分块解除映射循环

while (start != end) {
unsigned long block;
if (!tlb_start_valid) {
tlb_start = start;
tlb_start_valid = 1;
}
  • 在单个VMA内分块处理,避免长时间持有锁

  • 如果是新的TLB批处理,记录起始地址

  • tlb_start_valid = 1标记已设置有效起始地址

大页与普通页处理

if (is_vm_hugetlb_page(vma)) {
block = end - start;
unmap_hugepage_range(vma, start, end);
} else {
block = min(zap_bytes, end - start);
unmap_page_range(*tlbp, vma, start, start + block, details);
}
  • is_vm_hugetlb_page(vma): 检查是否为大页VMA
  • block = end - start: 大页一次性处理整个范围
  • unmap_hugepage_range(): 大页专用的解除映射函数

普通页处理

  • block = min(zap_bytes, end - start): 取块大小和剩余范围的较小值
  • unmap_page_range(): 实际解除页表映射的核心函数
  • 使用TLB收集器批量处理TLB条目

进度更新和调度检查

start += block;
zap_bytes -= block;
if ((long)zap_bytes > 0)
continue;
  • start += block: 推进当前处理位置

  • zap_bytes -= block: 减少当前块的剩余字节数

  • 如果zap_bytes > 0,说明已经释放完成,即end - start < zap_bytes

调度和TLB重置

if (!atomic && need_resched()) {
int fullmm = tlb_is_full_mm(*tlbp);
tlb_finish_mmu(*tlbp, tlb_start, start);
cond_resched_lock(&mm->page_table_lock);
*tlbp = tlb_gather_mmu(mm, fullmm);
tlb_start_valid = 0;
}
zap_bytes = ZAP_BLOCK_SIZE;
  • !atomic && need_resched(): 非原子操作且需要调度时

  • tlb_is_full_mm(*tlbp): 检查是否为完整地址空间的TLB

  • tlb_finish_mmu(): 完成当前TLB批处理,刷新TLB

  • cond_resched_lock(): 条件调度,暂时释放页表锁

  • tlb_gather_mmu(): 重新初始化TLB收集器

  • tlb_start_valid = 0: 重置起始地址标记

  • zap_bytes = ZAP_BLOCK_SIZE: 重置为初始块大小

函数功能总结

主要功能:高效、可调度地解除一段连续虚拟地址空间的页表映射

核心特性

  • 分块处理:避免长时间持有锁,减少调度延迟
  • 大页支持:区分处理普通页和大页映射
  • TLB优化:批量处理TLB刷新,提高性能
  • 资源统计:准确统计需要记账的内存页面
  • 调度友好:在长时间操作中提供调度机会

解除大页内存映射unmap_hugepage_range

void unmap_hugepage_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long address;
pte_t pte;
struct page *page;
BUG_ON(start & (HPAGE_SIZE - 1));
BUG_ON(end & (HPAGE_SIZE - 1));
for (address = start; address < end; address += HPAGE_SIZE) {
pte = ptep_get_and_clear(huge_pte_offset(mm, address));
if (pte_none(pte))
continue;
page = pte_page(pte);
put_page(page);
}
mm->rss -= (end - start) >> PAGE_SHIFT;
flush_tlb_range(vma, start, end);
}

函数功能概述

这个函数用于解除大页(Hugepage)在指定地址范围内的页表映射,并释放相关资源

代码逐段解析

函数声明和变量定义

void unmap_hugepage_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long address;
pte_t pte;
struct page *page;
  • vma: 虚拟内存区域,包含大页映射信息

  • start: 起始虚拟地址(必须是大页对齐)

  • end: 结束虚拟地址(必须是大页对齐)

  • mm = vma->vm_mm: 从VMA获取内存描述符

  • address: 循环变量,遍历大页地址

  • pte: 页表项(Page Table Entry)

  • page: 对应的物理页面

参数验证

BUG_ON(start & (HPAGE_SIZE - 1));
BUG_ON(end & (HPAGE_SIZE - 1));
  • HPAGE_SIZE - 1: 大页大小掩码(如2MB大页为0x1FFFFF)

  • start & (HPAGE_SIZE - 1): 检查起始地址是否大页对齐

  • end & (HPAGE_SIZE - 1): 检查结束地址是否大页对齐

  • BUG_ON(): 如果不对齐,触发内核错误(严重bug)

  • 确保传入的地址范围符合大页的边界要求。

大页遍历循环

for (address = start; address < end; address += HPAGE_SIZE) {
  • address = start: 从起始地址开始
  • address < end: 遍历到结束地址
  • address += HPAGE_SIZE: 每次递增一个大页大小
  • 按大页粒度逐个处理每个大页映射

页表项获取和清除

pte = ptep_get_and_clear(huge_pte_offset(mm, address));
  1. huge_pte_offset(mm, address)

    • 根据虚拟地址查找大页的页表项位置
    • 遍历页表层次结构,找到对应的PTE指针
  2. ptep_get_and_clear()

    • 原子操作:先获取当前PTE值,然后清空页表项
    • 防止在获取和清除之间发生竞态条件
    • 返回清除前的PTE值,用于后续操作

空页表项检查

if (pte_none(pte))
continue;
  • 检查页表项是否为空(没有映射)
  • 如果为空,说明这个大页已经被解除映射,跳过后续处理
  • continue: 直接处理下一个大页

获取物理页面并释放引用

page = pte_page(pte);
put_page(page);
  1. page = pte_page(pte)

    • 从PTE中提取物理页面指针
    • PTE中包含物理页帧号(PFN),转换为struct page*
  2. put_page(page)

    • 减少页面引用计数
    • 如果引用计数降为0,页面将被释放回伙伴系统
    • 这是内存管理的关键操作,确保无人使用的页面被正确回收

更新内存统计信息

mm->rss -= (end - start) >> PAGE_SHIFT;
  • (end - start) >> PAGE_SHIFT: 计算解除映射的普通页面数量
    • 大页大小通常是普通页的整数倍(如2MB大页 = 512个4KB页面)
    • 右移PAGE_SHIFT位相当于除以页面大小
  • mm->rss -=: 减少进程的常驻内存集大小统计
  • RSS是进程实际使用的物理内存量的重要指标

刷新TLB

flush_tlb_range(vma, start, end);
  • flush_tlb_range(vma, start, end)
  • 刷新指定地址范围的TLB(转换后备缓冲区)条目
  • 必要性:CPU缓存了虚拟到物理的映射,必须清除这些缓存以确保解除映射立即生效

函数功能总结

主要功能:安全、高效地解除大页内存映射,并管理相关资源

  1. 参数验证:确保地址范围符合大页对齐要求
  2. 页表清理:原子性地清除页表项,消除映射
  3. 资源释放:减少物理页面的引用计数,可能释放页面
  4. 统计更新:准确更新进程的内存使用统计(RSS)
  5. 缓存维护:刷新TLB,确保映射变更立即生效

解除指定虚拟地址范围的页表映射unmap_page_range

static void unmap_page_range(struct mmu_gather *tlb,
struct vm_area_struct *vma, unsigned long address,
unsigned long end, struct zap_details *details)
{
pgd_t * dir;
BUG_ON(address >= end);
dir = pgd_offset(vma->vm_mm, address);
tlb_start_vma(tlb, vma);
do {
zap_pmd_range(tlb, dir, address, end - address, details);
address = (address + PGDIR_SIZE) & PGDIR_MASK;
dir++;
} while (address && (address < end));
tlb_end_vma(tlb, vma);
}

函数功能概述

这个函数是页表解除映射的核心遍历函数,负责按PGD(页全局目录)粒度遍历页表并解除指定地址范围的映射

代码逐段解析

函数声明和参数

static void unmap_page_range(struct mmu_gather *tlb,
struct vm_area_struct *vma, unsigned long address,
unsigned long end, struct zap_details *details)
{
pgd_t * dir;
  • tlb: TLB收集器,用于批量处理TLB刷新

  • vma: 虚拟内存区域,描述要解除映射的内存区域

  • address: 起始虚拟地址

  • end: 结束虚拟地址

  • details: 解除映射的详细信息和控制参数

  • dir: 指向PGD(Page Global Directory)项的指针

参数验证

BUG_ON(address >= end);
  • BUG_ON(): 如果条件为真,触发内核bug
  • address >= end: 确保起始地址小于结束地址
  • 防止传入无效的空范围或反向范围

获取PGD指针

dir = pgd_offset(vma->vm_mm, address);
  • vma->vm_mm: 从VMA获取内存描述符
  • pgd_offset(mm, address): 根据虚拟地址计算PGD项的位置

PGD遍历循环

do {
zap_pmd_range(tlb, dir, address, end - address, details);
address = (address + PGDIR_SIZE) & PGDIR_MASK;
dir++;
} while (address && (address < end));
  • 按PGD粒度遍历地址空间

  • zap_pmd_range(): 处理当前PGD项下的所有PMD(Page Middle Directory)

  • dir: 当前PGD项指针

  • address: 当前处理的起始地址

  • end - address: 剩余要处理的长度

  • details: 传递控制参数

  • address + PGDIR_SIZE: 地址增加一个PGD大小

  • & PGDIR_MASK: 对齐到下一个PGD边界

  • PGD是一个数组,dir++移动到下一项

  • 准备处理下一个PGD范围的地址

  • address: 确保地址没有回绕到0(32位系统考虑)

  • address < end: 确保没有超过结束地址

函数功能总结

主要功能:按PGD粒度遍历页表层次结构,协调解除指定虚拟地址范围的页表映射

具体作用

  1. 参数验证:确保地址范围有效性
  2. PGD定位:找到起始的页全局目录项
  3. 层次遍历:按PGD边界遍历,调用下一级处理函数
  4. 进度控制:正确推进处理地址和PGD指针

检测并释放空闲的页表结构内存free_pgtables

static void free_pgtables(struct mmu_gather *tlb, struct vm_area_struct *prev,
unsigned long start, unsigned long end)
{
unsigned long first = start & PGDIR_MASK;
unsigned long last = end + PGDIR_SIZE - 1;
unsigned long start_index, end_index;
struct mm_struct *mm = tlb->mm;
if (!prev) {
prev = mm->mmap;
if (!prev)
goto no_mmaps;
if (prev->vm_end > start) {
if (last > prev->vm_start)
last = prev->vm_start;
goto no_mmaps;
}
}
for (;;) {
struct vm_area_struct *next = prev->vm_next;
if (next) {
if (next->vm_start < start) {
prev = next;
continue;
}
if (last > next->vm_start)
last = next->vm_start;
}
if (prev->vm_end > first)
first = prev->vm_end + PGDIR_SIZE - 1;
break;
}
no_mmaps:
if (last < first)	/* for arches with discontiguous pgd indices */
return;
/*
* If the PGD bits are not consecutive in the virtual address, the
* old method of shifting the VA >> by PGDIR_SHIFT doesn't work.
*/
start_index = pgd_index(first);
if (start_index < FIRST_USER_PGD_NR)
start_index = FIRST_USER_PGD_NR;
end_index = pgd_index(last);
if (end_index > start_index) {
clear_page_tables(tlb, start_index, end_index - start_index);
flush_tlb_pgtables(mm, first & PGDIR_MASK, last & PGDIR_MASK);
}
}

函数功能概述

这个函数尝试释放空闲的页目录结构,通过智能地检测PGD对齐的空闲区域来回收页表内存,而不需要全量扫描页表

代码逐段解析

函数声明和变量初始化

static void free_pgtables(struct mmu_gather *tlb, struct vm_area_struct *prev,
unsigned long start, unsigned long end)
{
unsigned long first = start & PGDIR_MASK;
unsigned long last = end + PGDIR_SIZE - 1;
unsigned long start_index, end_index;
struct mm_struct *mm = tlb->mm;
  • tlb: TLB收集器,用于页表释放时的TLB管理

  • prev: 在VMA链表中位于解除映射区域之前的VMA

  • start/end: 刚刚解除映射的地址范围

  • first = start & PGDIR_MASK: 起始地址对齐到PGD边界

  • last = end + PGDIR_SIZE - 1: 结束地址扩展到下一个PGD边界

  • 目的是考虑PGD粒度的完整范围

前驱VMA处理

if (!prev) {
prev = mm->mmap;
if (!prev)
goto no_mmaps;
if (prev->vm_end > start) {
if (last > prev->vm_start)
last = prev->vm_start;
goto no_mmaps;
}
}
  • 处理prev为NULL的情况

  • prev = mm->mmap: 如果没有前驱VMA,从链表头部开始

  • if (!prev) goto no_mmaps: 如果整个地址空间都没有VMA,直接跳到清理

  • prev->vm_end > start: 检查第一个VMA是否与解除映射区域重叠

    • 说明释放的映射区域在第一个VMA之前
  • 如果重叠,调整last边界并直接跳到清理

VMA边界扫描循环

for (;;) {
struct vm_area_struct *next = prev->vm_next;
if (next) {
if (next->vm_start < start) {
prev = next;
continue;
}
if (last > next->vm_start)
last = next->vm_start;
}
if (prev->vm_end > first)
first = prev->vm_end + PGDIR_SIZE - 1;
break;
}
  • 这个循环的目的是找到解除映射区域前后的实际VMA边界,确定可以安全释放页表的PGD对齐范围

  • prev->vm_next: 获取当前VMA的下一个VMA

  • next可能为NULL(如果是链表末尾)

  • 检查nextVMA是否存在

  • 如果存在,需要检查它与解除映射区域的关系

情况1:next在解除映射区域之前

  • next->vm_start < start: 下一个VMA的起始地址还在解除映射区域之前
  • 这意味着我们还没有找到解除映射区域的起始位置
  • prev = next: 向前移动prev指针
  • continue: 跳过本次循环剩余代码,继续查找

情况2:next在解除映射区域之后或重叠

  • last > next->vm_start: 我们计算的上边界超过了下一个VMA的起始位置
  • last = next->vm_start: 将上边界调整到下一个VMA的起始位置
  • 防止释放下一个VMA还在使用的页表

调整下边界

  • prev->vm_end > first: 前一个VMA的结束地址超过了我们计算的下边界
  • first = prev->vm_end + PGDIR_SIZE - 1: 将下边界调整到前一个VMA结束后的下一个PGD边界
  • 确保不释放前一个VMA还在使用的页表

循环退出

  • 找到正确的位置,退出无限循环
  • 此时prev指向解除映射区域之前的最后一个VMA
  • firstlast已调整为安全的PGD边界

边界检查和索引计算

no_mmaps:
if (last < first)	/* for arches with discontiguous pgd indices */
return;
  • 如果计算出的last小于first,说明没有有效的PGD范围可清理

PGD索引计算

start_index = pgd_index(first);
if (start_index < FIRST_USER_PGD_NR)
start_index = FIRST_USER_PGD_NR;
end_index = pgd_index(last);
  • pgd_index(): 根据地址计算PGD数组索引
  • FIRST_USER_PGD_NR: 保护内核空间的PGD项不被错误释放
  • 确保只处理用户空间的页表

页表清理操作

if (end_index > start_index) {
clear_page_tables(tlb, start_index, end_index - start_index);
flush_tlb_pgtables(mm, first & PGDIR_MASK, last & PGDIR_MASK);
}
  • 检查是否存在需要清理的连续PGD范围
clear_page_tables(tlb, start_index, end_index - start_index);
  • clear_page_tables核心操作:清理指定范围内的页表结构
  • 释放PMD、PTE等下级页表占用的内存

函数功能总结

主要功能:智能检测并释放空闲的页表结构内存,基于PGD对齐的空闲区域识别

具体作用

  1. 边界计算:将解除映射区域扩展到PGD边界
  2. VMA分析:扫描相邻VMA,确定实际可释放的页表范围
  3. 安全保护:确保不释放正在使用的页表结构
  4. 内存回收:实际释放PMD、PTE等页表占用的内存
  5. 缓存维护:刷新相关的TLB条目

unmap_vma_list/unmap_vma/remove_vm_struct

/*
* Update the VMA and inode share lists.
*
* Ok - we have the memory areas we should free on the 'free' list,
* so release them, and do the vma updates.
*/
static void unmap_vma_list(struct mm_struct *mm,
struct vm_area_struct *mpnt)
{
do {
struct vm_area_struct *next = mpnt->vm_next;
unmap_vma(mm, mpnt);
mpnt = next;
} while (mpnt != NULL);
validate_mm(mm);
}
/* Normal function to fix up a mapping
* This function is the default for when an area has no specific
* function.  This may be used as part of a more specific routine.
*
* By the time this function is called, the area struct has been
* removed from the process mapping list.
*/
static void unmap_vma(struct mm_struct *mm, struct vm_area_struct *area)
{
size_t len = area->vm_end - area->vm_start;
area->vm_mm->total_vm -= len >> PAGE_SHIFT;
if (area->vm_flags & VM_LOCKED)
area->vm_mm->locked_vm -= len >> PAGE_SHIFT;
vm_stat_unaccount(area);
area->vm_mm->unmap_area(area);
remove_vm_struct(area);
}
/*
* Remove one vm structure and free it.
*/
static void remove_vm_struct(struct vm_area_struct *vma)
{
struct file *file = vma->vm_file;
might_sleep();
if (file) {
struct address_space *mapping = file->f_mapping;
spin_lock(&mapping->i_mmap_lock);
__remove_shared_vm_struct(vma, file, mapping);
spin_unlock(&mapping->i_mmap_lock);
}
if (vma->vm_ops && vma->vm_ops->close)
vma->vm_ops->close(vma);
if (file)
fput(file);
anon_vma_unlink(vma);
mpol_free(vma_policy(vma));
kmem_cache_free(vm_area_cachep, vma);
}

函数功能概述

这些函数共同完成虚拟内存区域的解除映射和资源清理工作,是进程地址空间清理的核心组件

代码逐段解析

unmap_vma_list - VMA列表遍历解除映射

static void unmap_vma_list(struct mm_struct *mm,
struct vm_area_struct *mpnt)
{
do {
struct vm_area_struct *next = mpnt->vm_next;
unmap_vma(mm, mpnt);
mpnt = next;
} while (mpnt != NULL);
validate_mm(mm);
}
  • do-while循环:确保至少执行一次
  • next = mpnt->vm_next: 预先保存下一个VMA指针,因为当前VMA将被释放

解除映射当前VMA

  • 调用核心函数解除单个VMA的映射

移动到下一个VMA

  • mpnt = next: 移动到链表中的下一个VMA
  • 循环直到处理完所有VMA

内存验证

  • 验证内存描述符的完整性

unmap_vma - 单个VMA解除映射

static void unmap_vma(struct mm_struct *mm, struct vm_area_struct *area)
{
size_t len = area->vm_end - area->vm_start;

计算VMA长度

  • 计算VMA的字节长度
  • 用于后续的资源统计更新

更新内存统计信息

area->vm_mm->total_vm -= len >> PAGE_SHIFT;
  • total_vm: 减少进程的总虚拟内存计数
  • len >> PAGE_SHIFT: 将字节长度转换为页面数量

更新锁定内存统计

if (area->vm_flags & VM_LOCKED)
area->vm_mm->locked_vm -= len >> PAGE_SHIFT;
  • VM_LOCKED: 检查VMA是否被锁定在内存中
  • locked_vm: 减少锁定的内存页面计数

虚拟内存统计解除记账

vm_stat_unaccount(area);
  • 更新各种VM统计计数器
  • 根据VMA类型(匿名、文件映射等)更新相应统计

架构特定的取消映射

area->vm_mm->unmap_area(area);
  • 调用架构特定的取消映射函数
  • 处理特定CPU架构的内存管理需求

移除VMA结构

remove_vm_struct(area);
  • 核心的VMA资源清理函数

remove_vm_struct - VMA结构资源清理

static void remove_vm_struct(struct vm_area_struct *vma)
{
struct file *file = vma->vm_file;

获取文件指针

  • 保存VMA映射的文件指针(如果是文件映射)

文件映射清理

if (file) {
struct address_space *mapping = file->f_mapping;
spin_lock(&mapping->i_mmap_lock);
__remove_shared_vm_struct(vma, file, mapping);
spin_unlock(&mapping->i_mmap_lock);
}
  • 检查VMA是否与文件关联

  • 获取文件的地址空间结构

  • 获取地址空间锁,保护共享映射结构

  • 从文件的共享映射列表中移除VMA

  • 更新反向映射数据结构

  • 释放地址空间锁

VMA操作关闭回调

if (vma->vm_ops && vma->vm_ops->close)
vma->vm_ops->close(vma);
  • 如果VMA有特定的操作函数集,调用close回调
  • 允许驱动程序或特殊映射进行清理工作

文件引用释放

if (file)
fput(file);
  • fput(file): 减少文件引用计数
  • 如果引用计数降为0,文件将被关闭

匿名VMA反向映射清理

anon_vma_unlink(vma);
  • 从匿名页的反向映射中解除链接
  • 更新匿名内存管理数据结构

内存策略清理

mpol_free(vma_policy(vma));
  • 释放与VMA关联的内存策略
  • 处理NUMA策略相关的资源

VMA结构内存释放

kmem_cache_free(vm_area_cachep, vma);
  • 将VMA结构体释放回slab分配器
  • vm_area_cachep: VMA对象的slab缓存

函数功能总结

unmap_vma_list

  • 遍历已分离的VMA链表,逐个解除映射和清理
  • 确保所有相关VMA都被正确处理
  • 最终验证内存状态完整性

unmap_vma

  • 更新内存使用统计信息
  • 处理虚拟内存记账
  • 协调架构特定的取消映射
  • 触发VMA结构清理

remove_vm_struct

  • 清理文件映射关联和共享结构
  • 调用VMA特定的关闭回调
  • 管理文件引用计数
  • 清理匿名映射反向链接
  • 释放内存策略资源
  • 最终释放VMA结构内存
posted on 2025-12-13 09:06  ljbguanli  阅读(51)  评论(0)    收藏  举报