Linux 內(nèi)核頁表的創(chuàng)建

原文地址 jekton.github.io,未經(jīng)允許,不得轉(zhuǎn)載。

源碼使用 Linux 2.6.24,基于 x86 平臺(tái);參考書是《深入理解 LINUX 內(nèi)核》第三版

內(nèi)核跟普通的應(yīng)用一樣,為了使用虛擬內(nèi)存,也需要一個(gè)給 CPU 設(shè)置一個(gè)頁表。在這篇文章中,我們就一起來了解 Linux 是如何為內(nèi)核創(chuàng)建頁表的。需要注意的是,這里我并不打算詳細(xì)講解頁表的方方面面,硬件相關(guān)的基礎(chǔ)知識(shí),讀者可以參考《深入理解LINUX內(nèi)核》第3版第2章。本文的目的在于,作為該書的補(bǔ)充,基于真實(shí)的源碼來講解這一過程。

臨時(shí)內(nèi)核頁表的構(gòu)造

x86 系統(tǒng)剛剛啟動(dòng)時(shí)候運(yùn)行在實(shí)模式下,這個(gè)時(shí)候線性地址就是物理地址。為了進(jìn)入 32 位保護(hù)模式,首先就要啟用分頁(paging)。這就要求我們構(gòu)建一個(gè)頁表;這張頁表把線性地址映射轉(zhuǎn)換為物理地址。由于不同的計(jì)算機(jī)的配置不一樣,他們需要的頁表大小、頁表個(gè)數(shù)也都不一樣,所以需要在運(yùn)行時(shí)動(dòng)態(tài)分配頁表,這就要求我們具有動(dòng)態(tài)內(nèi)存分配能力。

為了解決構(gòu)造頁表時(shí)候的雞生蛋蛋生雞問題,Linux 使用了一個(gè)臨時(shí)的內(nèi)核頁表。它只有兩個(gè)頁表(這里的頁表指的是用來索引頁框的最后一級(jí)頁表)。在不啟用 PAE (Page Addression Extension) 和 PSE(Page Size Extension)的情況下,一個(gè)頁表可以指向 10^2 = 1024 個(gè)內(nèi)存頁,一個(gè)內(nèi)存頁 4K,所以兩個(gè)頁表允許我們索引 8M 的內(nèi)存。

頂層的頁目錄(page directory)使用全局變量 swapper_pg_dir 定義,下面是它的聲明:

// ${linux_source}/include/asm-x86/pgtable_32.h

// empty_zero_page 在后面也會(huì)用到,這里就一并列出來了
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 個(gè) 4 byte 長度的內(nèi)存(一個(gè)頁目錄項(xiàng)(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 后,接下來的工作就是對(duì)它們進(jìn)行初始化。此時(shí)還處于實(shí)模式下,這部分工作是由匯編代碼完成的。

# ${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 位都是一些標(biāo)志物,各個(gè)位代表的含義
    # 讀者可以參考 https://wiki.osdev.org/Paging 或者書中的第 52 頁
    movl $0x007, %eax           /* 0x007 = PRESENT+RW+USER */
10:
    # 下面這兩行代碼對(duì)熟悉 C 語言的讀者可能會(huì)造成一定的困擾。如果從 C 語言的角度
    # 來看,它們是把地址 &pg0 + 7 放到了 swapper_pg_dir 的第一項(xiàng);但問題在于,
    # 為什么要 +7?
    # 其實(shí)這里的 7 和前面那個(gè) 7 一樣,指的是頁目錄項(xiàng)的標(biāo)志物 PRESENT+RW+USER,
    # pg0 的地址是 4K 對(duì)齊的,這意味著他的地址的低 12 位都為 0,加上 7 以后,剛
    # 好就是我們所需要的頁目錄項(xiàng)的值。
    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,下面這一行設(shè)置的 0xc000 0000
    # 對(duì)應(yīng)的頁目錄項(xiàng)。
    # 這里的問題在于,按照書里的說明,我們應(yīng)該設(shè)置的是第 0x300 項(xiàng),這里是加上的卻是 0xc00。
    # 這里需要提一下平時(shí)用 C 語言時(shí)編譯器幫我們做的事。當(dāng)我們寫下 int *p = NULL; p+2
    # 的時(shí)候,編譯器知道 int 是 4 個(gè)字節(jié),所以 p+2 會(huì)匯編代碼里面是 +8。
    # 一個(gè) PDE 也是 32 位,所以真正的偏移量是 0x300 << 2 = 0xc00
    movl %ecx,page_pde_offset(%edx)     /* Store kernel PDE entry */
    # edx + 4 以后,就是下一個(gè)頁目錄項(xiàng)了,下個(gè)循環(huán)將會(huì)繼續(xù)初始化(一共兩個(gè)頁目錄項(xiàng))
    addl $4,%edx
    # 一個(gè)頁表有 1024 個(gè)頁表項(xiàng),這里初始化一個(gè)在接下來的循環(huán)里面用到的計(jì)數(shù)器
    movl $1024, %ecx
11:
    # stosl 把 %eax 的內(nèi)容復(fù)制到物理地址 ES:EDI,也就是 pg0 處;并且 %edi + 4
    stosl
    # 加上 0x1000 后,%eax 指向下一個(gè)頁
    addl $0x1000,%eax
    # %ecx -= 1,如果 %ecx 不為 0,跳轉(zhuǎn)到 11 處。這里總共會(huì)循環(huán) 1024 次,初始化 1024 個(gè)頁表項(xiàng)。
    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
    # 到這里的時(shí)候,%edi 的值是我們映射的最后一個(gè)頁表項(xiàng)的地址,這里我們把它存到變量
    # 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

前面代碼的最后一行是一個(gè) jmp 3f,下面,我們就看看這個(gè) 3 處的代碼。

啟用分頁

構(gòu)建好臨時(shí)內(nèi)核頁表后,接下來就該啟用分頁了。

# ${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 后啟用分頁
    # 關(guān)于 cr0,參考 https://en.wikipedia.org/wiki/Control_register#CR0
    orl $0x80000000,%eax
    movl %eax,%cr0      /* ..and set paging (PG) bit */

CPU 的分頁機(jī)制現(xiàn)在已經(jīng)啟用了,但是我們的頁表還是不完整的,剩下部分將會(huì)使用 C 語言來完成。

構(gòu)建線性地址的內(nèi)核頁表

完整的頁表構(gòu)建是從函數(shù) 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);

    // 下面是固定映射相關(guān)的內(nèi)容,這里就先忽略了
}

實(shí)際的頁表構(gòu)建是在函數(shù) 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,這里拿的內(nèi)核虛擬地址第一項(xiàng)對(duì)應(yīng)的 pgd 的 index
    pgd_idx = pgd_index(PAGE_OFFSET);
    pgd = pgd_base + pgd_idx;
    pfn = 0;    // pfn 代表 page frame number

    // 初始化 pgd。pgd 的項(xiàng)數(shù)由 PTRS_PER_PGD 定義,在最普通的情況下,它是 1024。
    // 如果啟用了 PAE,則等于 4
    for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
        // 32 位的系統(tǒng)一般是 2 級(jí)頁表結(jié)構(gòu)(為什么說它是一般,讀者后面就會(huì)知道了)
        // 每個(gè) pgd 項(xiàng)都指向一個(gè) pmd,one_md_table_init 初始化一個(gè) pmd。
        // 建議讀者這里先跳過本函數(shù)后面部分,看完 one_md_table_init 再回過頭來繼續(xù)往下看
        pmd = one_md_table_init(pgd);
        // max_low_pfn 是被內(nèi)核直接映射的最后一個(gè)頁框的頁框號(hào),參考書中第 72 頁
        if (pfn >= max_low_pfn)
            // 超過 max_low_pfn 的 pte 可以不初始化,但 pmd 必須初始化,所以用 continue
            continue;
        // 對(duì)不啟用 PAE 的系統(tǒng)來說,這里的 pmd 就是 pgd,PTRS_PER_PMD 等于 1。
        // 如果啟用 PAE,PTRS_PER_PMD 等于 512。
        // 這里的 pmd 相當(dāng)于頁目錄(Page Directory),下面的循環(huán)里初始化每個(gè)頁目錄項(xiàng)(每個(gè)頁目錄項(xiàng)
        // 指向一個(gè)頁表項(xiàng))
        for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {
            // address 是當(dāng)前(物理)頁框開頭對(duì)應(yīng)的虛擬地址
            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 是當(dāng)前 pmd 能夠索引的最大的頁框號(hào)
                // * PAGE_SIZE + PAGE_OFFSET + (PAGE_SIZE-1) 就是當(dāng)前 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 包含了內(nèi)核的 text 段,所以加上了 exec 標(biāo)記
                    set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
                else
                    set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));

                // 啟用 PSE 后就不需要 pte 了。
                // 對(duì)于啟用了 PAE 的機(jī)器來說,一頁是 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 項(xiàng);pmd 是 2^9 = 512 項(xiàng);然后是 pte 2^9 = 512 項(xiàng);
        // pte 在 kernel_physical_mapping_init 中初始化。
        // PAE 相關(guān)知識(shí)參考書上第 56 頁

        // bootmem 相關(guān)的后面昨晚單獨(dú)的一篇文章來講述,這里假裝內(nèi)存被
        // 神奇地分配出來就好
        pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);

        // 虛擬化相關(guān)的東西,忽略就好
        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 其實(shí)就是 pgd(也就是
    // 直接從 pgd 到 pte,兩者都是 2^10 = 1024 項(xiàng))
    pud = pud_offset(pgd, 0);
    pmd_table = pmd_offset(pud, 0);
    return pmd_table;
}


// 這個(gè)函數(shù)就比較平凡了,沒有什么好說的
/*
 * 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);
}

這部分代碼其實(shí)有 4 中情況:有 PAE 和沒有 PAE兩種,這兩種又分別有 PSE 啟不啟用兩種情況。讀者可以分情況一個(gè)一個(gè)看,分情況弄清楚后,再合并一起看。

固定映射的線性地址、非連續(xù)內(nèi)存區(qū)的線性地址

處于篇幅和學(xué)習(xí)目的考慮,固定映射、非連續(xù)內(nèi)存的處理在這里就先略去了,以后有機(jī)會(huì)再單獨(dú)開一篇文章補(bǔ)上。內(nèi)核頁表的創(chuàng)建相關(guān)的代碼我們就先看到這里。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 內(nèi)存尋址 1.1 物理地址、虛擬地址以及線性地址 物理地址: 物理內(nèi)存的內(nèi)存單元地址 虛擬地址: 程序員看到的...
    瘋狂小王子閱讀 3,126評(píng)論 3 21
  • 第二章 內(nèi)存尋址 內(nèi)存地址 內(nèi)存地址分為三種:邏輯地址(logical address)(段+偏移量) ...
    rlkbk閱讀 486評(píng)論 0 1
  • 轉(zhuǎn)眼來到科銳學(xué)習(xí)已經(jīng)超過一年的時(shí)間了,眼看三階段已經(jīng)進(jìn)入尾聲,內(nèi)核的學(xué)習(xí)也快要結(jié)束,記錄一下筆記和心得,也給剛接觸...
    五行貓閱讀 1,235評(píng)論 0 0
  • 5-Level Paging and 5-Level EPT white paper原文 修訂版本1.1 2017...
    公子小水閱讀 4,289評(píng)論 0 3
  • 提交東西給領(lǐng)導(dǎo)未檢查一遍, 便有了標(biāo)題那句話。 或許,你覺得沒什么~ 然而,領(lǐng)導(dǎo)卻覺得很有什么…… 畢竟,你已工作...
    飯后雜記閱讀 192評(píng)論 0 0

友情鏈接更多精彩內(nèi)容