原文地址 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_dir 和 pg0 后,接下來的工作就是對(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)的代碼我們就先看到這里。