Linux 系统内存地址映射(四):Linux 内核缺页异常处理机制源码分析

在前面的文章中,我们从理论层面深入探讨了 Linux 系统虚拟地址映射的基本原理和内核数据结构。本文将聚焦于内核源码,详细解析缺页异常处理的核心逻辑 ——do_page_fault()函数。通过分析这一关键函数,我们将揭示 Linux 内核如何处理内存访问缺失的情况,以及如何动态建立虚拟地址到物理地址的映射关系。

一、缺页异常的触发与处理流程

当 CPU 访问一个尚未映射到物理内存的虚拟地址时,MMU 会触发一个缺页异常(Page Fault)。Linux 内核通过异常处理机制捕获该异常,并调用do_page_fault()函数进行处理。这个过程可以概括为:

  1. MMU 地址转换失败:CPU 尝试访问虚拟地址,但对应的页表项不存在或无效
  2. 异常向量跳转:硬件跳转到预定义的缺页异常处理向量
  3. 内核态切换:CPU 从用户态切换到内核态
  4. 执行异常处理函数:调用do_page_fault()处理缺页情况

缺页异常处理函数入口:do_page_fault()

下面我们从内核源码出发,分析do_page_fault()的核心逻辑。该函数位于内核源码的arch/x86/mm/fault.c文件中:

/*
 * 缺页异常处理的主函数
 * error_code: 硬件提供的错误码,包含异常类型信息
 * address: 触发缺页异常的虚拟地址(通过CR2寄存器读取)
 */
asmlinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    struct task_struct *tsk = current;  // 获取当前进程的task_struct
    struct mm_struct *mm = tsk->mm;     // 获取进程的内存描述符
    unsigned long address = read_cr2(); // 从CR2寄存器读取引发异常的虚拟地址
    struct vm_area_struct *vma;
    int fault;

    // 1. 查找该虚拟地址所属的VMA(vm_area_struct),通常是通过红黑树查找,在更早版本可能是通过AVL树查找
    vma = find_vma(mm, address);
    
    // 2. 检查VMA是否存在
    if (!vma)
        goto bad_area;//不存在抛出段错误

    // 3. address合法,但不是栈上地址(因为栈上address <=  vm_start)
    if (vma->vm_start <= address)
        goto good_area;

    // 4. address合法,且是栈上地址,但没有VM_GROWSDOWN栈增长权限
    if (!(vma->vm_flags & VM_GROWSDOWN))
        goto bad_area;
        
    // 执行栈增长操作
    if (unlikely(expand_stack(vma, address)))
        goto bad_area;

good_area:
    // 5. 检查访问权限是否匹配
    if (unlikely(access_error(error_code, vma))) {
        bad_area_access_error(error_code, address, vma);
        return;
    }

    // 6. 处理页错误(核心映射逻辑) 这是下一步的核心函数
    fault = handle_mm_fault(mm, vma, address, flags);
    
    // 7. 处理错误情况
    if (unlikely(fault & VM_FAULT_ERROR)) {
        if (fault & VM_FAULT_OOM)
            pagefault_out_of_memory();
        else if (fault & VM_FAULT_SIGBUS)
            __do_sigbus(regs, error_code, address);
        else if (fault & VM_FAULT_SIGSEGV)
            __do_sigsegv(regs, error_code, address);
        return;
    }
}

二、关键步骤解析

1. 查找虚拟地址所属的 VMA

find_vma()函数负责在进程的虚拟地址空间中查找包含指定地址的 VMA(虚拟内存区域)。该函数优先使用红黑树进行高效查找:

/*
 * 在mm的VMA树中查找第一个vm_end > addr的VMA
 */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
    struct rb_node *rb_node;
    struct vm_area_struct *vma;

    // 先尝试使用缓存的VMA(利用局部性原理)
    vma = mm->mmap_cache;
    if (vma && vma->vm_end > addr && vma->vm_start <= addr)
        return vma;

    // 从红黑树根节点开始查找
    rb_node = mm->mm_rb.rb_node;
    vma = NULL;

    while (rb_node) {
        struct vm_area_struct *tmp;
        tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);

        if (tmp->vm_end > addr) {
            vma = tmp;
            if (tmp->vm_start <= addr)
                break;
            rb_node = rb_node->rb_left;
        } else
            rb_node = rb_node->rb_right;
    }

    // 更新VMA缓存
    if (vma)
        mm->mmap_cache = vma;
        
    return vma;
}

2. 处理栈增长

当访问的地址在 VMA 起始地址之前,且该 VMA 具有VM_GROWSDOWN标志时,内核会尝试动态扩展栈空间:

/*
 * 扩展用户栈
 */
static int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
    struct mm_struct *mm = vma->vm_mm;
    unsigned long new_start;
    int error;

    // 计算新的栈起始地址(按页面对齐)
    new_start = PAGE_ALIGN(address);

    // 检查栈深度限制
    if (new_start < mm->start_stack - STACK_GROWTH_LIMIT)
        return -ENOMEM;

    // 检查内存使用限制
    if (mm->total_vm >= rlimit(RLIMIT_STACK) >> PAGE_SHIFT)
        return -ENOMEM;

    // 扩展VMA范围
    error = vma_merge(mm, NULL, new_start, vma->vm_end,
                      vma->vm_flags, NULL, NULL, NULL);
    if (error)
        return error;

    // 更新统计信息
    mm->total_vm += (vma->vm_start - new_start) >> PAGE_SHIFT;
    return 0;
}

3. 权限检查

access_error()函数检查当前访问操作是否符合 VMA 的权限设置:

/*
 * 检查访问是否违反VMA权限
 */
static inline int access_error(unsigned long error_code,
                               struct vm_area_struct *vma)
{
    // 检查是否为写操作但VMA不可写
    if (error_code & PF_WRITE) {
        if (!(vma->vm_flags & VM_WRITE))
            return 1;
    }
    // 检查是否为执行操作但VMA不可执行
    else if (error_code & PF_EXEC) {
        if (!(vma->vm_flags & VM_EXEC))
            return 1;
    }
    // 检查是否为用户态访问但VMA不可用户访问
    else if (!(error_code & PF_USER)) {
        if (!(vma->vm_flags & VM_MAYREAD))
            return 1;
    }
    
    return 0;
}

三、页表映射的核心实现:handle_mm_fault()

handle_mm_fault()函数负责完成从虚拟地址到物理地址的映射建立过程,它是缺页处理的核心,32 位系统中这个函数主要处理页目录(顶级页表),如果pde(页目录项)->pt(页表)不存在,则创建,创建好,根据address中间10位,得到页表项,64位系统是四级页表,但原理类似,以下代码为64 位内核:

/*
 * 处理内存页错误,建立虚拟地址到物理地址的映射
 */
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                    unsigned long address, unsigned int flags)
{
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    spinlock_t *ptl;
    int ret;

    // 1. 获取页目录项(PGD)
    pgd = pgd_offset(mm, address);
    
    // 2. 获取P4D项(仅在64位系统中存在,32位系统中跳过)
    p4d = p4d_offset(pgd, address);
    
    // 3. 获取PUD项(仅在大内存系统中存在,32位系统中跳过)
    pud = pud_offset(p4d, address);
    
    // 4. 获取PMD项(页中间目录)
    pmd = pmd_offset(pud, address);
    
    // 5. 处理PMD项(可能需要分配新的页表)
    if (pmd_none(*pmd)) {
        if (pmd_alloc(mm, pud, address))
            return VM_FAULT_OOM;
    }
    
    // 6. 处理PMD保护错误等特殊情况
    if (pmd_trans_huge(*pmd)) {
        // 处理巨型页(Huge Page)情况
        return handle_huge_pmd_fault(mm, vma, address, pmd, flags);
    }
    
    // 7. 获取PTE(页表项)
    pte = pte_offset_map_lock(mm, pmd, address, &ptl);
    
    // 8. 处理PTE错误(核心映射逻辑)
    ret = handle_pte_fault(mm, vma, address, pte, ptl, flags);
    
    // 9. 释放锁并解除映射
    pte_unmap_unlock(pte, ptl);
    
    return ret;
}

四、页表项处理:handle_pte_fault()

handle_pte_fault()函数是建立物理页映射的最后一步,它根据不同的情况处理页表项:

/*
 * 处理页表项错误,建立最终的物理页映射
 */
static int handle_pte_fault(struct mm_struct *mm,
                           struct vm_area_struct *vma,
                           unsigned long address, pte_t *pte,
                           spinlock_t *ptl, unsigned int flags)
{
    pte_t entry;
    struct page *page;
    int fault;
    int write = flags & FAULT_FLAG_WRITE;

    // 1. 检查页表项是否存在
    if (!pte_present(*pte)) {
        // 页表项不存在,处理页面不在内存的情况
        fault = handle_pte_missing(mm, vma, address, pte, ptl, flags);
        return fault;
    }

    // 2. 检查是否为写操作但页表项只读
    if (write && !pte_write(*pte)) {
        // 处理写保护错误(COW机制)
        fault = handle_pte_wrprotect(mm, vma, address, pte, ptl, flags);
        return fault;
    }

    // 3. 处理其他特殊情况(如页表项被标记为脏页、访问过等)
    entry = *pte;
    
    // 更新页表项的访问和脏标志
    if (pte_dirty(entry) && !(flags & FAULT_FLAG_WRITE))
        entry = pte_mkold(entry);
        
    if (pte_young(entry))
        entry = pte_mkold(entry);
    else
        entry = pte_mkyoung(entry);
        
    if (flags & FAULT_FLAG_WRITE)
        entry = pte_mkdirty(entry);
        
    // 更新页表项
    pte_set(pmd, pte, entry);
    
    return 0;
}

五、处理页面缺失:handle_pte_missing()

当发现页表项不存在时,handle_pte_missing()函数负责分配物理页面并建立映射:

/*
 * 处理页表项缺失的情况
 */
static int handle_pte_missing(struct mm_struct *mm,
                             struct vm_area_struct *vma,
                             unsigned long address, pte_t *pte,
                             spinlock_t *ptl, unsigned int flags)
{
    struct page *page;
    pte_t entry;
    int ret;

    // 1. 检查是否为匿名映射(没有对应文件的内存区域,如堆、栈)
    if (vma->vm_flags & VM_ANON) {
        // 分配一个新的物理页(匿名映射)
        page = alloc_zeroed_user_highpage_movable(vma, address);
        if (!page)
            return VM_FAULT_OOM;
            
        // 建立页表映射
        entry = mk_pte(page, vma->vm_page_prot);
        entry = pte_mkwrite(pte_mkdirty(entry));
        
        // 设置页表项
        set_pte_at(mm, address, pte, entry);
        
        // 更新统计信息
        page_add_new_anon_rmap(page, vma, address, false);
        lru_cache_add_active_or_unevictable(page, vma);
        
        return 0;
    }
    
    // 2. 处理文件映射情况
    if (vma->vm_file) {
        // 从文件中读取数据到物理页
        ret = filemap_fault(vma, address, pte, flags);
        return ret;
    }
    
    // 3. 处理其他情况(如缺页中断处理程序未正确设置)
    return VM_FAULT_SIGBUS;
}

六、总结

通过深入分析do_page_fault()函数及其相关调用链,我们揭示了 Linux 内核处理缺页异常的完整流程:

  1. 异常捕获:MMU 在地址转换失败时触发缺页异常
  2. VMA 查找:通过红黑树快速定位虚拟地址所属的 VMA
  3. 权限检查:验证访问权限是否符合 VMA 定义
  4. 页表遍历:从 PGD 开始,逐级查找或创建页表项
  5. 物理页分配:在页表项缺失时,分配物理页面并建立映射
  6. 特殊情况处理:如写时复制(COW)、巨型页等

这种设计使得 Linux 内核能够高效处理内存访问缺失的情况,实现虚拟内存的动态扩展和管理。理解这一机制对于优化应用程序内存使用、调试内存相关问题,以及深入理解操作系统工作原理都具有重要意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值