在Linux 2.4.x内核中,如何从一个page找到所有映射该页面的VMA?反向映射可以带来哪些便利?
Linux 2.4.x内核中的反向映射详解
在Linux 2.4.x内核中,反向映射机制尚未引入(反向映射在2.6内核才正式加入)。2.4.x内核中要查找映射特定页面的VMA是相对低效的,需要通过遍历进程和页表实现。
一、在2.4.x内核中如何从page找到VMA
在Linux 2.4.x中,没有直接的方法可以从一个struct page找到所有映射它的VMA。需要遍历所有进程的内存描述符和页表:
// Linux 2.4.x内核中查找映射特定页面的VMA
void find_vmas_for_page(struct page *page, struct mm_struct **mm_list, struct vm_area_struct **vma_list)
{
struct task_struct *p;
unsigned long pfn = page_to_pfn(page);
// 遍历所有进程
for_each_process(p) {
struct mm_struct *mm = p->mm;
struct vm_area_struct *vma;
if (!mm)
continue;
// 锁定内存描述符
spin_lock(&mm->page_table_lock);
// 遍历进程的所有VMA
for (vma = mm->mmap; vma; vma = vma->vm_next) {
unsigned long addr;
pte_t *pte;
// 检查地址范围
if (!(vma->vm_flags & VM_SHARED))
continue;
// 遍历VMA内的所有页表项
for (addr = vma->vm_start; addr < vma->vm_end; addr += PAGE_SIZE) {
pte = lookup_pte(mm, addr);
if (!pte)
continue;
if (pte_present(*pte) && pfn == pte_pfn(*pte)) {
// 找到映射关系,添加到列表
add_to_list(mm_list, vma_list, mm, vma);
}
}
}
// 解锁内存描述符
spin_unlock(&mm->page_table_lock);
}
}
这段代码的几个关键问题:
性能极差:需要遍历系统中所有进程的页表
复杂锁操作:需要获取每个进程的page_table_lock
无法处理共享内存:仅能查找VM_SHARED类型的映射
二、反向映射带来的便利
当反向映射在2.6内核被引入后,带来了显著的优化:
场景 2.4.x方法 反向映射方法 优势
查找映射页面的VMA O(N^2) 遍历所有进程和页表 通过struct page->mapping直接访问 性能从O(N)提升到O(1)
页面回收 扫描所有进程的页表 直接访问反向映射列表 减少70%的页面回收延迟
共享内存管理 无法高效处理 priority search tree优化映射 提高共享内存访问效率
NUMA优化 无NUMA感知 知晓页面的所有访问位置 改进内存迁移和NUMA调度
反向映射的核心优势:
1.页面回收效率提升:在内存压力下,页面回收速度显著加快
2.页面迁移优化:透明大页迁移、NUMA负载均衡的基础
3.共享内存性能:高效处理共享库和共享内存映射
4.简化内核代码:无需复杂的遍历逻辑,减少锁竞争
三、反向映射代码案例(模拟2.6+内核)
以下是一个模拟2.6+内核反向映射机制的概念性实现:
#include <linux/mm.h>
#include <linux/rmap.h>
#include <linux/list.h>
// 定义反向映射元素结构
struct rmap_element {
struct vm_area_struct *vma; // 映射的VMA
unsigned long address; // 页面在VMA中的地址
struct list_head list; // 链表元素
};
// 将VMA添加到页面的反向映射中
int page_add_rmap(struct page *page, struct vm_area_struct *vma,
unsigned long address)
{
struct rmap_element *rme;
// 为新映射元素分配内存
rme = kmalloc(sizeof(*rme), GFP_KERNEL);
if (!rme)
return -ENOMEM;
// 初始化映射元素
rme->vma = vma;
rme->address = address;
// 添加到页面的反向映射列表
spin_lock(&page->rmap_lock);
list_add(&rme->list, &page->rmap_list);
spin_unlock(&page->rmap_lock);
return 0;
}
// 从页面移除VMA映射
void page_remove_rmap(struct page *page, struct vm_area_struct *vma,
unsigned long address)
{
struct rmap_element *rme;
struct list_head *pos, *next;
spin_lock(&page->rmap_lock);
// 遍历反向映射列表
list_for_each_safe(pos, next, &page->rmap_list) {
rme = list_entry(pos, struct rmap_element, list);
// 匹配VMA和地址
if (rme->vma == vma && rme->address == address) {
list_del(&rme->list);
kfree(rme);
break;
}
}
spin_unlock(&page->rmap_lock);
}
// 查找所有映射页面的VMA
int page_get_rmap(struct page *page, struct vm_area_struct **vmas,
int max_vmas)
{
struct rmap_element *rme;
int count = 0;
spin_lock(&page->rmap_lock);
// 遍历反向映射列表
list_for_each_entry(rme, &page->rmap_list, list) {
if (count >= max_vmas)
break;
vmas[count++] = rme->vma;
}
spin_unlock(&page->rmap_lock);
return count;
}
// 使用反向映射进行页面迁移
int migrate_page(struct page *page, struct page *new_page)
{
struct rmap_element *rme;
int ret = 0;
// 复制反向映射信息
INIT_LIST_HEAD(&new_page->rmap_list);
spin_lock(&page->rmap_lock);
list_splice_init(&page->rmap_list, &new_page->rmap_list);
spin_unlock(&page->rmap_lock);
// 更新所有映射的页表项
list_for_each_entry(rme, &new_page->rmap_list, list) {
pte_t *pte;
// 获取页表项锁
spin_lock(&rme->vma->vm_mm->page_table_lock);
// 查找页表项
pte = lookup_pte(rme->vma->vm_mm, rme->address);
if (!pte) {
spin_unlock(&rme->vma->vm_mm->page_table_lock);
ret = -EFAULT;
continue;
}
// 更新页表项指向新页面
set_pte_at(rme->vma->vm_mm, rme->address, pte,
mk_pte(new_page, pte_prot(*pte)));
spin_unlock(&rme->vma->vm_mm->page_table_lock);
}
return ret;
}
反向映射在实际操作中的流程:
1.映射添加:
进程映射页面时,调用page_add_rmap()添加反向映射项
添加到页面的rmap_list链表中
2.映射移除:
进程取消映射时,调用page_remove_rmap()移除项
从链表删除并释放元素
3.查找映射:
通过page_get_rmap()直接获取所有映射的VMA
O(1)时间复杂度
4.页面迁移:
迁移时复制反向映射信息到新页面
遍历映射更新所有进程的页表
无需扫描整个系统
四、反向映射的演进
Linux内核中反向映射的演进历史:
版本 机制 特点
2.4.x 无反向映射 通过遍历进程页表查找映射关系
2.6.0 原始反向映射 每个页面维护映射链表
2.6.11 对象化反向映射 将反向映射与地址空间关联
2.6.20 优先搜索树 高效处理共享映射
4.0+ 简化反向映射 移除不必要的数据结构
五、实际应用场景
反向映射的实际应用示例:
1.页面回收:
static int shrink_page_list(struct list_head *page_list)
{
struct page *page;
while (!list_empty(page_list)) {
page = list_entry(page_list->prev, struct page, lru);
// 1. 检查页面是否被映射
if (page_mapped(page)) {
struct rmap_element *rme;
// 2. 解锁操作:清除所有映射
list_for_each_entry(rme, &page->rmap_list, list) {
ptep_clear_flush(rme->vma, rme->address,
pte_offset_map(rme->vma->vm_mm, rme->address));
}
}
// 3. 回收页面
free_unref_page(page);
}
}
2.大页迁移:
int migrate_pages(struct list_head *pages)
{
struct page *page, *next;
list_for_each_entry_safe(page, next, pages, lru) {
// 1. 分配新页面
struct page *new_page = alloc_page(GFP_HIGHUSER);
// 2. 复制内容
copy_page(page_address(new_page), page_address(page));
// 3. 使用反向映射迁移
migrate_page(page, new_page);
// 4. 释放原页面
__free_page(page);
}
}
六、总结
2.4.x的限制:
缺乏反向映射机制
查找映射关系需要遍历所有进程和页表
性能差,特别在大内存系统上
反向映射的价值:
极大优化了页面回收效率
为内存迁移、共享内存和NUMA优化提供基础
减少锁竞争,简化内核代码逻辑
内核演进的启示:
反向映射是Linux内存管理的重要创新
解决了可扩展性问题,为支持TB级内存铺平道路
展示了内核数据结构优化对系统整体性能的影响
通过理解反向映射的演变,我们可以更好地把握Linux内存管理的设计哲学,以及为现代大型系统设计高效数据结构的重要性。