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之前的VMAstart/end: 要解除映射的地址范围tlb: TLB(转换后备缓冲区)收集器,用于批量处理TLB刷新nr_accounted: 已记账的内存页数,用于资源统计
准备阶段
lru_add_drain();
tlb = tlb_gather_mmu(mm, 0);
lru_add_drain();- 排空LRU(最近最少使用)缓存
- 将待处理的页面添加到LRU列表中,确保后续操作能看到一致的页面状态
- 防止在解除映射过程中有页面滞留在per-CPU缓存中
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);
unmap_vmas(&tlb, mm, vma, start, end, &nr_accounted, NULL);- 核心函数:实际执行虚拟内存区域的解除映射
- 遍历指定范围内的所有VMA,清除页表项
- 收集需要刷新的TLB条目到tlb结构中
- 统计已解除映射的页面数量到
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);
is_hugepage_only_range(start, end - start))- 检查指定地址范围是否只包含大页(
Hugepage) - 大页有特殊的内存管理机制,需要特殊处理
- 检查指定地址范围是否只包含大页(
hugetlb_free_pgtables(tlb, prev, start, end);- 如果是大页区域,调用大页专用的页表释放函数
- 处理大页特有的页表结构和映射关系
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参数用于可能的架构特定优化
函数功能总结
主要功能:完整地解除指定地址区域的页表映射,并释放相关的页表数据结构
具体作用:
- 准备阶段:确保系统状态一致,初始化TLB批处理机制
- 核心操作:清除页表映射,更新内存记账信息
- 资源回收:根据页类型释放相应的页表结构
- 清理阶段:批量刷新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: 移动到下一个VMAstart = 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): 检查是否为大页VMAblock = 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): 检查是否为完整地址空间的TLBtlb_finish_mmu(): 完成当前TLB批处理,刷新TLBcond_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));
huge_pte_offset(mm, address)- 根据虚拟地址查找大页的页表项位置
- 遍历页表层次结构,找到对应的PTE指针
ptep_get_and_clear()- 原子操作:先获取当前PTE值,然后清空页表项
- 防止在获取和清除之间发生竞态条件
- 返回清除前的PTE值,用于后续操作
空页表项检查
if (pte_none(pte))
continue;
- 检查页表项是否为空(没有映射)
- 如果为空,说明这个大页已经被解除映射,跳过后续处理
continue: 直接处理下一个大页
获取物理页面并释放引用
page = pte_page(pte);
put_page(page);
page = pte_page(pte)- 从PTE中提取物理页面指针
- PTE中包含物理页帧号(PFN),转换为
struct page*
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缓存了虚拟到物理的映射,必须清除这些缓存以确保解除映射立即生效
函数功能总结
主要功能:安全、高效地解除大页内存映射,并管理相关资源
- 参数验证:确保地址范围符合大页对齐要求
- 页表清理:原子性地清除页表项,消除映射
- 资源释放:减少物理页面的引用计数,可能释放页面
- 统计更新:准确更新进程的内存使用统计(RSS)
- 缓存维护:刷新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(): 如果条件为真,触发内核bugaddress >= 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粒度遍历页表层次结构,协调解除指定虚拟地址范围的页表映射
具体作用:
- 参数验证:确保地址范围有效性
- PGD定位:找到起始的页全局目录项
- 层次遍历:按PGD边界遍历,调用下一级处理函数
- 进度控制:正确推进处理地址和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链表中位于解除映射区域之前的VMAstart/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的下一个VMAnext可能为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 first和last已调整为安全的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对齐的空闲区域识别
具体作用:
- 边界计算:将解除映射区域扩展到PGD边界
- VMA分析:扫描相邻VMA,确定实际可释放的页表范围
- 安全保护:确保不释放正在使用的页表结构
- 内存回收:实际释放PMD、PTE等页表占用的内存
- 缓存维护:刷新相关的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结构内存
浙公网安备 33010602011771号