美文网首页
Linux 内核页表的创建

Linux 内核页表的创建

作者: 虾饺的开发手记 | 来源:发表于2018-11-25 11:27 被阅读0次

    原文地址 jekton.github.io,未经允许,不得转载。

    源码使用 Linux 2.6.24,基于 x86 平台;参考书是《深入理解 LINUX 内核》第三版

    内核跟普通的应用一样,为了使用虚拟内存,也需要一个给 CPU 设置一个页表。在这篇文章中,我们就一起来了解 Linux 是如何为内核创建页表的。需要注意的是,这里我并不打算详细讲解页表的方方面面,硬件相关的基础知识,读者可以参考《深入理解LINUX内核》第3版第2章。本文的目的在于,作为该书的补充,基于真实的源码来讲解这一过程。

    临时内核页表的构造

    x86 系统刚刚启动时候运行在实模式下,这个时候线性地址就是物理地址。为了进入 32 位保护模式,首先就要启用分页(paging)。这就要求我们构建一个页表;这张页表把线性地址映射转换为物理地址。由于不同的计算机的配置不一样,他们需要的页表大小、页表个数也都不一样,所以需要在运行时动态分配页表,这就要求我们具有动态内存分配能力。

    为了解决构造页表时候的鸡生蛋蛋生鸡问题,Linux 使用了一个临时的内核页表。它只有两个页表(这里的页表指的是用来索引页框的最后一级页表)。在不启用 PAE (Page Addression Extension) 和 PSE(Page Size Extension)的情况下,一个页表可以指向 10^2 = 1024 个内存页,一个内存页 4K,所以两个页表允许我们索引 8M 的内存。

    顶层的页目录(page directory)使用全局变量 swapper_pg_dir 定义,下面是它的声明:

    // ${linux_source}/include/asm-x86/pgtable_32.h
    
    // empty_zero_page 在后面也会用到,这里就一并列出来了
    extern unsigned long empty_zero_page[1024];
    extern pgd_t swapper_pg_dir[1024];
    

    他在 head_32.S 里面定义的:

    # ${linux_source}/arch/x86/kernel/head_32.S
    
    /*
     * BSS section
     */
    .section ".bss.page_aligned","wa"
        .align PAGE_SIZE_asm
    ENTRY(swapper_pg_dir)
        .fill 1024,4,0
    ENTRY(swapper_pg_pmd)
        .fill 1024,4,0
    ENTRY(empty_zero_page)
        .fill 4096,1,0
    

    这里的 .fill 1024,4,0 的意思是用 0 填充 1024 个 4 byte 长度的内存(一个页目录项(page table entry)的大小是 32 bit)。

    接下来是变量 pg0

    // ${linux_source}/include/asm-x86/pgtable_32.h
    
    /* The boot page tables (all created as a single array) */
    extern unsigned long pg0[];
    

    pg0 通过指示链接器,放在了 bss 段的后面。

    SECTIONS
    {
      /* 前面那些都略去了 */
    
      .bss : AT(ADDR(.bss) - LOAD_OFFSET) {
        __init_end = .;
        __bss_start = .;        /* BSS */
        *(.bss.page_aligned)
        *(.bss)
        . = ALIGN(4);
        __bss_stop = .;
        _end = . ;
        /* This is where the kernel creates the early boot page tables */
        . = ALIGN(4096);
        pg0 = . ;
      }
    
      /* ... */
    }
    

    有了 swapper_pg_dirpg0 后,接下来的工作就是对它们进行初始化。此时还处于实模式下,这部分工作是由汇编代码完成的。

    # ${linux_source}/arch/x86/kernel/head_32.S
    
    /*
     * Initialize page tables.  This creates a PDE and a set of page
     * tables, which are located immediately beyond _end.  The variable
     * init_pg_tables_end is set up to point to the first "safe" location.
     * Mappings are created both at virtual address 0 (identity mapping)
     * and PAGE_OFFSET for up to _end+sizeof(page tables)+INIT_MAP_BEYOND_END.
     *
     * Warning: don't use %esi or the stack in this code.  However, %esp
     * can be used as a GPR if you really need it...
     */
    # __PAGE_OFFSET 是 0xc000 0000,所以 page_pde_offset 是 0xc00
    page_pde_offset = (__PAGE_OFFSET >> 20);
    
    default_entry:
        # __PAGE_OFFSET 是 3G,pg0 是虚拟地址,减去 __PAGE_OFFSET 后就得到了
        # pg0 的物理地址。我们把 pg0 的物理地址放在了 edi 寄存器里
        movl $(pg0 - __PAGE_OFFSET), %edi
        # 同理,这里把 swapper_pg_dir 的物理地址放在 edx
        movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
        # page directory/table entry 的低 12 位都是一些标志物,各个位代表的含义
        # 读者可以参考 https://wiki.osdev.org/Paging 或者书中的第 52 页
        movl $0x007, %eax           /* 0x007 = PRESENT+RW+USER */
    10:
        # 下面这两行代码对熟悉 C 语言的读者可能会造成一定的困扰。如果从 C 语言的角度
        # 来看,它们是把地址 &pg0 + 7 放到了 swapper_pg_dir 的第一项;但问题在于,
        # 为什么要 +7?
        # 其实这里的 7 和前面那个 7 一样,指的是页目录项的标志物 PRESENT+RW+USER,
        # pg0 的地址是 4K 对齐的,这意味着他的地址的低 12 位都为 0,加上 7 以后,刚
        # 好就是我们所需要的页目录项的值。
        leal 0x007(%edi),%ecx           /* Create PDE entry */
        movl %ecx,(%edx)            /* Store identity PDE entry */
        # 书里有说明,我们要把 0x0000 0000 ~ 0x007f ffff 和 0xc000 0000 ~ 0xc07f ffff
        # 都映射到物理地址 0x0000 0000 ~ 0x007f ffff,下面这一行设置的 0xc000 0000
        # 对应的页目录项。
        # 这里的问题在于,按照书里的说明,我们应该设置的是第 0x300 项,这里是加上的却是 0xc00。
        # 这里需要提一下平时用 C 语言时编译器帮我们做的事。当我们写下 int *p = NULL; p+2
        # 的时候,编译器知道 int 是 4 个字节,所以 p+2 会汇编代码里面是 +8。
        # 一个 PDE 也是 32 位,所以真正的偏移量是 0x300 << 2 = 0xc00
        movl %ecx,page_pde_offset(%edx)     /* Store kernel PDE entry */
        # edx + 4 以后,就是下一个页目录项了,下个循环将会继续初始化(一共两个页目录项)
        addl $4,%edx
        # 一个页表有 1024 个页表项,这里初始化一个在接下来的循环里面用到的计数器
        movl $1024, %ecx
    11:
        # stosl 把 %eax 的内容复制到物理地址 ES:EDI,也就是 pg0 处;并且 %edi + 4
        stosl
        # 加上 0x1000 后,%eax 指向下一个页
        addl $0x1000,%eax
        # %ecx -= 1,如果 %ecx 不为 0,跳转到 11 处。这里总共会循环 1024 次,初始化 1024 个页表项。
        loop 11b
        /* End condition: we must map up to and including INIT_MAP_BEYOND_END */
        /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
        leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
        cmpl %ebp,%eax
        jb 10b
        # 到这里的时候,%edi 的值是我们映射的最后一个页表项的地址,这里我们把它存到变量
        # init_pg_tables_end 里。init_pg_tables_end 在 setup_32.c 里定义
        movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
    
        # 下面是固定映射的,这部分就先不看了
        /* Do an early initialization of the fixmap area */
        movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
        movl $(swapper_pg_pmd - __PAGE_OFFSET), %eax
        addl $0x67, %eax            /* 0x67 == _PAGE_TABLE */
        movl %eax, 4092(%edx)
    
        xorl %ebx,%ebx              /* This is the boot CPU (BSP) */
        jmp 3f
    

    前面代码的最后一行是一个 jmp 3f,下面,我们就看看这个 3 处的代码。

    启用分页

    构建好临时内核页表后,接下来就该启用分页了。

    # ${linux_source}/arch/x86/kernel/head_32.S
    
    3:
    /*
     * Enable paging
     */
        movl $swapper_pg_dir-__PAGE_OFFSET,%eax
        # %cr3 寄存器存放的是页表的地址
        movl %eax,%cr3      /* set the page table pointer.. */
        movl %cr0,%eax
        # cr0 的最高位是 Paging 位,置 1 后启用分页
        # 关于 cr0,参考 https://en.wikipedia.org/wiki/Control_register#CR0
        orl $0x80000000,%eax
        movl %eax,%cr0      /* ..and set paging (PG) bit */
    

    CPU 的分页机制现在已经启用了,但是我们的页表还是不完整的,剩下部分将会使用 C 语言来完成。

    构建线性地址的内核页表

    完整的页表构建是从函数 pagetable_init 开始的:

    // ${linux_source}/arch/x86/mm/init_32.S
    
    static void __init pagetable_init (void)
    {
        unsigned long vaddr, end;
        pgd_t *pgd_base = swapper_pg_dir;
    
        /* Enable PSE if available */
        if (cpu_has_pse)
            set_in_cr4(X86_CR4_PSE);
    
        /* Enable PGE if available */
        if (cpu_has_pge) {
            set_in_cr4(X86_CR4_PGE);
            __PAGE_KERNEL |= _PAGE_GLOBAL;
            __PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;
        }
    
        kernel_physical_mapping_init(pgd_base);
    
        // 下面是固定映射相关的内容,这里就先忽略了
    }
    

    实际的页表构建是在函数 kernel_physical_mapping_init 完成的:

    // ${linux_source}/arch/x86/mm/init_32.c
    
    /*
     * This maps the physical memory to kernel virtual address space, a total 
     * of max_low_pfn pages, by creating page tables starting from address 
     * PAGE_OFFSET.
     */
    static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
    {
        unsigned long pfn;
        pgd_t *pgd;
        pmd_t *pmd;
        pte_t *pte;
        int pgd_idx, pmd_idx, pte_ofs;
    
        // PAGE_OFFSET 是 0xc000 0000,这里拿的内核虚拟地址第一项对应的 pgd 的 index
        pgd_idx = pgd_index(PAGE_OFFSET);
        pgd = pgd_base + pgd_idx;
        pfn = 0;    // pfn 代表 page frame number
    
        // 初始化 pgd。pgd 的项数由 PTRS_PER_PGD 定义,在最普通的情况下,它是 1024。
        // 如果启用了 PAE,则等于 4
        for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
            // 32 位的系统一般是 2 级页表结构(为什么说它是一般,读者后面就会知道了)
            // 每个 pgd 项都指向一个 pmd,one_md_table_init 初始化一个 pmd。
            // 建议读者这里先跳过本函数后面部分,看完 one_md_table_init 再回过头来继续往下看
            pmd = one_md_table_init(pgd);
            // max_low_pfn 是被内核直接映射的最后一个页框的页框号,参考书中第 72 页
            if (pfn >= max_low_pfn)
                // 超过 max_low_pfn 的 pte 可以不初始化,但 pmd 必须初始化,所以用 continue
                continue;
            // 对不启用 PAE 的系统来说,这里的 pmd 就是 pgd,PTRS_PER_PMD 等于 1。
            // 如果启用 PAE,PTRS_PER_PMD 等于 512。
            // 这里的 pmd 相当于页目录(Page Directory),下面的循环里初始化每个页目录项(每个页目录项
            // 指向一个页表项)
            for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {
                // address 是当前(物理)页框开头对应的虚拟地址
                unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
    
                /* Map with big pages if possible, otherwise create normal page tables. */
                if (cpu_has_pse) {
                    // pfn + PTRS_PER_PTE - 1 是当前 pmd 能够索引的最大的页框号
                    // * PAGE_SIZE + PAGE_OFFSET + (PAGE_SIZE-1) 就是当前 pmd 做能够指向的最大的
                    // 地址。也就是说,pmd 的地址范围是 [address, address2]
                    unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;
                    if (is_kernel_text(address) || is_kernel_text(address2))
                        // pmd 包含了内核的 text 段,所以加上了 exec 标记
                        set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
                    else
                        set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));
    
                    // 启用 PSE 后就不需要 pte 了。
                    // 对于启用了 PAE 的机器来说,一页是 2^(9+12) = 2M
                    // 没有 PAE 则是 2^(10+12) = 4M
                    pfn += PTRS_PER_PTE;
                } else {
                    pte = one_page_table_init(pmd);
    
                    for (pte_ofs = 0;
                         pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn;
                         pte++, pfn++, pte_ofs++, address += PAGE_SIZE) {
                        if (is_kernel_text(address))
                            set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
                        else
                            set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
                    }
                }
            }
        }
    }
    
    
    /*
     * Creates a middle page table and puts a pointer to it in the
     * given global directory entry. This only returns the gd entry
     * in non-PAE compilation mode, since the middle layer is folded.
     */
    static pmd_t * __init one_md_table_init(pgd_t *pgd)
    {
        pud_t *pud;
        pmd_t *pmd_table;
            
    #ifdef CONFIG_X86_PAE
        if (!(pgd_val(*pgd) & _PAGE_PRESENT)) {
            // 启用 PAE 的情况下,32 bit 的虚拟地址分为 2 9 9 12,pgd 有
            // 2^2 = 4 项;pmd 是 2^9 = 512 项;然后是 pte 2^9 = 512 项;
            // pte 在 kernel_physical_mapping_init 中初始化。
            // PAE 相关知识参考书上第 56 页
    
            // bootmem 相关的后面昨晚单独的一篇文章来讲述,这里假装内存被
            // 神奇地分配出来就好
            pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
    
            // 虚拟化相关的东西,忽略就好
            paravirt_alloc_pd(__pa(pmd_table) >> PAGE_SHIFT);
            set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
            pud = pud_offset(pgd, 0);
            if (pmd_table != pmd_offset(pud, 0))
                BUG();
        }
    #endif
        // 在不启用 PAE 的情况下,下面返回的 pmd_table 其实就是 pgd(也就是
        // 直接从 pgd 到 pte,两者都是 2^10 = 1024 项)
        pud = pud_offset(pgd, 0);
        pmd_table = pmd_offset(pud, 0);
        return pmd_table;
    }
    
    
    // 这个函数就比较平凡了,没有什么好说的
    /*
     * Create a page table and place a pointer to it in a middle page
     * directory entry.
     */
    static pte_t * __init one_page_table_init(pmd_t *pmd)
    {
        if (!(pmd_val(*pmd) & _PAGE_PRESENT)) {
            pte_t *page_table = NULL;
    
    #ifdef CONFIG_DEBUG_PAGEALLOC
            page_table = (pte_t *) alloc_bootmem_pages(PAGE_SIZE);
    #endif
            if (!page_table)
                page_table =
                    (pte_t *)alloc_bootmem_low_pages(PAGE_SIZE);
    
            paravirt_alloc_pt(&init_mm, __pa(page_table) >> PAGE_SHIFT);
            set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
            BUG_ON(page_table != pte_offset_kernel(pmd, 0));
        }
    
        return pte_offset_kernel(pmd, 0);
    }
    

    这部分代码其实有 4 中情况:有 PAE 和没有 PAE两种,这两种又分别有 PSE 启不启用两种情况。读者可以分情况一个一个看,分情况弄清楚后,再合并一起看。

    固定映射的线性地址、非连续内存区的线性地址

    处于篇幅和学习目的考虑,固定映射、非连续内存的处理在这里就先略去了,以后有机会再单独开一篇文章补上。内核页表的创建相关的代码我们就先看到这里。

    相关文章

      网友评论

          本文标题:Linux 内核页表的创建

          本文链接:https://www.haomeiwen.com/subject/ujhpqqtx.html