Linux内核内存管理-内存访问与缺页中断

简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断的具体处理流程。先介绍了用户态进程的执行流程,然后对比了内核线程,引入高端内存的概念,最后分析了缺页中断的流程。

  • 用户进程
    fork之后的用户态进程已经建立好了所需的数据结构,比如task struct,thread info,mm struct等,将编译链接好的可执行程序的地址区域与进程结构中内存区域做好映射,等开始执行的时候,访问并未经过映射的用户地址空间,会发生缺页中断,然后内核态的对应中断处理程序负责分配page,并将用户进程空间导致缺页的地址与page关联,然后检查是否有相同程序文件的buffer,因为可能其他进程执行同一个程序文件,已经将程序读到buffer里边了,如果没有,则将磁盘上的程序部分读到buffer,而buffer head通常是与分配的页面相关联的,所以实际上会读到对应页面代表的物理内存之中,返回到用户态导致缺页的地址继续执行,此时经过mmu的翻译,用户态地址成功映射到对应页面和物理地址,然后读取指令执行。在上述过程中,如果由于内存耗尽或者权限的问题,可能会返回-NOMEM或segment fault错误给用户态进程。
  • 内核线程
    没有独立的mm结构,所有内核线程共享一个内核地址空间与内核页表,由于为了方便系统调用等,在用户态进程规定了内核的地址空间是高1G的线性地址,而低3G线性地址空间供用户态使用。注意这部分是和用户态进程的线性地址是重合的,经过mmu的翻译,会转换到相同的物理地址,即前1G的物理地址(准确来讲后128M某些部分的物理地址可能会变化),内核线程访问内存也是要经过mmu的,所以借助用户态进程的页表,虽然内核有自己的内核页表,但不直接使用(为了减少用户态和内核态页表切换的消耗?),用户进程页表的高1G部分实际上是共享内核页表的映射的,访问高1G的线性地址时能访问到低1G的物理地址。而且,由于从用户进程角度看,内核地址空间只有3G-4G这一段(内核是无法直接访问0-3G的线性地址空间的,因为这一段是用户进程所有,一方面如果内核直接读写0-3G的线性地址可能会毁坏进程数据结构,另一方面,不同用户态进程线性地址空间实际映射到不同的物理内存地址,所以可能此刻内核线程借助这个用户态进程的页表成功映射到某个物理地址,但是到下一刻,借助下一个用户态进程的页表,相同的线性地址就可能映射到不同的物理内存地址了)。
  • 高端内存
    那么,如何让内核访问到大于1G的物理内存?由此引入高端内存的概念,基本思路就是将3G-4G这1G的内核线性地址空间(从用户进程的角度看,从内核线程的角度看是0-1G)取出一部分挪作他用,而不是固定映射,即重用部分内核线性地址空间,映射到1G之上的物理内存。所以,对于x86 32位体系上的Linux内核将3G-4G的线性地址空间分为0-896m和896m-1G的部分,前面部分使用固定映射,当内核使用进程页表访问3G-3G+896m的线性地址时,不会发生缺页中断,但是当访问3G+896m以上的线性地址时,可能由于内核页表被更新,而进程页表还未和内核页表同步,此时会发生内核地址空间的缺页中断,从而将内核页表同步到当前进程页表。注意,使用vmalloc分配内存的时候,可能已经设置好了内核页表,等到下一次借助进程页表访问内核空间地址发生缺页时才会触发内核页表和当前页表的同步。
    Linux x86 32位下的线性地址空间与物理地址空间
    (图片出自《understanding the linux virtual memory manager》)
  • 缺页
    page fault的处理过程如下:在用户空间上下文和内核上下文下都可能访问缺页的线性地址导致缺页中断,但有些情况没有实际意义。

    • 如果缺页地址位于内核线性地址空间
      • 如果在vmalloc区,则同步内核页表和用户进程页表,否则挂掉。注意此处未分具体上下文
    • 如果发生在中断上下文或者!mm,则检查exception table,如果没有则挂掉。
    • 如果缺页地址发生在用户进程线性地址空间
      • 如果在内核上下文,则查exception table,如果没有,则挂掉。这种情况没多大实际意义
      • 如果在用户进程上下文
        • 查找vma,找到,先判断是否需要栈扩张,否则进入通常的处理流程
        • 查找vma,未找到,bad area,通常返回segment fault

    具体的缺页中断流程图及代码如下:
    (图片出自《understanding the linux virtual memory manager》)

(Linux 3.19.3 arch/x86/mm/fault.c 1044)
/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 *
 * This function must have noinline because both callers
 * {,trace_}do_page_fault() have notrace on. Having this an actual function
 * guarantees there's a function trace entry.
 */

//处理缺页中断
//参数:寄存器值,错误码,缺页地址
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
        unsigned long address)
{
    struct vm_area_struct *vma;
    struct task_struct *tsk;
    struct mm_struct *mm;
    int fault, major = 0;
    unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

    tsk = current;
    mm = tsk->mm;

    /*
     * Detect and handle instructions that would cause a page fault for
     * both a tracked kernel page and a userspace page.
     */
    if (kmemcheck_active(regs))
        kmemcheck_hide(regs);
    prefetchw(&mm->mmap_sem);

    if (unlikely(kmmio_fault(regs, address)))
        return;

    /*
     * We fault-in kernel-space virtual memory on-demand. The
     * 'reference' page table is init_mm.pgd.
     *
     * NOTE! We MUST NOT take any locks for this case. We may
     * be in an interrupt or a critical region, and should
     * only copy the information from the master page table,
     * nothing more.
     *
     * This verifies that the fault happens in kernel space
     * (error_code & 4) == 0, and that the fault was not a
     * protection error (error_code & 9) == 0.
     */

    //如果缺页地址位于内核空间
    if (unlikely(fault_in_kernel_space(address))) {
        if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) { //位于内核上下文
            if (vmalloc_fault(address) >= 0) //如果位于vmalloc区域 vmalloc_sync_one同步内核页表进程页表
                return;

            if (kmemcheck_fault(regs, address, error_code))
                return;
        }

        /* Can handle a stale RO->RW TLB: */
        if (spurious_fault(error_code, address))
            return;

        /* kprobes don't want to hook the spurious faults: */
        if (kprobes_fault(regs))
            return;
        /*
         * Don't take the mm semaphore here. If we fixup a prefetch
         * fault we could otherwise deadlock:
         */
        bad_area_nosemaphore(regs, error_code, address);

        return;
    }

    /* kprobes don't want to hook the spurious faults: */
    if (unlikely(kprobes_fault(regs)))
        return;

    if (unlikely(error_code & PF_RSVD))
        pgtable_bad(regs, error_code, address);

    if (unlikely(smap_violation(error_code, regs))) {
        bad_area_nosemaphore(regs, error_code, address);
        return;
    }

    /*
     * If we're in an interrupt, have no user context or are running
     * in an atomic region then we must not take the fault:
     */

    //如果位于中断上下文或者!mm, 出错
    if (unlikely(in_atomic() || !mm)) {
        bad_area_nosemaphore(regs, error_code, address);
        return;
    }

    /*
     * It's safe to allow irq's after cr2 has been saved and the
     * vmalloc fault has been handled.
     *
     * User-mode registers count as a user access even for any
     * potential system fault or CPU buglet:
     */
    if (user_mode_vm(regs)) {
        local_irq_enable();
        error_code |= PF_USER;
        flags |= FAULT_FLAG_USER;
    } else {
        if (regs->flags & X86_EFLAGS_IF)
            local_irq_enable();
    }

    perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

    if (error_code & PF_WRITE)
        flags |= FAULT_FLAG_WRITE;

    /*
     * When running in the kernel we expect faults to occur only to
     * addresses in user space.  All other faults represent errors in
     * the kernel and should generate an OOPS.  Unfortunately, in the
     * case of an erroneous fault occurring in a code path which already
     * holds mmap_sem we will deadlock attempting to validate the fault
     * against the address space.  Luckily the kernel only validly
     * references user space from well defined areas of code, which are
     * listed in the exceptions table.
     *
     * As the vast majority of faults will be valid we will only perform
     * the source reference check when there is a possibility of a
     * deadlock. Attempt to lock the address space, if we cannot we then
     * validate the source. If this is invalid we can skip the address
     * space check, thus avoiding the deadlock:
     */
    if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
        if ((error_code & PF_USER) == 0 &&
            !search_exception_tables(regs->ip)) {
            bad_area_nosemaphore(regs, error_code, address);
            return;
        }
retry:
        down_read(&mm->mmap_sem);
    } else {
        /*
         * The above down_read_trylock() might have succeeded in
         * which case we'll have missed the might_sleep() from
         * down_read():
         */
        might_sleep();
    }

    //缺页中断地址位于用户空间
    //查找vma
    vma = find_vma(mm, address);

    //没找到,出错
    if (unlikely(!vma)) {
        bad_area(regs, error_code, address);
        return;
    }

    //检查在vma的地址的合法性
    if (likely(vma->vm_start <= address))
        goto good_area;

    if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
        bad_area(regs, error_code, address);
        return;
    }

    //如果在用户上下文
    if (error_code & PF_USER) {
        /*
         * Accessing the stack below %sp is always a bug.
         * The large cushion allows instructions like enter
         * and pusha to work. ("enter $65535, $31" pushes
         * 32 pointers and then decrements %sp by 65535.)
         */
        if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
            bad_area(regs, error_code, address);
            return;
        }
    }

    //栈扩张
    if (unlikely(expand_stack(vma, address))) {
        bad_area(regs, error_code, address);
        return;
    }

    /*
     * Ok, we have a good vm_area for this memory access, so
     * we can handle it..
     */

    //vma合法
good_area:
    if (unlikely(access_error(error_code, vma))) {
        bad_area_access_error(regs, error_code, address);
        return;
    }

    /*
     * If for any reason at all we couldn't handle the fault,
     * make sure we exit gracefully rather than endlessly redo
     * the fault.  Since we never set FAULT_FLAG_RETRY_NOWAIT, if
     * we get VM_FAULT_RETRY back, the mmap_sem has been unlocked.
     */

    //调用通用的缺页处理
    fault = handle_mm_fault(mm, vma, address, flags);
    major |= fault & VM_FAULT_MAJOR;

    /*
     * If we need to retry the mmap_sem has already been released,
     * and if there is a fatal signal pending there is no guarantee
     * that we made any progress. Handle this case first.
     */
    if (unlikely(fault & VM_FAULT_RETRY)) {
        /* Retry at most once */
        if (flags & FAULT_FLAG_ALLOW_RETRY) {
            flags &= ~FAULT_FLAG_ALLOW_RETRY;
            flags |= FAULT_FLAG_TRIED;
            if (!fatal_signal_pending(tsk))
                goto retry;
        }

        /* User mode? Just return to handle the fatal exception */
        if (flags & FAULT_FLAG_USER)
            return;

        /* Not returning to user mode? Handle exceptions or die: */
        no_context(regs, error_code, address, SIGBUS, BUS_ADRERR);
        return;
    }

    up_read(&mm->mmap_sem);
    if (unlikely(fault & VM_FAULT_ERROR)) {
        mm_fault_error(regs, error_code, address, fault);
        return;
    }

    /*
     * Major/minor page fault accounting. If any of the events
     * returned VM_FAULT_MAJOR, we account it as a major fault.
     */
    if (major) {
        tsk->maj_flt++;
        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, address);
    } else {
        tsk->min_flt++;
        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, address);
    }

    check_v8086_mode(regs, address, tsk);
}
NOKPROBE_SYMBOL(__do_page_fault);
时间: 2024-10-31 10:28:36

Linux内核内存管理-内存访问与缺页中断的相关文章

Linux内核中的内存管理浅谈

 [十月往昔]--Linux内核中的内存管理浅谈 为什么要叫做"十月往昔"呢?是为了纪念我的原博客. 不知道为什么,突然想来一个新的开始--而那个博客存活至今刚好十个月,也有十个月里的文档. 十月往昔,总有一些觉得珍贵的,所以搬迁到这里来. 而这篇文章是在09.04.20-09.04.21里写的. Jason Lee   ------------–cut-line   1.基本框架(此处主要谈页式内存管理) 4G是一个比较敏感的字眼,早些日子,大多数机器(或者说操作系统)支持的内存上限

Linux内核内存管理-内存访问与缺页中断【转】

转自:https://yq.aliyun.com/articles/5865 摘要: 简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断的具体处理流程.先介绍了用户态进程的执行流程,然后对比了内核线程,引入高端内存的概念,最后分析了缺页中断的流程. 用户进程 fork之后的用户态进... 简单描述了x86 32位体系结构下Linux内核的用户进程和内核线程的线性地址空间和物理内存的联系,分析了高端内存的引入与缺页中断

Linux内核剖析 之 内存管理

1. 内存管理区     为什么分成不同的内存管理区?     ISA总线的DMA处理器有严格的限制:只能对物理内存前16M寻址.     内核线性地址空间只有1G,CPU不能直接访问所有的物理内存.     ZONE_DMA                  小于16M内存页框     ZONE_NORMAL          16M~896M内存页框     ZONE_HIGHMEM        大于896M内存页框     ZONE_DMA和ZONE_NORMAL区域包含的页框,通过线性

Linux内核中常见内存分配函数(三)

ioremap void * ioremap (unsigned long offset, unsigned long size) ioremap是一种更直接的内存"分配"方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段 物理地址映射到内核地址空间.ioremap用到的物理地址空间都是事先确定的,和上面的几种内存 分配方式并不太一样,并不是分配一段新的物理内存. ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间.ioremap能映射的内存由原有的物理

Linux内核中常见内存分配函数【转】

 转自:http://blog.csdn.net/wzhwho/article/details/4996510 1.      原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分别为: l         页全局目录(Page Global Directory) l         页上级目录(Page Upper Directory) l         页中间

Linux内核中常见内存分配函数(二)

常用内存分配函数 __get_free_pages unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) __get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返 回值为第一个页框的起始地址.__get_free_pages在实现上只是封装了alloc_pages函 数,Linux培训 从代码分析,alloc_pages函数会分配长度为1< kmem_cache_alloc str

Linux内核中常见内存分配函数(一)

linux内核中采 用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表. * 页全局目录(Page Global Directory) * 页上级目录(Page Upper Directory) * 页中间目录(Page Middle Directory) * 页表(Page Table) 页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址 ,而页中间目录又包含若干页表的地址,每一个页表项指

Linux内核剖析 之 内存寻址(三)

//接前一篇博客: 本节主要讲述Linux中的分页机制,注意对比Linux和80x86下分页机制的不同. 5.Linux中的分页                  图:Linux分页模式(四级页表)        Linux的进程处理很大程度上依赖于分页,每个进程都有自己的页全局目录和自己的页表集. 5.1.线性地址字段:       下列宏简化了页表处理.(80x86)       PAGE_SHIFT:       指定Offset字段的位数:当用于80x86处理器时,其值为12.     

Linux内核剖析 之 内存寻址(一)

1.内存地址 逻辑地址(Logical Address):包含在机器语言中用来指定一个操作数或一条指令的地址.每个逻辑地址都由一个端(segment)和偏移量(offset或displacement)组成,偏移量指明了从段开始的地方到实际地址之间的距离. 线性地址(Linear Address)(也称虚拟地址 Virtual Address):是一个32位无符号整数,可以用来表示高达4GB的地址.线性地址通常用十六进制数字来表示,值的范围从0x00000000到0xffffffff. 物理地址(