【原創】(一)ARMv8 MMU及Linux頁表映射


背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具: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概述

  1. MMU:完成的工作就是虛擬地址到物理地址的轉換,可以讓系統中的多個程序跑在自己獨立的虛擬地址空間中,相互不會影響。程序可以對底層的物理內存一無所知,物理地址可以是不連續的,但是不妨礙映射連續的虛擬地址空間。
  2. TLBMMU工作的過程就是查詢頁表的過程,頁表放置在內存中時查詢開銷太大,因此專門有一小片訪問更快的區域用於存放地址轉換條目,用於提高查找效率。當頁表內容有變化的時候,需要清除TLB,以防止地址映射出錯。
  3. Cache:處理器和存儲器之間的緩存機制,用於提高訪問速率,在ARMv8上會存在多級Cache,其中L1 Cache分為指令Cache數據Cache,在CPU Core的內部,支持虛擬地址尋址;L2 Cache容量更大,同時存儲指令和數據,為多個CPU Core共用,這多個CPU Core也就組成了一個Cluster

下圖淺黃色部分描述的就是一個地址轉換的過程。

由於上圖沒有體現出L1和L2 CacheMMU的關系,所以再來一張圖吧:

那具體是怎么訪問的呢?再來一張圖:

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的過程,簡直完美:

  1. 虛擬地址[63:39]用於區分內核空間與用戶空間,從而選擇不同的TTBRn寄存器來獲取Level 1頁表基地址
  2. 虛擬地址[38:30]放置Level 1頁表中的索引,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取Level 2頁表基地址;
  3. 虛擬地址[29:21]Level 2頁表中的索引,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取Level 3頁表基地址;
  4. 虛擬地址[20:12]Level 3頁表中的索引,從而找到對應的描述符地址並獲取描述符內容,根據描述符中的內容獲取物理地址的高36位,以4K地址對齊;
  5. 虛擬地址[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時,沒有PUDPMD頁表: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_dirswapper_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中,創建頁表相關的有三個宏:

  1. 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兩級頁表是物理連續的。

  1. 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

  1. 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的格式,理解起來就會順暢很多了。

一摳細節深似海,點到為止,防止一葉障目不見泰山,收工!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM