linux內存-x86-64頁表初始化

頁表存儲着虛擬地址到物理地址的映射關係,同時爲了減少頁表的內存消耗發明了多級頁表,更多基礎內容可以看淺析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.
pgd extra 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

關於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程序的兼容性.

  1. 兼容32bit程序是當時64位設計時必然要考慮的問題,32bit系統存在的時代有很多優秀的軟件,這也是一種財富,不能到了64bit就不能繼續使用了,而且最大限度的保持兼容,不需要重新編譯即可運行.
    所以32bit程序的用戶空間是0-3G,在64bit空間中用戶空間地址是0-128TB,32bit程序運行時只佔據了它最低端的一部分空間,運行時空間的兼容性使得不需要重新編譯.另外是系統調用的兼容性,這裏不在贅述,看linux系統調用過程剖析

  2. 在用戶空間地址和內核空間中間有個大大的hole,它利用高16bit不參與尋址的特點,製造了這麼大一個hole,只能說有錢任性.

  3. 64TB的空間來做直接映射,也就是最大能夠支持64TB的線性映射內存,高於64TB估計也得在vmalloc中動態使用了,目前64TB的內存支持是足夠大了

  4. ffffffff80000000 - ffffffffa0000000這塊空間用來映射內核的文本段,數據段等,最大512M,物理內存區域和直接映射區域是交叉的,不過直接映射區域不訪問它,頁表映射一塊物理內存區域多次又有什麼關係呢?

  5. 其他區域就不再贅述了

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 

x86_64 地址空間
對於虛擬地址空間來說,這樣的劃分不是固定的,從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
  1. 初始化狀態

目前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的映射關係,計算方法類似.

  1. 編譯時初始化頁表的結果如下,在運行時會進行一些偏移校準

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
  1. 內核啓動早期的頁表初始化,在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個字節
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章