一、前言
在 嵌入式Linux 開發(fā)中,往往會聽到 MMU 這個詞,但大多數(shù)情況下并不會去了解它,因為操作系統(tǒng)已經(jīng)做好了關(guān)于 MMU 的一切操作,我們只需要在操作系統(tǒng)的框架下直接使用即可。但了解 MMU 有助于幫助我們理解操作系統(tǒng),理解進程等,讓我們對 嵌入式Linux 的理解上升一個層次。本文將簡單地講述一下關(guān)于 MMU 的基本信息。
注意:本文將按照ARMv7的二級頁表映射進行講述
二、MMU
2.1 MMU基本信息
MMU 全稱為 Memory Management Unit,即 內(nèi)存管理單元。在 帶有MMU的嵌入式Linux 中,CPU 訪問的地址都是 虛擬地址,而 MMU 負責將程序中 代碼或數(shù)據(jù) 的 虛擬地址 翻譯為 物理地址,以便程序訪問內(nèi)存。
在執(zhí)行操作時,MMU 會自動轉(zhuǎn)換 CPU發(fā)出的虛擬地址,無法人工進行操作,只需要配置好 MMU 相關(guān)屬性即可。
虛擬地址 是在 編譯和鏈接 時定義的,可以簡單地理解為 由鏈接器和鏈接器腳本 指定虛擬地址。
除了 翻譯虛擬地址,MMU 還可以配置 內(nèi)存區(qū)域 的各項配置,如內(nèi)存區(qū)域的訪問權(quán)限,內(nèi)存區(qū)域是否使能cache等功能。
總結(jié) MMU 的功能,如下:
- 翻譯虛擬地址
- 配置內(nèi)存區(qū)域的相關(guān)屬性
2.2 MMU基本概念
看到 MMU 的相關(guān)文章時,總會提及幾個概念如 頁,頁框(頁幀),頁表,頁表項,TLB等等,下面我們逐個拆分來講述。
2.2.1 頁
MMU 管理 虛擬地址空間 時,是按照 頁 為單位來進行管理。在 ARMv7 的 MMU ,頁大小 一共有 16M(Super Section)、1M(Section) 、64K(Large Page) 4K(Page)。頁大小 可以通過 協(xié)處理器CP15 進行配置,越小的頁意味著內(nèi)存的顆粒度越小,內(nèi)存使用時的浪費會越小,但也意味著使用的TLB行越多。越大的也內(nèi)存的顆粒度月大,內(nèi)存的使用浪費也可能月大,但使用的TLB行越少。比如只需要申請 7K 大小的 物理內(nèi)存,如果使用 7K大小 的內(nèi)存,我們可以分配 2 個 4K頁,如果分配 64K的大頁,則浪費的空間就比較大。
2.2.2 頁框
因為 虛擬地址空間 需要有所對應的 物理地址,這樣才能在 虛擬地址 中存儲數(shù)據(jù)。所以 MMU 管理 物理地址空間 時,按照 頁幀 為單位進行管理。其大小分為 64K 和 4K。一段 虛擬地址空間 有可能存在著多個 頁,這些 頁 對應著多個 頁幀。
按照筆者理解,頁 和 頁幀 是 不同地址空間下的關(guān)于內(nèi)存空間大小的概念。
2.2.3 頁表及頁表項
MMU 在進行 地址轉(zhuǎn)換 時,需要一些信息,存放這些信息的就是 頁表。每個 頁表 的最小單位就是 頁表項。
頁表 存儲在 物理地址空間 中,且一個 頁表項 對應著一個 頁。
在 切換頁表 時,通過將 頁表的物理首地址 設(shè)置到 協(xié)處理器CP15 中的 TTBR寄存器(Translation Table Base Register)。此后 MMU 會通過該地址自動去 物理地址空間 中找到對應的 頁表,從而完成 虛擬地址到物理地址的映射。
在不考慮 TLB 和 多級頁表 的情況下,可以簡單地如下圖所示:
2.2.4 TLB
TLB 全程為 Translation Lookaside Buffer,即 旁路轉(zhuǎn)換緩沖。它是 MMU 的專屬 全相聯(lián)cache,用于臨時存放 虛擬地址到物理地址映射 所需要的信息。
下面按照步驟說明 TLB 的作用:
- CPU 訪問 虛擬地址 到 MMU。
- MMU 根據(jù)規(guī)則(規(guī)則在下文講述)查看 虛擬地址 是否在 TLB 中。
- 如果在 TLB 中,則稱為 TLB命中。從 TLB 中直接獲取 物理地址 對內(nèi)存進行訪問
- 如果不在 TLB 中,則稱為 TLB失效。此時 MMU 將進行 translation table walking,即通過 訪問頁表來獲取 物理地址。并將該 虛擬地址 的信息存入 TLB,以便下次使用。
值得注意的是:ARM架構(gòu)的TLB只存儲有效的頁表項,對于無效的頁表項TLB并不會存儲
TLB 由許多 TLB行 組成,如下圖所示:
TLB行 由 3個 部分組成,分別為 標簽、ASID 和 描述符
- 標簽:該部分由 虛擬地址的一部分bit 組成,MMU 通過將 虛擬地址的一部分bit 和 TLB 的所有標簽對比進行搜索。
- ASID:全稱為 Address Space ID,一般用于 多進程系統(tǒng),下文會詳細講述。
- 描述符:由 2個 部分組成,分別為 物理地址(一部分bit) 和 內(nèi)存區(qū)域?qū)傩?/strong> 組成。可以理解為 cache 中的數(shù)據(jù)。
一般情況下,切換 進程 時會切換 頁表,因為隨著進程的切換, 虛擬地址 到 物理地址 的映射已經(jīng)改變。此時需要 清理TLB(即無效化TLB中的數(shù)據(jù)) 來保持 TLB一致性。清理TLB 一般通過 協(xié)處理器CP15 來完成,在 Linux內(nèi)核 中,有 flush_tlb_all() 和 flush_tlb_range() 函數(shù)來完成該工作。
2.3 MMU組成
如下圖所示:
MMU 的工作流程可以總結(jié)為下面 2 種情況:
- 訪問 虛擬地址 時,MMU 通過查找 TLB 來找出對應的 頁幀,從而訪問 物理地址,如圖中的 頁1、頁2 和 頁3。
- 如果 MMU 在 TLB 中沒找到對應的 TLB行 時,將進行 traslation table working。即從 物理地址空間 的 頁表中 找出對應的 頁表項,并根據(jù) 頁表項 找到對應的 物理地址。并將該 頁表項 更新到 TLB 中,以備下次使用。
2.4 MMU工作過程
ARMv7 下的 MMU 具有 2級頁表,分為 1級頁表 和 2級頁表。
2.4.1 1級頁表
1級頁表 也稱 主頁表 和 段頁表,下面簡稱 L1頁表。它將 4GB 的地址空間劃分為 4096 個 1MB 大小的 段,每個段的地址為 32bit。所以 1級頁表 擁有 4096 個 32bit 的 頁表項。
2.4.1.1 一級頁表項
L1頁表 使用了 短描述符頁表(Short-descriptor translation table),其 頁表項 具有以下特征:
- 32bit 的頁描述符
- 具有 2級 以上的 頁表
- 支持 32bit 的 物理地址
- 支持 4種 內(nèi)存大小:
- 16MB/1M,稱為 段
- 64KB/4KB,稱為 頁
在前面說了 TTBR寄存器 是存放 頁表物理地址 的寄存器,需要注意的是:存放在TTBR寄存器的地址需要16KB對齊
一級頁表項 一共有 4種 格式,如下圖所示:
每種格式都由 物理地址部分+屬性部分 組成,可以直接在圖中看出 物理地址部分 的示意,這里不多贅述。各種格式的含義如下:
- 1MB段轉(zhuǎn)換頁表項(Section) ,映射到 1MB 的物理地址范圍。其 物理地址部分 即為所需要映射的 物理基地址。
- 物理地址部分 指向 2級頁表 的 物理基地址。
- 16MB段(SuperSection) 轉(zhuǎn)換頁表項,是一種特殊的 1MB段轉(zhuǎn)換頁表項。其 物理地址部分 即為所需要映射的 物理基地址。
- 無效頁表項,當訪問該頁表項時,將觸發(fā) 指令取指異常 或 取數(shù)據(jù)異常
下面簡單說下各個字段的含義:
- Ignored:忽略
- Level 2 Descriptor Base Address:二級頁表物理基地址
- Section Base Address:1MB段基地址
- Supersection Base Address:16MB段基地址
- SBZ:全稱 should be zero,無效屬性字段
- AP:全稱 Access Permissions,內(nèi)存區(qū)域訪問權(quán)限
- Domain:用于權(quán)限控制,下文講述。
- TEX:全稱 Type extension,設(shè)置內(nèi)存區(qū)域類型
- B:全稱 Bufferable,是否設(shè)置 寫緩沖。
- C:全稱 Cacheable,是否設(shè)置 cache。
- nG:全稱 non-Global。如果 頁表項的nGbit 被設(shè)置,那么該 頁表項 對應的 內(nèi)存區(qū)域 將只能被 特定的進程 使用。當MMU 使用該 頁表項 進行映射時,也需要使用到 ASID。
- S:全稱 Shareable,共享設(shè)置項。
- bit[18]:該 bit 決定 段頁表項 是 1MB頁表項 還是 16MB頁表項。
-
bit[1:0]:這 2個bit 決定頁表項的類型,如下:
- 00:無效頁表項
- 01:轉(zhuǎn)換表頁表項
- 10:段頁表項
2.4.1.2 一級頁轉(zhuǎn)換
以 1MB段 舉例,假設(shè) L1頁表 的物理地址為 0x12300000,現(xiàn)在有一個虛擬地址 0x00100000。其轉(zhuǎn)換過程如圖所示:
- 查表過程:將 虛擬地址高12bit,即0x001 乘以 4 得到 0x004。0x004 即為 該虛擬地址所在段的頁表項在頁表中的偏移,所以 該虛擬地址對應的頁表項的物理地址為0x12300000+0x004=0x12300004。
- 根據(jù)查到的 頁表項,將 頁表項高12bit 和 虛擬地址低30bit 結(jié)合,即為 該虛擬地址在該1MB段內(nèi)的物理地址
值得注意的是:例子中,高12位一共是4096個頁表項,那么4096x4一共是16384字節(jié)的大小,因為每個頁表項是32位。所以4096個頁表項需要16K大小的內(nèi)存來存儲頁表。也是因為如此,每個虛擬地址的高12bit都需要乘以4.
下圖為例子的完整轉(zhuǎn)換過程,其余類型的 頁表項 轉(zhuǎn)換過程類似、
2.4.2 二級頁表
2級頁表 一共有 256 個 4字節(jié)大小 的 頁表項,總共占據(jù) 1KB大小 的內(nèi)存空間。L2頁表 的大部分內(nèi)容與 L1頁表 類似,相同部分下文將不再贅述
2.4.2 二級頁表項
二級頁表項 一共有 3種 格式,如下圖所示:
每種格式與 L1頁表項 一樣由 物理地址部分+屬性部分 組成,可以直接在圖中看出 物理地址部分 的示意,格式如下:
2級頁表項 具有以下特征:
- 粗頁表項: 其 物理地址部分指向 64KB大小 的 物理基地址。
- 細頁表項: 其 物理地址部分指向 4KB大小 的 物理基地址
- 無效頁表項,當訪問該頁表項時,將觸發(fā) 指令取指異常 或 取數(shù)據(jù)異常
屬性字段 的含義請參考 1級頁表 章節(jié)。
2.4.2 二級頁表轉(zhuǎn)換
L2頁表 的轉(zhuǎn)換過程與 L1頁表 的轉(zhuǎn)換過程一脈相承。以 4KB 為例子,如下圖所示:
由上圖可以看出其轉(zhuǎn)換步驟如下:
- 通過 虛擬地址 找出 L1頁表項 并轉(zhuǎn)換為 L2頁表 的 基地址。
- 根據(jù) L2頁表基地址 并集合 虛擬地址的[19:12]bit 找出 虛擬地址 對應的 L2頁表項。
- 將 虛擬地址[11:0]bit 和 L2頁表項 的 物理地址部分 結(jié)合得出具體的 物理地址
結(jié)合 L1頁表 的完整轉(zhuǎn)化過程如下圖所示:
2.5 MMU內(nèi)存屬性
2.5.1 內(nèi)存區(qū)域權(quán)限
每個 內(nèi)存區(qū)域 都有自己的權(quán)限,不符合訪問權(quán)限的 內(nèi)存訪問 都會引發(fā) 異常。如果是 數(shù)據(jù)訪問 則引發(fā) 數(shù)據(jù)異常。如果是 指令訪問,且該指令在執(zhí)行前沒有被 flush,將引發(fā) 預取指異常。
引發(fā)的 異常原因 將會被設(shè)置在 CP15 的 the fault address and fault status registers
內(nèi)存區(qū)域權(quán)限 由 AP、APX 和 Domain(域) 共同控制,如下:
-
AP/APX:字段 AP 和 APX 的不同組合將形成不同的 訪問權(quán)限,如圖所示。按照筆者理解,
Privileged 指的是 CPU 處于 svc 等狀態(tài),而 Unprivileged 則為 CPU 處于 user 狀態(tài)。
訪問權(quán)限組合表 -
Domain:這是一種 ARM架構(gòu) 不常用的 內(nèi)存全權(quán)限控制 方式。MMU 可以將所有 內(nèi)存區(qū)域 分配到 16個域 中,每一個 域 都有自己的 訪問權(quán)限。被分配到 域 中的 內(nèi)存區(qū)域 必須遵循該 域 的訪問權(quán)限。可以通過設(shè)置 頁表項 中的 Domain字段 來實現(xiàn) 域 的分配。
在 CP15協(xié)處理器 中有一個 DACR寄存器(Domain Access Control Register),該寄存器為 32bit,每個 域 的 訪問權(quán)限 由 2個bit 設(shè)置,一共設(shè)置 16個域。域 的權(quán)限設(shè)置如下:- 不可訪問(no-access):對該 域 的 內(nèi)存區(qū)域 進行訪問將引發(fā) 異常。
- 管理者(Manager mode):訪問不受任何控制,不產(chǎn)生 異常
- 用戶(Client mode):使用 頁表項 中的 AP/APX字段 進行控制
需要注意的是:內(nèi)存區(qū)域控制以域控制為主,頁表項的AP/APX字段為次。ARMv7不建議使用域進行控制,所以建議把DACR寄存器設(shè)置為用戶模式
2.5.2 內(nèi)存類型
ARM架構(gòu) 實現(xiàn)了 3種內(nèi)存類型,每種類型都是 互斥的,如下:
- Strongly-ordered
- Device
- Normal
每種 類型 的細節(jié)如下圖所示:
需要注意的是,Device類型的Shareable內(nèi)存區(qū)域現(xiàn)在已經(jīng)被棄用
內(nèi)存區(qū)域類型 可以通過 TEX字段、C字段 和 B字段 來進行設(shè)置,如下圖所示
值得注意的是:按照筆者理解,inner cache是L1 cache,而outer cache是指在L1cache下面的cache,比如L2cache
操作系統(tǒng)如何使用頁表
2.6 進程與MMU
操作系統(tǒng) 會為 每個進程 分配一個 頁表,該 頁表 使用 物理地址 存儲。當 進程 使用類似 malloc 等需要 映射代碼或數(shù)據(jù) 的操作時,操作系統(tǒng) 會在隨后馬上 修改頁表 以加入新的 物理內(nèi)存。當進程完成退出時,內(nèi)核會將相關(guān)的頁表項刪除掉,以便分配給新的進程。
2.6.1 Address Space ID
在操作系統(tǒng)中, 多進程 是一種常態(tài)。那么多進程 的情況下,每次 切換進程 都需要進行 TLB清理。這樣會導致切換的效率變低。
為了解決問題,TLB 引入了 ASID(Address Space ID) 。ASID 的范圍是 0-255。
ASID 由操作系統(tǒng)分配,當前進程的ASID值 被寫在 ASID寄存器(使用CP15 c3訪問)。TLB 在更新 頁表項 時也會將 ASID 寫入 TLB。
如果設(shè)置了如果 當前進程的ASID,那么 MMU 在查找 TLB 時, 只會查找 TLB 中具有 相同ASID值 的 TLB行。且在切換進程是,TLB 中被設(shè)置了 ASID 的 TLB行 不會被清理掉,當下次切換回來的時候還在。所以ASID 的出現(xiàn)使得切換進程時不需要清理 TLB 中的所有數(shù)據(jù),可以大大減少 切換開銷。
具體可以看參考鏈接《多核MMU和ASID管理邏輯》
2.6.2 TTBR0和TTBR1
前面講了 TTBR寄存器 是用于存放 頁表基地址,在 ARmv7 中一共有 2個 這樣的寄存器,分別是 TTBR0 和 TTBR1。
那么這里提出一個問題:在進行 Translation Table walking 的時候,選擇哪個TTBR寄存器,又如何選擇?
在 ARMv7 中,有一個寄存器為 TTBCR(TTB Control Register),即TTB控制寄存器。TTBCR寄存器 可以被設(shè)置為 0-7 這幾個值。
在進行 地址映射 時, MMU 會根據(jù) TTBCR寄存器 中的值查看 虛擬地址 是高位地址,根據(jù) 高位地址 選擇對應的 TTBR寄存器。
舉個例子,假設(shè) TTBCR寄存器 被設(shè)置為 4,則 MMU 會檢查 虛擬地址 的 高4bit,如果 高4bit 都為 0,則此時選擇 TTBR0。
需要注意的是:TTBCR被設(shè)置為 0 時,默認選擇 TTBR0。
下面我們看看使用和不使用 TTBR1 帶來的影響。
- 不使用:在 ARM32架構(gòu)的操作系統(tǒng)中,不使用 TTBR1寄存器。此時,用戶空間 和 內(nèi)核空間 共用一個 頁表。也就是說 用戶空間 和 內(nèi)核空間 都使用 TTBR0 來記錄 頁表地址,這樣可以避免一個問題,就是進行 用戶空間和內(nèi)核空間的切換時,可以避免切換頁表帶來的性能損耗。但與此同時也帶來一個問題,用戶空間 的 每個進程 都擁有 內(nèi)核頁表副本,當 內(nèi)核空間頁表 修改時,所有 進程 都需要同步修改其 內(nèi)核頁表副本。造成一定的性能損失
- 使用:ARM64架構(gòu)的操作系統(tǒng)中,虛擬地址空間 非常大。用戶空間 和 內(nèi)核空間 都是 256T。用戶空間地址高位為0,內(nèi)核空間地址高位為1。這樣的特性滿足 TTBR1寄存器 的使用條件。根據(jù) 用戶空間地址 和 內(nèi)核空間地址 的不同,選擇對應的 TTBR寄存器。這樣就不需要為每個 進程 維護一份 內(nèi)核頁表副本。
2.6.3 代碼實例
本小節(jié)簡單地講述一下 Linux 進行 MMU切換 時的代碼片段。以 ARMv7單核CPU 為例子。
根據(jù)筆者的理解,其調(diào)用圖譜如下:
->switch_mm
->check_and_switch_context
->cpu_switch_mm(processor.switch_mm)
->cpu_v7_switch_mm
筆者會將簡單的說明注釋在代碼中,不進行另外的說明。
/* arch/arm/include/asm/mmu_context.h */
static inline void
switch_mm(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk)
{
#ifdef CONFIG_MMU
unsigned int cpu = smp_processor_id();
/*
* __sync_icache_dcache doesn't broadcast the I-cache invalidation,
* so check for possible thread migration and invalidate the I-cache
* if we're new to this CPU.
*/
/* 這里應該是說進程如果調(diào)度到新的CPU,則需要將該CPU的cache給清理掉 */
if (cache_ops_need_broadcast() &&
!cpumask_empty(mm_cpumask(next)) &&
!cpumask_test_cpu(cpu, mm_cpumask(next)))
__flush_icache_all();
if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next)) || prev != next) {
/* 如果調(diào)度的進程不是本進程,則執(zhí)行check_and_switch_context */
check_and_switch_context(next, tsk);
if (cache_is_vivt())
cpumask_clear_cpu(cpu, mm_cpumask(prev));
}
#endif
}
static inline void check_and_switch_context(struct mm_struct *mm,
struct task_struct *tsk)
{
if (unlikely(mm->context.vmalloc_seq != init_mm.context.vmalloc_seq))
__check_vmalloc_seq(mm);
if (irqs_disabled())
/*
* cpu_switch_mm() needs to flush the VIVT caches. To avoid
* high interrupt latencies, defer the call and continue
* running with the old mm. Since we only support UP systems
* on non-ASID CPUs, the old mm will remain valid until the
* finish_arch_post_lock_switch() call.
*/
mm->context.switch_pending = 1;
else
/* 使用該函數(shù)進行MMU切換頁表 */
cpu_switch_mm(mm->pgd, mm);
}
/* arch/arm/include/asm/proc-fns.h */
/* 根據(jù)筆者找的代碼,cpu_switch_mm 應該直接調(diào)用了processor.switch_mm */
#define cpu_do_switch_mm processor.switch_mm
#define cpu_switch_mm(pgd,mm) cpu_do_switch_mm(virt_to_phys(pgd),mm)
processor.switch_mm 是一個 回調(diào)函數(shù),根據(jù)筆者找到的資料,應該是指向 ** arch/arm/mm** 目錄下的一些列 MMU 操作代碼。這里以 proc-v7-2level.S(即ARMv7 2級頁表) 進行說明
/* arch/arm/mm/proc-v7-2level.S */
/* 根據(jù)APCS,傳入的參數(shù)是存放在寄存器 r0和r1 */
ENTRY(cpu_v7_switch_mm)
#ifdef CONFIG_MMU
mmid r1, r1 @ get mm->context.id
ALT_SMP(orr r0, r0, #TTB_FLAGS_SMP)
ALT_UP(orr r0, r0, #TTB_FLAGS_UP)
#ifdef CONFIG_PID_IN_CONTEXTIDR
mrc p15, 0, r2, c13, c0, 1 @ read current context ID
lsr r2, r2, #8 @ extract the PID
bfi r1, r2, #8, #24 @ insert into new context ID
#endif
#ifdef CONFIG_ARM_ERRATA_754322
dsb
#endif
mcr p15, 0, r1, c13, c0, 1 @ set context ID
isb
/* 在這里,將r0所指向的頁表基地址設(shè)置到TTBR0中,完成頁表的切換 */
mcr p15, 0, r0, c2, c0, 0 @ set TTB 0
isb
#endif
bx lr
ENDPROC(cpu_v7_switch_mm)
三、參考鏈接
《ARM Cortex-A Series Programmer’s Guide》
《Cortex-A7 MPCore Technical Reference Manual》
《多核MMU和ASID管理邏輯》
TLB的作用及工作過程
MMU和cache詳解(TLB機制)
inux-kernel – Linux內(nèi)核ARM轉(zhuǎn)換表庫(TTB0和TTB1)
ARM TTBR0,TTBR1寄存器與ARM32頁表復制
選擇使用TTBR0或TTBR1做為translation table base地址寄存器
TLB中ASID和nG bit的關(guān)系
ASID
Linux arm 進程切換
ARM-LINUX的進程切換