• Linux内存管理之地址映射


    写在前面:由于地址映射涉及到各种寄存器的设置访问,Linux对于不同体系结构处理器的地址映射采用不同的方法,例如对于i386及后来的32位的Intel的处理器在页式映射时采用的是2级页表映射,而对于IA64的处理器则采用3级分页。对于其他类型的处理器,例如MK68000等其他许多处理器,在地址映射时则忽略了段式映射,只是因为Intel的X86系列需要兼容早期的段式映射,才在后来的设计中即使用了段式映射,也采用了页式映射。以后关于Linux的笔记,除特别说明外,均是在i386体系结构之上,笔记中所有源码除特别说明外均摘自linux-2.4.0源码树。

    现代操作系统在内存管理上均使用高效的页式管理,Linux也不例外。对于i386处理器则有些例外,为了兼容早期的处理器,Intel强制要求必须先经过段式映射。在地址映射时,虚拟地址被划分成固定的页面大小,由MMU将虚拟地址映射到实际的物理地址。在访问一个虚拟地址表示的内存空间中,CPU必须经过若干次的内存访问才能完成映射,具体访问次数为N+1(N为页表级数),同时还需要N次加法运算。

    在Linux进行段式映射和页式映射之前,需要搞清楚X86系列的地址描述方式:

    • 逻辑地址:出现在机器指令中,用来制定操作数的地址。
    • 线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。
    • 物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
    段式映射即为将逻辑地址与线性地址映射起来,而页式映射则为将线性地址和物理地址对应起来。
     
    段式映射阶段:i386CPU选择代码段寄存器CS的当前值作为段描述符表中的下标,段式寄存器的第2位为0时使用GDT,为1时使用LDT。Intel设计为内核使用GDT,各个进程使用自己的LDT,寄存器的最低两位表示特权级别。在Linux内核中其实只使用GDT,在4个权限级别中只是用了0代表kernel级,3代表用户级。内核在创建一个新进程时都会先设定其段寄存器,对i386处理器中,段寄存器的设置代码位于include/asm-i386/processor.h
    include/asm-i386/processor.h
     
    #define start_thread(regs, new_eip, new_esp) do {   
        __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));
        set_fs(USER_DS);                
        regs->xds = __USER_DS;               
        regs->xes = __USER_DS;               
        regs->xss = __USER_DS;               
        regs->xcs = __USER_CS;               
        regs->eip = new_eip;             
        regs->esp = new_esp;             
    } while (0)

    从代码可以看出,Linux将i386处理器的DS,ES,SS寄存器均设置为USER_DS,这表示在Linux中对于进程的代码段,数据段和堆栈段是不区分的。__USER_CS和__USER_DS的设置位于include/asm-i386/segment.h

    include/asm-i386/segment.h
     
    #ifndef _ASM_SEGMENT_H
    #define _ASM_SEGMENT_H
      
    #define __KERNEL_CS 0x10
    #define __KERNEL_DS 0x18
      
    #define __USER_CS   0x23
    #define __USER_DS   0x2B
      
    #endif

    由以上代码可以看出,CS寄存器中的内容是0x23,通过段寄存器各位的含义可知,CPU以4作为下标,从全局描述符表GDT中寻找段描述选项,GDT的内容在arch/i386/kernel/head.S中定义

    arch/i386/kernel/head.S
     
    ENTRY(gdt_table)
        .quad 0x0000000000000000    /* NULL descriptor */
        .quad 0x0000000000000000    /* not used */
        .quad 0x00cf9a000000ffff    /* 0x10 kernel 4GB code at 0x00000000 */
        .quad 0x00cf92000000ffff    /* 0x18 kernel 4GB data at 0x00000000 */
        .quad 0x00cffa000000ffff    /* 0x23 user   4GB code at 0x00000000 */
        .quad 0x00cff2000000ffff    /* 0x2b user   4GB data at 0x00000000 */
        .quad 0x0000000000000000    /* not used */
        .quad 0x0000000000000000    /* not used */
        /*
         * The APM segments have byte granularity and their bases
         * and limits are set at run time.
         */
        .quad 0x0040920000000000    /* 0x40 APM set up for bad BIOS's */
        .quad 0x00409a0000000000    /* 0x48 APM CS    code */
        .quad 0x00009a0000000000    /* 0x50 APM CS 16 code (16 bit) */
        .quad 0x0040920000000000    /* 0x58 APM DS    data */
        .fill NR_CPUS*4,8,0     /* space for TSS's and LDT's */
    到此段式映射已经完成,实际上对于Linux的页式映射来说这一步完全没用但又不得不做。通过段映射,进程的逻辑地址已经映射到了线性地址,但实际上通过段描述符的意义来看,二者是相同的。
     
    页式映射阶段:在页式存储中,每个进程都有其自身的PGD,指向PGD的指针存在进程的mm_struct中,每当进程运行的时候,内核需要设定好控制寄存器CR3,MMU是从CR3中取得页面目录指针的。我们知道CPU在执行程序时使用的是虚拟的地址,而MMU硬件在映射时使用的是实际的物理内存地址,其具体实现是由include/asm-i386/mmu_context.h中的函数实现
    include/asm-i386/mmu_context.h
     
    static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
    {
        if (prev != next) {
            /* stop flush ipis for the previous mm */
            clear_bit(cpu, &prev->cpu_vm_mask);
            /*
             * Re-load LDT if necessary
             */
            if (prev->context.segments != next->context.segments)
                load_LDT(next);
    #ifdef CONFIG_SMP
            cpu_tlbstate[cpu].state = TLBSTATE_OK;
            cpu_tlbstate[cpu].active_mm = next;
    #endif
            set_bit(cpu, &next->cpu_vm_mask);
            /* Re-load page tables */
            asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
        }
    #ifdef CONFIG_SMP
        else {
            cpu_tlbstate[cpu].state = TLBSTATE_OK;
            if(cpu_tlbstate[cpu].active_mm != next)
                BUG();
            if(!test_and_set_bit(cpu, &next->cpu_vm_mask)) {
                /* We were in lazy tlb mode and leave_mm disabled 
                 * tlb flush IPI delivery. We must flush our tlb.
                 */
                local_flush_tlb();
            }
        }
    #endif
    }
      
    #define activate_mm(prev, next) 
        switch_mm((prev),(next),NULL,smp_processor_id())
      
    #endif

    重点关注其中的asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));它实现的功能即为将页目录指针读入CR3寄存器中。通过线性地址的最高10位可以从页面目录中知道具体的目录项,在找到进程的目录项之后,在目录项中,高20位指向页面表,在得到页面表之后,CPU再从线性地址的中间10位得到页表中的表项。在32位处理器上页表中的高20位指向物理内存的初始地址,在其后添加12个0,然后加上线性地址中的低12位(即为线性地址中的偏移量),这样就得到了一个具体的物理地址了。

    在地址映射这个问题上,内核只提供页表,实际的转换是由硬件去完成的。那么内核如何生成这些页表呢?这就有两方面的内容,虚拟地址空间的管理和物理内存的管理。实际上只有用户态的地址映射才需要管理,内核态的地址映射是写死的即为[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB)。在这一部分中,内核要实现的一个重要功能就是通过高速缓存来提高查找速度。

    参考资料:

    ------------------------------- 问道,修仙 -------------------------------
  • 相关阅读:
    jquerymobile 页面间URL传值
    xcode 静态链接库的问题
    iPad 用户体验关键要素
    Enable SharePoint Designer for Project Web App PWA 2010
    后台定位
    做一个iPhone应用需要花多少钱?
    ios无法获取坐标
    重装系统后ORACLE数据库恢复的方法
    【Web】百度有聊官网的一些布局不好之处
    【Pagoda】在pagodabox里建立项目并连接数据库
  • 原文地址:https://www.cnblogs.com/elvalad/p/4052549.html
Copyright © 2020-2023  润新知