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

来源:https://www.cnblogs.com/LoyenWang/archive/2019/08/25/11406693.html
-Advertisement-
Play Games

背景 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 高爾基

說明:

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 公司的項目架構演進,我們也趁機嘗試遷移到netcore,系列隨筆講記錄我們的踩坑和填坑記錄。 HttpClient不行? 這是我們第一次嘗試netcore 簡要介紹環境 netcore2.2+aspnetcore2.2+windows 2008R2+SqlServer2008R2 問題場景 支付寶支 ...
  • 文章起源來自一篇博客: "使用 .NET CORE 創建 項目模板,模板項目,Template DeepThought 博客園" 之前使用Abp的時候就很認同Abp創建模板項目的方式。想不到.Net Core出了更贊的方式創建模板。之前寫過一個系列文章,有不少對Abp框架的改動(見文章: "基於.N ...
  • 前提 入行已經7,8年了,一直想做一套漂亮點的自定義控制項,於是就有了本系列文章。 GitHub:https://github.com/kwwwvagaa/NetWinformControl 碼雲:https://gitee.com/kwwwvagaa/net_winform_custom_contr ...
  • .NET Core 中三種模式依賴註入的生命周期簡要說明 ...
  • 電子發票是電商時代的產物,PDF發票是最常見的電子發票之一。在這篇文章中,我將給大家分享一個免費的動態生成PDF電子發票的C#方案,併在文章末尾附上Java解決方案。 典型的發票包含客戶和供應商的名稱和地址、發票編號、購買物品的描述、付款金額等信息。為了動態地生成發票,我使用MS Word創建了一個 ...
  • 在開發個人博客的時候,用到了騰訊移動分析(MTA),相比其他數據統計平臺來說我喜歡她的簡潔高效,易上手,同時文檔也比較全面,提供了數據介面供用戶調用。 在看了MTA演示 "Demo" 和 "官方文檔" 後,我就決定使用 .NET Core將其HTML5統計API進行封裝,以供博客直接調用,省去各種鑒 ...
  • 一.概述 本章Web架構分層指南,參考了“Microsoft應用程式體繫結構指南”(該書是在2009年出版的,當時出版是為了幫助開發人員和架構師更快速,更低風險地使用Microsoft平臺和.NET Framework設計和構建有效,高質量的應用程式)。雖然已過去十年了,技術架構已更新(如流行的DD ...
  • 若是在 Linux 中搭建了 FTP 伺服器,為了安全性,就要考慮磁碟配額,以防伺服器磁碟空間被惡意占滿。 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...