頁表存儲着虛擬地址到物理地址的映射關係,同時爲了減少頁表的內存消耗發明了多級頁表,更多基礎內容可以看淺析linux內存管理.
一個虛擬地址到物理地址通過頁表的轉換過程如下,<深入理解LINUX內核>的經典圖:
32bit系統上一般只有PGD(Page Global Directory)和pte(page table entry),32bit虛擬地址劃分成三段: 10:10:12,高10bit是PGD中偏移,中間10bit是pte數組中的偏移,低12bit是頁內偏移.在x86 arch中cr3負責加載頁表,它屬於MMU組件部分,它看到的是物理地址空間,頁表存儲在進程的task_struct->active_mm->pgd
中,不過它是個虛擬地址,經過pa轉換才傳給寄存器,具體可以看switch_mm->load_cr3
.
頁幀的大小是預先設定好的一組值,不是隨意設定的,桌面版上一般是4k,它還能提供頁幀配置的選項,對於服務器有特別的意義,在x86上如果pte上設置了PSE,則page的大小就是4M.
本來是一整塊物理內存,現在分成頁幀來管理,這樣必然會有一些管理數據,在linux中對應的數據結構就是page,頁幀爲4k時8G內存需要128M的page區域,而頁幀爲4M只需要128K的page區域,所以在服務器上4k的頁幀設置已經不適宜了;此外頁幀越小,相同的虛擬地址空間大小頁表項所需越少,TLB miss事件會更頻繁.不過更大的內存會有更多頁內碎片,造成頁內浪費.
在32bit系統中,每個地址需要4byte表示,經典的10:10:12
劃分中,每個地址空間PGD中有需要2^10=1024項,所以每個進程PGD佔據4k,每個PGD指向的pte也是佔據4k,這樣剛好不浪費空間.
頁地址是4k對齊的,低12bit全是0,即PGD中和PTE中低12bit是冗餘的,所以通常用作他途,下面是PGD中冗餘位中存儲一些flag.
- S 標識page size,如果置位則頁大小是4M,此時pte中PSE位也需要置位;如果是0,則頁大小是4k
- A 標識是否訪問過是否訪問過範圍的頁
- D 標識是否Cache Disable,如果置位這個範圍的頁則不會cache,每次都要從memory中讀寫
- W 標識Write through策略,如果設置則是write-through,如果是0則是write-back
- U 標識範圍的頁屬於用戶空間還是內核空間,頁的訪問控制基於特權級別.如果設置,這個頁屬於用戶空間,沒有限制;如果沒有設置,頁屬於內核空間,只有內核能夠訪問.
- R 標識R/W, 1:可讀可寫 0:只讀
- P 標識Present,如果置位則映射有物理頁,如果是0可能是還沒分配物理頁或者是swap out了.
關於pte的flag詳細用途:https://blog.csdn.net/faxiang1230/article/details/106112857
裏面的有些flag和PGD中的flag作用是相同的,下面只列出了不同項:
- C 標識是否Cache Disable,和pgd中的 ‘D’ 位作用相同
- G 標識全局屬性,如果置位,如果CR3重新設置,它仍然在TLB中保持這部分頁表項,需要CR4中使能這個功能
- D 標識頁是否被寫過,這個是由MMU訪問時自動置位的,但是需要CPU在回寫完成後清除flag
x86-64的地址空間
64bit地址空間是對32bit的有效擴充,不過地址空間實在太大了,沒有機器的內存能夠達到這種級別.
目前64bit系統中地址空間只使用了低48bit,即256TB大小.intel規劃了下一步可以擴充到57bit的地址空間方案,128PB大小,目前看虛擬地址資源短時間內應該不是瓶頸.
和32bit系統中有限的虛擬地址空間相比,64bit基本上可以隨意使用虛擬地址空間,它去除了一些概念:HIGHMEM區域的物理內存,pkmap區域.不過地址空間劃分繼續保持了對32bit程序的兼容性.
-
兼容32bit程序是當時64位設計時必然要考慮的問題,32bit系統存在的時代有很多優秀的軟件,這也是一種財富,不能到了64bit就不能繼續使用了,而且最大限度的保持兼容,不需要重新編譯即可運行.
所以32bit程序的用戶空間是0-3G,在64bit空間中用戶空間地址是0-128TB,32bit程序運行時只佔據了它最低端的一部分空間,運行時空間的兼容性使得不需要重新編譯.另外是系統調用的兼容性,這裏不在贅述,看linux系統調用過程剖析 -
在用戶空間地址和內核空間中間有個大大的hole,它利用高16bit不參與尋址的特點,製造了這麼大一個hole,只能說有錢任性.
-
64TB的空間來做直接映射,也就是最大能夠支持64TB的線性映射內存,高於64TB估計也得在vmalloc中動態使用了,目前64TB的內存支持是足夠大了
-
ffffffff80000000 - ffffffffa0000000
這塊空間用來映射內核的文本段,數據段等,最大512M,物理內存區域和直接映射區域是交叉的,不過直接映射區域不訪問它,頁表映射一塊物理內存區域多次又有什麼關係呢? -
其他區域就不再贅述了
https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
hole caused by [48:63] sign extension
ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffec0000000000 - fffffc0000000000 (=44 bits) kasan shadow memory (16TB)
... unused hole ...
ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks
... unused hole ...
ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0
ffffffffa0000000 - fffffffffeffffff (=1520 MB) module mapping space
ffffffffff000000 - FIXADDR_START unused hole
FIXADDR_START - ffffffffff9fffff (~0.5 MB) kernel-internal fixmap range, variable size and offset
ffffffffffa00000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole
對於虛擬地址空間來說,這樣的劃分不是固定的,從2004年x86_64的address map文檔合併到內核Documentation/x86/x86_64/mm.txt
一直到最新的內核文檔來說,變化真的很大,虛擬地址空間的規劃只是衆多約定的一種
x86-64的頁表初始化
64bit地址需要佔據8byte,所以如果是4k的頁大小,則每個頁只能允許512項,即2^9,每一組PGD,PUD等都佔據一頁的大小,頁內偏移仍然是12bit.
PGD | PUD | PMD | PTE | page offset |
---|---|---|---|---|
9 | 9 | 9 | 9 | 12 |
- 初始化狀態
目前x86的內核鏡像基本都是經過壓縮的,這能減少load內核鏡像花費的IO時間,將經過壓縮的鏡像load到內核之後,頭部包含自解壓代碼,將解壓後的內核放到約定的地址CONFIG_PHYSICAL_START
.另外前期bootloader已經開啓了MMU功能,創建了部分頁表,但是內核是一個獨立的系統,它不能依賴於bootloader的工作,所以雖然它自己現在可以運行,仍需初始化內存管理數據並創建加載自己的頁表.
32bit和64bit平臺上加載內核的方式是保持兼容的,內核加載後的地址佈局和32bit中仍然是相同的,查看/proc/iomem
獲取詳細信息
2.內核的頁表初始化
內核的入口地址.head.text
,在鏈接腳本vmlinux.ldS中指定鏈接順序,在System.map中也可以觀察到入口函數是startup_64
ffffffff81000000 T _text
ffffffff81000000 T startup_64
ffffffff81000110 T secondary_startup_64
64bit中使用4級頁表,在初始化的時候分別是:early_level4_pgt, level3_kernel_pgt,level2_kernel_pgt,level2_fixmap_pgt,level1_fixmap_pgt,在編譯的時候,進行了頁表初始化.
在地址空間規劃中,內核鏡像映射地址爲ffffffff80000000 - ffffffffa0000000 kernel text mapping, from phys 0
,下面計算一下它在各級頁表中對應的哪些項,在早期頁表中內核鏡像的頁幀設置成了2M的大小,只需要三級頁表就可以完成映射
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) ==> (0xffffffff80000000 >> 39) &(512-1) = 511
#define pud_index(address) (((address) >> PUD_SHIFT) & (PTRS_PER_PUD - 1) ==> (0xffffffff80000000 >> 30) &(512-1) = 510
#define pmd_index(address) (((address) >> PMD_SHIFT) & (PTRS_PER_PMD - 1) ==> (0xffffffff80000000 >> 21) &(512-1) = 0
而除了內核的text和data段的映射關係,還有fixmap的映射關係,計算方法類似.
- 編譯時初始化頁表的結果如下,在運行時會進行一些偏移校準
arch/x86/kernel/head_64.S
early_level4_pgt[511] -> level3_kernel_pgt[0]
level3_kernel_pgt[510] -> level2_kernel_pgt[0]
level3_kernel_pgt[511] -> level2_fixmap_pgt[0]
level2_kernel_pgt[0] -> 512 MB kernel mapping
level2_fixmap_pgt[507] -> level1_fixmap_pgt
- 內核啓動早期的頁表初始化,在4.0內核中位於
arch/x86/kernel/head_64.S
中,在編譯期間已經做完了初始化的工作,運行的時候進行偏移校準並且加載到CR3寄存器中生效。
下面是編譯時頁表初始化的代碼註釋
leaq _text(%rip), %rbp
subq $_text - __START_KERNEL_map, %rbp //rbp中存儲編譯地址和運行地址的偏移
/*
* 校準內核鏡像映射區域頁表項的物理地址偏移
*/
addq %rbp, early_level4_pgt + (L4_START_KERNEL*8)(%rip)
addq %rbp, level3_kernel_pgt + (510*8)(%rip)
addq %rbp, level3_kernel_pgt + (511*8)(%rip)
//校準固定映射區域頁表項的物理地址偏移
addq %rbp, level2_fixmap_pgt + (506*8)(%rip)
/* Fixup phys_base */
addq %rbp,phys_base(%rip)
movq $(early_level4_pgt - __START_KERNEL_map), %rax
jmp 1f
1:
/* 使能PGE即大頁模式 */
movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
movq %rcx, %cr4
/* Setup early boot stage 4 level pagetables. */
addq phys_base(%rip), %rax
movq %rax, %cr3 //load cr3
NEXT_PAGE(early_level4_pgt)
//前面511個地址全部清零,沒有進一步設置頁表之前,訪問這部分地址是非法的
.fill 511,8,0
//__START_KERNEL_map即kernel mapping區域的基地址,level3_kernel_pgt代表符號的加載地址,
//他們的差就是三級頁表的物理地址;地址低位存儲標誌位
.quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE
NEXT_PAGE(level3_kernel_pgt)
.fill L3_START_KERNEL,8,0 //kernel mapping區域之前的頁表項清零
/* (2^48-(2*1024*1024*1024)-((2^39)*511))/(2^30) = 510 */
.quad level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE //指向二級頁表
.quad level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE //fixmap區域的頁表
NEXT_PAGE(level2_kernel_pgt)
/*
* 512 MB kernel mapping. We spend a full page on this pagetable
* anyway.
*
* The kernel code+data+bss must not be bigger than that.
*
* (NOTE: at +512MB starts the module area, see MODULES_VADDR.
* If you want to increase this then increase MODULES_VADDR
* too.)
*/
//除了設置頁表項之外,它還設置了PSE標誌,內核早期的頁幀的大小爲2M,level2_kernel_pgt就是這塊區域的最後一級頁表
PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
KERNEL_IMAGE_SIZE/PMD_SIZE) //內核默認最大512M,這段空間直接映射到從0開始的物理內存,即虛擬地址0xffffffff80000000對應物理地址0
NEXT_PAGE(level2_fixmap_pgt)
.fill 506,8,0 //二級頁表項每項管理2M的空間
.quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE //fixmap最多2M的空間
/* 8MB reserved for vsyscalls + a 2MB hole = 4 + 1 entries */
.fill 5,8,0 //最後2M空間是個hole,還有8M給vsyscalls預留的空間
NEXT_PAGE(level1_fixmap_pgt)
.fill 512,8,0 //固定映射只是初始化了,但是present沒有設置,是不能使用的
附錄
asm中fill的用法爲:
.fill repeat , size , value //在該地址處重複repeat次,每次迭代地址增加size字節,填充值爲value
.quad value //在該地址放置4個字的數值,即8個字節