背景 By 魯迅 By 高爾基 說明: 1. Kernel版本:4.14 2. ARM64處理器,Contex A53,雙核 3. 使用工具:Source Insight 3.5, Visio 1. 介紹 要想理解好Linux的頁表映射,MMU的機制是需要去熟悉的,因此將這兩個模塊放到一起介紹。 關 ...
背景
Read the fucking source code!
--By 魯迅A picture is worth a thousand words.
--By 高爾基
說明:
- Kernel版本:4.14
- ARM64處理器,Contex-A53,雙核
- 使用工具:Source Insight 3.5, Visio
1. 介紹
要想理解好Linux的頁表映射,MMU的機制是需要去熟悉的,因此將這兩個模塊放到一起介紹。
關於ARMv8 MMU的相關內容,主要參考文檔:《ARM Cortex-A Series Programmer’s Guide for ARMv8-A》
。
2. ARMv8 MMU
2.1 MMU/TLB/Cache概述
MMU
:完成的工作就是虛擬地址到物理地址的轉換,可以讓系統中的多個程式跑在自己獨立的虛擬地址空間中,相互不會影響。程式可以對底層的物理記憶體一無所知,物理地址可以是不連續的,但是不妨礙映射連續的虛擬地址空間。TLB
:MMU
工作的過程就是查詢頁表的過程,頁表放置在記憶體中時查詢開銷太大,因此專門有一小片訪問更快的區域用於存放地址轉換條目
,用於提高查找效率。當頁表內容有變化的時候,需要清除TLB
,以防止地址映射出錯。Cache
:處理器和存儲器之間的緩存機制,用於提高訪問速率,在ARMv8上會存在多級Cache,其中L1 Cache
分為指令Cache
和數據Cache
,在CPU Core
的內部,支持虛擬地址定址;L2 Cache
容量更大,同時存儲指令和數據,為多個CPU Core
共用,這多個CPU Core
也就組成了一個Cluster
。
下圖淺黃色部分描述的就是一個地址轉換的過程。
由於上圖沒有體現出L1和L2 Cache
和MMU
的關係,所以再來一張圖吧:
那具體是怎麼訪問的呢?再來一張圖:
2.2 虛擬地址到物理地址的轉換
虛擬地址到物理地址的映射通過查表的機制來實現,ARMv8中,Kernel Space
的頁表基地址存放在TTBR1_EL1
寄存器中,User Space
頁表基地址存放在TTBR0_EL0
寄存器中,其中內核地址空間的高位為全1,(0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF)
,用戶地址空間的高位為全0,(0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF)
ARMv8中:
虛擬地址支持
64位虛擬地址中,並不是所有位都用上,除了高16位用於區分內核空間和用戶空間外,有效位的配置可以是:36, 39, 42, 47
。這可決定Linux內核中地址空間的大小。比如我使用的內核中有效位配置為CONFIG_ARM64_VA_BITS=39
,用戶空間地址範圍:0x00000000_00000000 ~ 0x0000007f_ffffffff
,大小為512G,內核空間地址範圍:0xffffff80_00000000 ~ 0xffffffff_ffffffff
,大小為512G。頁面大小支持
支持3種頁面大小:4KB, 16KB, 64KB
。頁表支持
支持至少兩級頁表,至多四級頁表,Level 0 ~ Level 3
。
結合有效虛擬地址位, 頁面大小,頁表的級數,可以組合成不同的頁表映射方式。
我使用的內核配置為:39位有效位,4KB大小頁面,3級頁表,所以我會以這個組合來介紹。
在ARMv8的手冊中剛好找到了下圖,描述了整個translation的過程,簡直完美:
- 虛擬地址[63:39]用於區分內核空間與用戶空間,從而選擇不同的
TTBRn寄存器
來獲取Level 1頁表基地址
; - 虛擬地址[38:30]放置
Level 1頁表中的索引
,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取Level 2頁表基地址
; - 虛擬地址[29:21]
Level 2頁表中的索引
,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取Level 3頁表基地址
; - 虛擬地址[20:12]
Level 3頁表中的索引
,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取物理地址的高36位,以4K地址對齊; - 虛擬地址[11:0]放置的是物理地址的偏移,結合獲取的物理地址高位,最終得到物理地址。
講到這裡還沒有完,是時候看一下Table Descriptor
了,也就是頁表中存放的內容,有以下四種類型:
類型有低兩位來決定,其中Level 0
中的Table Descriptor
只能輸出Level 1
頁表的地址,Level 3
中的Table Descriptor
只能輸出block addresses
。
看到圖中的attributes
了嗎,這些可以用於memory的許可權控制,memory ordering,cache policy的操作等。
在ARMv8中,與頁表相關的寄存器有:TCR_EL1, TTBRx_EL1
.
3. Linux頁表映射
3.1 Linux頁表基本操作
看過《深入理解Linux內核》的同學應該很熟悉下邊這張圖片,Linux的分頁模式(圖中以X86為例,頁表基地址由CR3寄存器指定):
在Linux內核中支持4級頁表的模型,同時適用於32位和64位系統。
那麼ARMv8與Linux內核是怎麼結合的呢?以我實際使用的設置(39位有效位,4KB大小頁面,3級頁表)為例,如下圖所示:
基本上內核中關於頁表的操作都會圍繞著上圖進行操作,似乎脫離了代碼有點不太合適,那麼就來一波fucking source code解析吧,主要講講各類page table相關的API。
代碼路徑:
arch/arm64/include/asm/pgtable-types.h
:定義pgd_t, pud_t, pmd_t, pte_t
等類型;
arch/arm64/include/asm/pgtable-prot.h
:針對頁表中entry中的許可權內容設置;
arch/arm64/include/asm/pgtable-hwdef.h
:主要包括虛擬地址中PGD/PMD/PUD等的劃分,這個與虛擬地址的有效位及分頁大小有關,此外還包括硬體頁表的定義, TCR寄存器中的設置等;
arch/arm64/include/asm/pgtable.h
:頁表設置相關;
在這些代碼中可以看到,
- 當
CONFIG_PGTABLE_LEVELS=4
時:pgd-->pud-->pmd-->pte
; - 當
CONFIG_PGTABLE_LEVELS=3
時,沒有PUD
頁表:pgd(pud)-->pmd-->pte
; - 當
CONFIG_PGTABLE_LEVELS=2
時,沒有PUD
和PMD
頁表:pgd(pud, pmd)-->pte
常用的巨集定義
頁表處理
/*描述各級頁表中的頁表項*/
typedef struct { pteval_t pte; } pte_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;
/* 將頁表項類型轉換成無符號類型 */
#define pte_val(x) ((x).pte)
#define pmd_val(x) ((x).pmd)
#define pud_val(x) ((x).pud)
#define pgd_val(x) ((x).pgd)
/* 將無符號類型轉換成頁表項類型 */
#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pud(x) ((pud_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
/* 獲取頁表項的索引值 */
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pud_index(addr) (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
#define pmd_index(addr) (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
/* 獲取頁表中entry的偏移值 */
#define pgd_offset(mm, addr) (pgd_offset_raw((mm)->pgd, (addr)))
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
#define pud_offset_phys(dir, addr) (pgd_page_paddr(*(dir)) + pud_index(addr) * sizeof(pud_t))
#define pud_offset(dir, addr) ((pud_t *)__va(pud_offset_phys((dir), (addr))))
#define pmd_offset_phys(dir, addr) (pud_page_paddr(*(dir)) + pmd_index(addr) * sizeof(pmd_t))
#define pmd_offset(dir, addr) ((pmd_t *)__va(pmd_offset_phys((dir), (addr))))
#define pte_offset_phys(dir,addr) (pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeof(pte_t))
#define pte_offset_kernel(dir,addr) ((pte_t *)__va(pte_offset_phys((dir), (addr))))
3.2 head.S中的頁表映射
3.2.1 idmap_pg_dir和swapper_pg_dir臨時頁表
是時候來個實例分析了,看看頁表的創建過程,代碼路徑:arch/arm64/kernel/head.S
。
內核啟動過程中,在真正的物理記憶體尚未添加進系統,以及頁表還未初始化之前,為了保證系統能正常運行,需要建立兩個臨時全局頁表:idmap_pg_dir
和swapper_pg_dir
:
其中兩個全局頁表的定義在arch/arm64/kernel/vmlinux.lds.S
中,放置在BSS段
之後:
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
/* 定義了連續的幾個頁,分別存放PGD,PMD,PTE等,連續在一起,這個也是head.S中填充的 */
#define SWAPPER_DIR_SIZE (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
idmap_pg_dir
從名字可以看出,identify map
,也就是物理地址和虛擬地址是相等的。為什麼需要這麼一個映射呢?我們都知道在MMU打開之前,CPU訪問的都是物理地址,那麼當MMU打開後訪問的就是虛擬地址了,這段頁表的映射就是從CPU到打開MMU之前的這段代碼物理地址的映射,防止開啟MMU後,無法獲取頁表。可以從System.map
文件中查看這些代碼:
swapper_pg_dir
Linux內核編譯後,kernel image是需要進行映射的,包括text,data
等各種段。
3.2.2 頁表創建
在head.S
中,創建頁表相關的有三個巨集:
create_pgd_entry
/*
* Macro to populate the PGD (and possibily PUD) for the corresponding
* block entry in the next level (tbl) for the given virtual address.
*
* Preserves: tbl, next, virt
* Corrupts: tmp1, tmp2
*/
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS > 3
create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
.endm
上述函數主要是調用create_table_entry
,由於SWAPPER_PGTABLES
配置為3,因此相當於創建了pgd和pmd
兩級頁表,此處需要註意一點,create_table_entry
函數執行後,tbl
參數會自動加上PAGE_SIZE
,也就是說pgd和pmd
兩級頁表是物理連續的。
create_block_map
/*
* Macro to populate block entries in the page table for the start..end
* virtual range (inclusive).
*
* Preserves: tbl, flags
* Corrupts: phys, start, end, pstate
*/
.macro create_block_map, tbl, flags, phys, start, end
lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT
lsr \start, \start, #SWAPPER_BLOCK_SHIFT
and \start, \start, #PTRS_PER_PTE - 1 // table index
orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT // table entry
lsr \end, \end, #SWAPPER_BLOCK_SHIFT
and \end, \end, #PTRS_PER_PTE - 1 // table end index
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
add \start, \start, #1 // next entry
add \phys, \phys, #SWAPPER_BLOCK_SIZE // next block
cmp \start, \end
b.ls 9999b
.endm
上述函數主要是往block
中填充pte entry
,真正創建虛擬地址到物理地址的映射,映射區域:start ~ end
。
create_table_entry
/*
* Macro to create a table entry to the next page.
*
* tbl: page table address
* virt: virtual address
* shift: #imm page table shift
* ptrs: #imm pointers per table page
*
* Preserves: virt
* Corrupts: tmp1, tmp2
* Returns: tbl -> next level table page address
*/
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #\ptrs - 1 // table index
add \tmp2, \tbl, #PAGE_SIZE
orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type
str \tmp2, [\tbl, \tmp1, lsl #3]
add \tbl, \tbl, #PAGE_SIZE // next level table page
.endm
上述函數創建頁表項,並且返回下一個Level的頁表地址。
上述三個孤立的函數並不直觀,所以,圖來了:
總體來說,頁表的創建過程相對來說還是比較易懂的,掌握好幾級頁表及各級頁表index所占的位域,此外熟悉各個Level頁表中entry的格式,理解起來就會順暢很多了。
一摳細節深似海,點到為止,防止一葉障目不見泰山,收工!