天天看點

頁表機制

轉自:

http://lli_njupt.0fees.net/ar01s12.html

12. 頁表機制
上一頁   下一頁

12. 頁表機制

12.1. 引言

在Linux系統中,存在以下三種位址:

  • 邏輯位址:它被包含在機器指令中用來指定一個操作數或一條指令的位址。每一個邏輯位址都由一個段(Segment)和偏移量(Offset)組成,偏移量指明了從段開始的地方到實際位址之間的距離。
  • 線性位址(虛拟位址):一個32位無符号整數,可以用來表示高達4GB的位址。線性位址通常用十六進制數字表示,值的範圍為[0x00000000, 0xffffffff)。
  • 實體位址:用于記憶體晶片級記憶體單元尋址。它們與從CPU的位址引腳發送到記憶體總線上的電信号相對應。實體位址由32位或36位無符号整數表示。

記憶體控制單元(MMU)通過一種稱為分段單元的硬體電路把一個邏輯位址轉換成線性位址;稱為分頁單元的硬體電路把線性位址轉換成一個實體位址。有些MMU沒有分頁單元,或者禁止使能分頁單元,比如x86的實模式,那麼就隻有分段單元,那麼經過分段單元轉換後的位址就是實體位址。有些MMU沒有分段單元,大多數RISC架構的CPU就是如此,此時段基址相當于0,而代碼中的偏移位址就是線性位址,所有Linux下邏輯位址和線性位址是一緻的。如下圖所示:

圖 60. 位址轉換

頁表機制

Linux中以非常有限的方式使用分段。運作在使用者态的所有Linux程序都使用一對相同的段來對指令和資料尋址,它們的段基址分别是__USER_CS和__USER_DS。與此同時,運作在核心态的所有Linux程序(核心線程)都使用一對相同的段對指令和資料尋址,它們的段基址分别__KERNEL_CS和__KERNEL_DS。

分段可以給每個程序配置設定不同的線性位址空間,分頁可以把同一線性位址空間映射到不同的實體空間。與分段相比,Linux更喜歡分頁方式,因為:

  • 當所有程序使用相同的段寄存器值時,記憶體管理變得更簡單,也就是說它們能共享同樣的一簇線性位址。
  • 為了相容絕大多數的CPU,RISC體系架構對分段的支援很有限,比如ARM架構的CPU中的MMU單元通常隻支援分頁,而不支援分段。

分頁使得不同的虛拟記憶體頁可以轉入同一實體頁框。于此同時分頁機制可以實作對每個頁面的通路控制,這是在平衡記憶體使用效率和位址轉換效率之間做出的選擇。如果4G的虛拟空間,每一個位元組都要使用一個資料結構來記錄它的通路控制資訊,那麼顯然是不可能的。如果把4G的虛拟空間以4K(為什麼是4K大小?這是由于Linux中的可執行檔案中的代碼段和資料段等都是相對于4K對齊的)大小分割成若幹個不同的頁面,那麼每個頁面需要一個資料結構進行控制,隻需要1M的記憶體來存儲。但是由于每一個使用者程序都有自己的獨立空間,是以每一個程序都需要一個1M的記憶體來存儲頁表資訊,這依然是對系統記憶體的浪費,采用兩級甚至多級分頁是一種不錯的解決方案。另外有些處理器采用64位體系架構,此時兩級也不合适了,是以Linux使用三級頁表。

  • 頁全局目錄(Page Global Directory),即 pgd,是多級頁表的抽象最高層。每一級的頁表都處理不同大小的記憶體。每項都指向一個更小目錄的低級表,是以pgd就是一個頁表目錄。當代碼周遊這個結構時(有些驅動程式就要這樣做),就稱為是在周遊頁表。
  • 頁中間目錄 (Page Middle Directory),即pmd,是頁表的中間層。在 x86 架構上,pmd 在硬體中并不存在,但是在核心代碼中它是與pgd合并在一起的。
  • 頁表條目 (Page Table Entry),即pte,是頁表的最低層,它直接處理頁,該值包含某頁的實體位址,還包含了說明該條目是否有效及相關頁是否在實體記憶體中的位。

12.2. 一級頁表

三級頁表由不同的的資料結構表示,它們分别是pgd_t,pmd_t和pte_t。注意到它們均被定義為unsigned long類型,也即大小為4bytes,32bits。

arch/arm/include/asm/page.h

typedef unsigned long pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;
      

以下是頁表操作相關的宏定義。

#define pte_val(x)      (x)
#define pmd_val(x)      (x)
#define pgd_val(x)      ((x)[0])
#define pgprot_val(x)   (x)

#define __pte(x)        (x)
#define __pmd(x)        (x)
#define __pgprot(x)     (x)
      

任何一個使用者程序都有自己的頁表,與此同時,核心本身就是一個名為init_task的0号程序,每一個程序都有一個mm_struct結構管理程序的記憶體空間,init_mm是核心的mm_struct。在系統引導階段,首先通過__create_page_tables在核心代碼的起始處_stext向低位址方向預留16K,用于一級頁表(主記憶體頁表)的存放,每個程序的頁表都通過mm_struct中的pgd描述符進行引用。核心頁表被定義在swapper_pg_dir。

arch/arm/kernel/init_task.c

#define INIT_MM(name) \
{                                                               \
        .mm_rb          = RB_ROOT,                              \
        .pgd            = swapper_pg_dir,                       \
        .mm_users       = ATOMIC_INIT(2),                       \
        .mm_count       = ATOMIC_INIT(1),                       \
        .mmap_sem       = __RWSEM_INITIALIZER(name.mmap_sem),   \
        .page_table_lock =  __SPIN_LOCK_UNLOCKED(name.page_table_lock), \
        .mmlist         = LIST_HEAD_INIT(name.mmlist),          \
        .cpu_vm_mask    = CPU_MASK_ALL,                         \
}

struct mm_struct init_mm = INIT_MM(init_mm); 
      

swapper_pg_dir在head.S中被定義為PAGE_OFFSET向上偏移TEXT_OFFSET。TEXT_OFFSET代表核心代碼段的相對于PAGE_OFFSET的偏移。KERNEL_RAM_VADDR的值與_stext的值相同,代表了核心代碼的起始位址。swapper_pg_dir為KERNEL_RAM_VADDR - 0x4000,也即向低位址方向偏移了16K。

arch/arm/Makefile

textofs-y       := 0x00008000
......
TEXT_OFFSET := $(textofs-y)
      

特定系統架構的Makefile中通過textofs-y定義了核心起始代碼相對于PAGE_OFFSET的偏移。

arch/arm/kernel/head.S
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
......
.globl  swapper_pg_dir
.equ    swapper_pg_dir, KERNEL_RAM_VADDR - 0x4000
      

ARM Linux中的主記憶體頁表,使用段表。每個頁表映射1M的記憶體大小,由于16K / 4 * 1M = 4G,這16K的首頁表空間正好映射4G的虛拟空間。核心頁表機制在系統啟動過程中的paging_init函數中使能,其中對核心首頁表的初始化等操作均是通過init_mm.pgd的引用來進行的。在系統執行paging_init之前,系統的位址空間如下圖所示:

圖 61. 核心RAM布局

頁表機制

圖中的黃色部分就是核心0号程序的首頁表。

arch/arm/mm/mmu.c

void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
	void *zero_page;

	build_mem_type_table();
	sanity_check_meminfo(mi);
	prepare_page_table(mi);
	
	bootmem_init(mi);
	devicemaps_init(mdesc);

	top_pmd = pmd_off_k(0xffff0000);

	zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
	memzero(zero_page, PAGE_SIZE);
	empty_zero_page = virt_to_page(zero_page);
	flush_dcache_page(empty_zero_page);
}
      

圖 62. ARM記憶體首頁表初始化

頁表機制

paging_init依次完成了以下工作:

  • 調用prepare_page_table初始化虛拟位址[0, PAGE_OFFSET]和[mi->bank[0].start + mi->bank[0].size, VMALLOC_END]所對應的首頁表項,所有表項均初始化為0。這裡保留了核心代碼區,首頁表區以及Bootmem機制中的位圖映射區對應的首頁表。這是為了保證核心代碼的執行以及對首頁表區和位圖區的通路。如果隻有一個記憶體bank,那麼mi->bank[0].start + mi->bank[0].size的值和high_memory保持一緻,它是目前bank進行實體記憶體一一映射後的虛拟位址。對于一個記憶體為256M的系統來說,它隻有一個bank,經過prepare_page_table處理後的記憶體如上圖所示。
  • 接着在bootmem_init函數将通過bootmem_init_node對每一個記憶體bank的頁表進行值的填充。bootmem_init_node将通過map_memory_bank間接調用create_mapping,最終由該函數建立頁表。
  • 通過devicemaps_init初始化裝置I/O對應的相關頁表。
  • 最後建立0頁表,并在Dcache中清空0頁表的緩存資訊。

圖 63. 頁表建立函數調用

頁表機制

12.3. ARM 記憶體通路

當ARM要通路記憶體RAM時,MMU首先查找TLB中的虛拟位址表,如果ARM的結構支援分開的位址TLB和指令TLB,那麼它用:

  • 取指令使用指令TLB
  • 其它的所有通路類别用資料TLB

指令TLB和資料TLB在ARMv6架構的MMU中被分别稱為指令MicroTLB和資料MicroTLB。如果沒有命中MicroTLB,那麼将查詢主TLB,此時不區分指令和資料TLB。

如果TLB中沒有虛拟位址的入口,則轉換表周遊硬體從存在主存儲器中的轉換表中擷取轉換頁表項,它包含了實體位址或者二級頁表位址和通路權限,一旦取到,這些資訊将被放在TLB中,它會放在一個沒有使用的入口處或覆寫一個已有的入口。一旦為存儲器通路的TLB 的入口被拿到,這些資訊将被用于:

  • C(高速緩存)和B(緩沖)位被用來控制高速緩存和寫緩沖,并決定是否高速緩存。
  • 首先檢查域位,然後檢查通路權限位用來控制通路是否被允許。如果不允許,則MMU 将向ARM處理器發送一個存儲器異常;否則通路将被允許進行。
  • 對沒有或者禁止高速緩存的系統(包括在沒有高速緩存系統中的所有存儲器通路),實體位址将被用作主存儲器通路的位址。

圖 64. 高速緩存的MMU存儲器系統

頁表機制

12.4. ARM MMU頁表

在ARMv6的MMU機制中,提供了兩種格式的頁表描述符:

  • 相容ARMv4和ARMv5 MMU機制的頁表描述符。這種描述符可以對64K大頁面和4K小頁面再進一步細分為子頁面。
  • ARMv6特有的MMU頁表描述符,這種頁表描述符内增加了額外的特定比特位:Not-Global(nG),Shared (S),Execute-Never (XN)和擴充的通路控制位APX。

圖 65. 向前相容的一級頁表描述符格式

頁表機制

圖 66. 向前相容的二級頁表描述符格式

頁表機制

Linux使用ARMv6特有的MMU頁表描述符格式,它們的标志位描述如下:

圖 67. ARMv6一級頁表描述符格式

頁表機制

表 20. ARMv6一級頁表描述符比特位含義

标志 含義
b[1:0] 類型

訓示頁表類型:b00 錯誤項;b11 保留;b01粗頁表,它指向二級頁表基址。

b10:1MB大小段頁表(b[18]置0)或16M大小超級段頁表(b[18]置1)

b[2] B[a] 寫緩沖使能[b]
b[4] Execute-Never(XN) 禁止執行标志:1,禁止執行;0:可執行
b[5:8] 域(domain) 指明所屬16個域的哪個域,通路權限由CP15的c3寄存器據定
b[9] P(ECC Enable) ECC使能标志,1:該頁表映射區使能ECC校驗[c]
b[10:11] AP(Access Permissions) 通路權限位,具體見通路權限清單
b[12:14] TEX(Type Extension Field) 擴充類型,與B,C标志協同控制記憶體通路類型
bit[15] APX(Access Permissions Extension Bit) 擴充通路權限位
bit[16] S(Shared) 共享通路
bit[17] nG(Not-Global) 全局通路
bit[18] 0/1 段頁表和超級段頁表開關
bit[19] NS

[a]高速緩存和寫緩存的引入是基于如下事實,即處理器速度遠遠高于存儲器通路速度;如果存儲器通路成為系統性能的瓶頸,則處理器再快也是浪費,因為處理器需要耗費大量的時間在等待存儲器上面。高速緩存正是用來解決這個問題,它可以存儲最近常用的代碼和資料,以最快的速度提供給CPU處理(CPU通路Cache不需要等待)。

[b]SBZ意味置0,該位在粗頁表中置0。

[c]ARM1176JZF-S處理器不支援該标志位。

Linux 在ARM體系架構的Hardware page table頭檔案中通過宏定義了這些位。

arch/arm/include/asm/pgtable-hwdef.h

/*
 * Hardware page table definitions.
 *
 * + Level 1 descriptor (PMD)
 *   - common
 */
#define PMD_TYPE_MASK           (3 << 0) // 擷取一級頁表類型的掩碼,它取bit[0:1]
#define PMD_TYPE_FAULT          (0 << 0) // 置bit[0:1]為b00,錯誤項
#define PMD_TYPE_TABLE          (1 << 0) // 置bit[0:1]為b01,粗頁表
#define PMD_TYPE_SECT           (2 << 0) // 置bit[0:1]為b10,段頁表
#define PMD_BIT4                (1 << 4) // 定義bit[4],禁止執行标志位
#define PMD_DOMAIN(x)           ((x) << 5) // 擷取域标志位b[5:8]
#define PMD_PROTECTION          (1 << 9)   // b[9]ECC使能标志
      

以上定義了一級頁表的相關标志位。Linux使用段頁表作為一級頁表,粗頁表作為二級頁表的基址頁表。段頁表的标志位定義如下:

#define PMD_SECT_BUFFERABLE     (1 << 2)	
#define PMD_SECT_CACHEABLE      (1 << 3)
#define PMD_SECT_XN             (1 << 4)        /* v6 */
#define PMD_SECT_AP_WRITE       (1 << 10)
#define PMD_SECT_AP_READ        (1 << 11)
#define PMD_SECT_TEX(x)         ((x) << 12)     /* v5 */
#define PMD_SECT_APX            (1 << 15)       /* v6 */
#define PMD_SECT_S              (1 << 16)       /* v6 */
#define PMD_SECT_nG             (1 << 17)       /* v6 */
#define PMD_SECT_SUPER          (1 << 18)       /* v6 */
      
頁表機制

圖 68. ARMv6二級頁表基址格式

頁表機制

二級頁表相同标志位的含義與一級頁表相同,這裡不再單獨列出。注意它的b[1]為1時,b[0]表示禁止執行标志。Linux對二級頁表中的标志位定義如下:

/*
 * + Level 2 descriptor (PTE)
 *   - common
 */
#define PTE_TYPE_MASK           (3 << 0) // 擷取二級頁表類型的掩碼,它取bit[0:1]
#define PTE_TYPE_FAULT          (0 << 0) // 置bit[0:1]為b00,錯誤項
#define PTE_TYPE_LARGE          (1 << 0) // 置bit[0:1]為b01,大頁表(64K)
#define PTE_TYPE_SMALL          (2 << 0) // 置bit[0:1]為b10,擴充小頁表(4K)
#define PTE_TYPE_EXT            (3 << 0) // 使能禁止執行标志的擴充小頁表(4K)
#define PTE_BUFFERABLE          (1 << 2) // B标志
#define PTE_CACHEABLE           (1 << 3) // C标志
      

Linux二級頁表使用擴充小頁表,這樣每個二級頁表可以表示通常的1個頁面大小(4K)。Linux對二級頁表标志位的定義如下:

/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN              (1 << 0)        /* v6 */
#define PTE_EXT_AP_MASK         (3 << 4)
#define PTE_EXT_AP0             (1 << 4)
#define PTE_EXT_AP1             (2 << 4)
#define PTE_EXT_AP_UNO_SRO      (0 << 4)
#define PTE_EXT_AP_UNO_SRW      (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW      (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW      (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)          ((x) << 6)      /* v5 */
#define PTE_EXT_APX             (1 << 9)        /* v6 */
#define PTE_EXT_COHERENT        (1 << 9)        /* XScale3 */
#define PTE_EXT_SHARED          (1 << 10)       /* v6 */
#define PTE_EXT_NG              (1 << 11)       /* v6 */
      

以上兩種頁表轉換機制由CP15協處理器的控制寄存器c1中的bit23來選擇。bit23為0時為第一種機制,否則為第二種。在CPU初始化後該位的預設值為0。Linux在系統引導時會設定MMU的控制寄存器的相關位,其中把bit23設定為1,是以Linux在ARMv6體系架構上采用的是ARMv6 MMU頁表轉換機制。

arch/arm/mm/proc-v6.S

__v6_setup:
......
        adr     r5, v6_crval
        ldmia   r5, {r5, r6}
        mrc     p15, 0, r0, c1, c0, 0           @ read control register
        bic     r0, r0, r5                      @ clear bits them
        orr     r0, r0, r6                      @ set them
        mov     pc, lr                          @ return to head.S:__ret

        /*
         *         V X F   I D LR
         * .... ...E PUI. .T.T 4RVI ZFRS BLDP WCAM
         * rrrr rrrx xxx0 0101 xxxx xxxx x111 xxxx < forced
         *         0 110       0011 1.00 .111 1101 < we want
         */
        .type   v6_crval, #object
v6_crval:
        crval   clear=0x01e0fb7f, mmuset=0x00c0387d, ucset=0x00c0187c
      

注意到v6_crval定義了三個常量,首先mrc指令讀取c1到r0,然後清除clear常量指定的比特位,然後設定mmuset指定的比特位,其中bit23為1。在mov pc, lr跳轉後将執行定義在head.S中的__enable_mmu函數,在進一步調節其它的比特位後最終将把r0中的值寫回c1寄存器。

頁表機制

12.5. 頁面通路控制

在談到create_mapping之前,必須說明一下Linux是如何實作對頁面的通路控制的。它定義了一個類型為struct mem_type的局部靜态數組。根據不同的映射類型,它定義了不同的通路權限,它通過md參數中的type成員傳遞給create_mapping。

arch/arm/include/asm/io.h
/*
 * Architecture ioremap implementation.
 */
#define MT_DEVICE               0
#define MT_DEVICE_NONSHARED     1
#define MT_DEVICE_CACHED        2
#define MT_DEVICE_WC            3

arch/arm/include/asm/mach/map.h
/* types 0-3 are defined in asm/io.h */
#define MT_UNCACHED             4
#define MT_CACHECLEAN           5
#define MT_MINICLEAN            6
#define MT_LOW_VECTORS          7
#define MT_HIGH_VECTORS         8
#define MT_MEMORY               9
#define MT_ROM                  10
      

系統中定義了多個映射類型,最常用的是MT_MEMORY,它對應RAM;MT_DEVICE則對應了其他I/O裝置,應用于ioremap;MT_ROM對應于ROM;MT_LOW_VECTORS對應0位址開始的向量;MT_HIGH_VECTORS對應高位址開始的向量,它有vector_base宏決定。

arch/arm/mm/mm.h

struct mem_type {
        unsigned int prot_pte;
        unsigned int prot_l1;
        unsigned int prot_sect;
        unsigned int domain;
};
      

盡管Linux在多數系統上實作或者模拟了3級頁表,但是在ARM Linux上它隻實作了首頁表和兩級頁表。首頁表通過ARM CPU的段表實作,段表中的每個頁表項管理1M的記憶體,虛拟位址隻需要一次轉換既可以得到實體位址,它通常存放在swapper_pg_dir開始的16K區域内。兩級頁表隻有在被映射的實體記憶體塊不滿足1M的情況下才被使用,此時它由L1和L2組成。

  • prot_pte和prot_l1分别對應兩級頁表中的L2和L1,分别代表了頁表項的通路控制位,其中prot_l1中還包含記憶體域
  • prot_sect代表首頁表的通路控制位和記憶體域。
  • domain代表了記憶體域。在ARM處理器中,MMU将整個存儲空間分成最多16個域,記作D0~D15,每個域對應一定的存儲區域,該區域具有相同的通路控制屬性。
arch/arm/include/asm/domain.h

#define DOMAIN_KERNEL   0
#define DOMAIN_TABLE    0
#define DOMAIN_USER     1
#define DOMAIN_IO       2
      

ARM Linux 中隻是用了16個域中的三個域D0-D2。它們由上面的宏來定義,在系統引導時初始化MMU的過程中将對這三個域設定域通路權限。以下是記憶體空間和域的對應表:

表 21. 記憶體空間和域的對應表

記憶體空間
裝置空間 DOMAIN_IO
内部高速SRAM空間/内部MINI Cache空間 DOMAIN_KERNEL
RAM記憶體空間/ROM記憶體空間 DOMAIN_KERNEL
高低端中斷向量空間 DOMAIN_USER

在ARM處理器中,MMU中的每個域的通路權限分别由CP15的C3寄存器中的兩位來設定,c3寄存器的小為32bits,剛好可以設定16個域的通路權限。下表列出了域的通路控制字段不同取值及含義:

表 22. ARM記憶體通路控制字

通路類型 含義
0b00 無通路權限 此時通路該域将産生通路失效
0b01 使用者(client) 根據CP15的C1控制寄存器中的R和S位以及頁表中位址變換條目中的通路權限控制位AP來确定是否允許各種系統工作模式的存儲通路
0b10 保留 使用該值會産生不可預知的結果
0b11 管理者(Manager) 不考慮CP15的C1控制寄存器中的R和S位以及頁表中位址變換條目中的通路權限控制位AP,在這種情況下不管系統工作在特權模式還是使用者模式都不會産生通路失效

Linux定義了其中可以使用的三種域控制:

arch/arm/include/asm/domain.h

#define DOMAIN_NOACCESS 0
#define DOMAIN_CLIENT   1
#define DOMAIN_MANAGER  3
      

Linux在系統引導設定MMU時初始化c3寄存器來實作對記憶體域的通路控制。其中對DOMAIN_USER,DOMAIN_KERNEL和DOMAIN_TABLE均設定DOMAIN_MANAGER權限;對DOMAIN_IO設定DOMAIN_CLIENT權限。如果此時讀取c3寄存器,它的值應該是0x1f。

arch/arm/include/asm/domain.h
#define domain_val(dom,type)    ((type) << (2*(dom)))

arch/arm/kernel/head.S
    ......	
    mov     r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
                  domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
                  domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
                  domain_val(DOMAIN_IO, DOMAIN_CLIENT))
    mcr     p15, 0, r5, c3, c0, 0           @ load domain access register
    mcr     p15, 0, r4, c2, c0, 0           @ load page table pointer
    b       __turn_mmu_on
ENDPROC(__enable_mmu)
      

在系統的引導過程中對這3個域的通路控制位并不是一成不變的,它提供了一個名為modify_domain的宏來修改域通路控制位。系統在setup_arch中調用early_trap_init後,DOMAIN_USER的權限位将被設定成DOMAIN_CLIENT,此時它的值應該是0x17。

arch/arm/include/asm/domain.h

#define set_domain(x)                                   \
        do {                                            \
        __asm__ __volatile__(                           \
        "mcr    p15, 0, %0, c3, c0      @ set domain"   \
          : : "r" (x));                                 \
        isb();                                          \
        } while (0)

#define modify_domain(dom,type)                                 \
        do {                                                    \
        struct thread_info *thread = current_thread_info();     \
        unsigned int domain = thread->cpu_domain;               \
        domain &= ~domain_val(dom, DOMAIN_MANAGER);             \
        thread->cpu_domain = domain | domain_val(dom, type);    \
        set_domain(thread->cpu_domain);                         \
        } while (0)
      

通路權限由CP15的c1控制寄存器中的R和S位以及頁表項中的通路權限控制位AP[0:1]以及通路權限擴充位APX來确定,通過R和S的組合控制方式在第一項中說明,并且已不被推薦使用。具體說明如下表所示。

表 23. MMU中存儲通路權限控制[8]

APX AP[1:0] 特權模式通路權限 使用者模式通路權限
b00 禁止通路;S=1,R=0或S=0,R=1時隻讀 禁止通路;S=1,R=0時隻讀
b01 讀寫 禁止通路
b10 讀寫 隻讀
b11 讀寫 讀寫
1 b00 保留 保留
1 b01 隻讀 禁止通路
1 b10 隻讀 隻讀
1 b11 隻讀 隻讀
[8]參考ARM1176JZF-S Revision: r0p7->6.5.2 Access permissions
static struct mem_type mem_types[] = {
	......
	[MT_MEMORY] = {
		.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
		.domain    = DOMAIN_KERNEL,
	},
	......
};
      

對于MT_MEMORY的記憶體映射類型,依據段頁表的各位功能,定義了如下的宏,顯然PMD_TYPE_SECT定義了段頁表類型0b10,PMD_SECT_AP_WRITE和PMD_SECT_AP_READ則對應AP[0]和AP[1]通路權限控制位。根據 表 23 “MMU中存儲通路權限控制”,在使用AP[0:1]進行權限控制時,CP15中的C1寄存器中的S和R标志位不影響權限,根據AP權限位的意義,并不能直接根據宏的字尾名得出是對讀還是寫的控制。

arch/arm/include/asm/pgtable-hwdef.h

#define PMD_TYPE_TABLE          (1 << 0)
#define PMD_TYPE_SECT           (2 << 0)
#define PMD_BIT4                (1 << 4)
#define PMD_DOMAIN(x)           ((x) << 5)
......
#define PMD_SECT_AP_WRITE       (1 << 10)
#define PMD_SECT_AP_READ        (1 << 11)
      

一個描述了目前系統mem_types描述的所有記憶體映射類型權限控制的清單如下所示:

表 24. ARM Linux記憶體映射權限控制[9]

記憶體映射類型 域定義 段頁表項權限定義 L1頁表項權限定義 PTE項權限定義
MT_DEVICE DOMAIN_IO

PROT_SECT_DEVICE[a]

PMD_SECT_S

PMD_TYPE_TABLE

PROT_PTE_DEVICE[b]

L_PTE_MT_DEV_SHARED

L_PTE_SHARED

MT_DEVICE_NONSHARED DOMAIN_IO PROT_SECT_DEVICE PMD_TYPE_TABLE

PROT_PTE_DEVICE

L_PTE_MT_DEV_NONSHARED

MT_DEVICE_CACHED DOMAIN_IO

PROT_SECT_DEVICE

PMD_SECT_WB

PMD_TYPE_TABLE

PROT_PTE_DEVICE

L_PTE_MT_DEV_CACHED

MT_DEVICE_WC DOMAIN_IO PROT_SECT_DEVICE PMD_TYPE_TABLE

ROT_PTE_DEVICE

L_PTE_MT_DEV_WC

MT_UNCACHED DOMAIN_IO

PMD_TYPE_SECT

PMD_SECT_XN

PMD_TYPE_TABLE PROT_PTE_DEVICE
MT_CACHECLEAN DOMAIN_KERNEL

PMD_TYPE_SECT

PMD_SECT_XN

MT_MINICLEAN DOMAIN_KERNEL

PMD_TYPE_SECT

PMD_SECT_XN

PMD_SECT_MINICACHE,

MT_LOW_VECTORS DOMAIN_USER PMD_TYPE_TABLE

L_PTE_PRESENT

L_PTE_YOUNG

L_PTE_DIRTY

L_PTE_EXEC

MT_HIGH_VECTORS DOMAIN_USER PMD_TYPE_TABLE

L_PTE_PRESENT

L_PTE_YOUNG

L_PTE_DIRTY

L_PTE_USER

L_PTE_EXEC

MT_MEMORY DOMAIN_KERNEL

PMD_TYPE_SECT

PMD_SECT_AP_WRITE

MT_ROM DOMAIN_KERNEL PMD_TYPE_SECT

[9]該表描述了Linux2.6.28 ARM體系架構的mem_types記憶體映射權限控制

[a]在mmu.c中它被定義為 PMD_TYPE_SECT|PMD_SECT_AP_WRITE

[b]在mmu.c中它被定義為 L_PTE_PRESENT|L_PTE_YOUNG|L_PTE_DIRTY|L_PTE_WRITE

build_mem_type_table函數在paging_init執行的開始被調用,它将根據目前CPU的特性進一步調整這些通路權限位。

  • MT_MEMORY被用來映射主存RAM。它隻有段頁表,對應通路權限中的第二條:特權模式可以讀寫,使用者模式禁止通路。

12.6. create_mapping

create_mapping是完成頁表創的核心函數。它的聲明如下:

arch/arm/mm/mmu.c

void __init create_mapping(struct map_desc *md);
      

create_mapping隻有一個類型為struct map_desc的參數。這是一個非常簡單的參數,但是包含了建立頁表相關的所有資訊。

  • virtual記錄了映射到的虛拟位址的開始。
  • pfn指明了被映射的實體記憶體的起始頁框。
  • length指明了被映射的實體記憶體的大小,注意這裡不是頁框大小。對于256M的實體記憶體,這裡的值為0x10000000。
  • type指明映射類型,MT_xxx,決定了相應映射頁面的通路權限。
arch/arm/include/asm/mach/map.h

struct map_desc {
        unsigned long virtual;
        unsigned long pfn;
        unsigned long length;
        unsigned int type;
};
      

在Bootmem機制應用中有提到,系統中所有的記憶體塊都在啟動時被注冊到meminfo中以struct membank類型的數組形式存在。map_memory_bank的作用就是将以struct membank類型的記憶體節點轉換為struct map_desc類型然後傳遞給create_mapping。

struct meminfo {
  .nr_banks = 1;
  bank[8] = 
  {
    {
      .start = 0x50000000;
      .size = 0x10000000;
      .node = 0;
    };
   ...
  }
};
      

對于隻有一個大小為256M實體RAM記憶體的系統來說,如果它具有以上的struct membank類型的記憶體資訊,那麼create_mapping得到的參數md如下所示:

map_desc {
    .virtual = 0xc0000000;
    .pfn = 0x50000;
    .length = 0x10000000;
    .type = MT_MEMORY;
};

[MT_MEMORY] = {
	.prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
	.domain    = DOMAIN_KERNEL,
}
      

create_mapping依次完成了以下工作:

  • 首先根據傳入的virtual虛拟位址與中斷向量起始位址比較,除特殊的中斷向量使用的虛拟位址可能落在0位址外,確定映射到的虛拟位址落在核心空間。通過vectors_base宏,擷取中斷向量的起始位址,這是由于ARMv4以下的版本,該位址固定為0;ARMv4及以上版本,ARM中斷向量表的位址由CP15協處理器c1寄存器中的V位(bit[13])控制,如果V位為1,那麼該位址為0xffff0000。考慮到除了ARMv4以下的版本的中斷向量所在的虛拟位址為0,其他所有映射到的虛拟位址應該都在位址0xc0000000之上的核心空間,而不可能被映射到使用者空間。
  • 接着根據type類型判斷是否為普通裝置或者ROM,這些裝置中的記憶體不能被映射到RAM映射的區域[PAGE_OFFSET,high_memory),也不能被映射到VMALLOC所在的區域[high_memory,VMALLOC_END),由于這兩個區域是連續的,中間隔了8M的VMALLOC_OFFSET隔離區,準确來說是不能映射到[VMALLOC_START,VMALLOC_END),但是這一隔離區為了使能隔離之用也是不能被映射的。
  • 根據pfn與1G記憶體對應的最大實體頁框0x100000比較,如果實體記憶體的起始位址位于32bits的實體位址之外,那麼通過create_36bit_mapping建立36bits長度的頁表,對于嵌入式系統來說很少有這種應用。
  • 調整傳入的md中的各成員資訊:virtual向低位址對齊到頁面大小;根據length參數取對齊到頁面的大小的長度,并以此計算映射結束的虛拟位址。根據pfn參數計算起始實體位址。
  • 根據虛拟位址和公式pgd = pgd_offset_k(addr)計算頁表位址。
  • 根據虛拟位址的起始位址參數以及起始實體位址調用alloc_init_section來生成頁表。

pgd_offset_k宏将一個0-4G範圍内的虛拟位址轉換為核心程序首頁表中的對應頁表項所在的位址。它首先根據pgd_index計算該虛拟位址對應的頁表項在首頁表中的索引值這裡需要注意PGDIR_SHIFT的值為21,而非20,是以它的偏移是取2M大小區塊的索引,這是由于pgd_t的類型為兩個長整形的元素。然後根據索引值和核心程序中的init_mm.pgd取得頁表項位址。

arch/arm/include/asm/pgtable.h

/* to find an entry in a page-table-directory */
#define pgd_index(addr)         ((addr) >> PGDIR_SHIFT)
#define pgd_offset(mm, addr)    ((mm)->pgd+pgd_index(addr))
/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)      pgd_offset(&init_mm, addr)
      

create_mapping在本質上是對傳入參數的檢查,并為調用alloc_init_section準備四要素:頁表位址,虛拟位址的起止位址和實體位址的起始位址。Linux對create_mapping的調用除了在arch/arm/mm/init.c中通過map_memory_bank初始化主記憶體頁面映射外,對其的調用均集中在arch/arm/mm/mmu.c中,其中iotable_init封裝create_mapping用于特定機器架構的裝置I/O映射。

12.7. alloc_init_section

頁表建立函數調用圖中給出了alloc_init_section在頁表建立中所處的實作位置,本質上它是與alloc_init_pte并行的函數,alloc_init_section被用來建立段頁表(首頁表),alloc_init_pte則用來在被映射區長度小于1M時建立2級頁表。

static void __init alloc_init_section(pgd_t *pgd, unsigned long addr,
				      unsigned long end, unsigned long phys,
				      const struct mem_type *type);
      
  • pgd參數指定生成的頁表的起始位址,它是一個pgd_t類型,被定義為typedef unsigned long pgd_t[2],是以它是一個2維數組。
  • addr和end分别指明被映射到的虛拟位址的起止位址。
  • phys指明被映射的實體位址的起始位址。
  • type參數指明映射類型,所有映射類型在struct mem_type mem_types[]數組中被統一定義。

alloc_init_section依次完成了以下工作:

  • 首先根據公式(addr | end | phys) & ~SECTION_MASK) == 0依據傳入的的addr,end和phys參數判斷是否滿足位址對齊到1M。
  • 如果滿足則直接生成段頁表(首頁表),并存入pgd指向的位址。由于pgd是一個2維數組,是以每次需要對2個元素指派,也即一次可以處理2M的記憶體映射,生成兩個頁表項。它在一個循環中以SECTION_SIZE為步進機關,通過phys | type->prot_sect來生成和填充頁表。主RAM記憶體就是通過這種方法生成。一個256M記憶體RAM的生成頁表如下圖中的棕色部分所示。
  • 調用flush_pmd_entry清空TLB中的頁面Cache,以使得新頁表起作用。
  • 如不滿足直接生成首頁表,那麼調用alloc_init_pte生成二級頁表。

圖 69. 核心RAM映射後的頁表布局

頁表機制

12.8. alloc_init_pte

alloc_init_pte在初始化非主RAM中起到重要的作用,它嘗試建立二級頁表。二級頁表的L1實際上還是存在于首頁表中,隻不過此時的首頁表項不再是實體位址,而是二級頁表,或者稱為中間頁表(PMD)。

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
				  unsigned long end, unsigned long pfn,
				  const struct mem_type *type);
      
  • pmd參數傳遞L1頁表位址。
  • addr和end分别指明被映射到的虛拟位址的起止位址。
  • pfn是将被映射的實體位址的頁框。
  • type參數指明映射類型。

alloc_init_pte依次完成以下工作:

  • 首先判斷pmd指向的L1頁表中的頁表項是否存在,如果不存在則首先使用Bootmem機制中的alloc_bootmem_low_pages函數申請所需的二級頁表空間,大小為4K,1個PAGE。

表 25. 頁表計算

頁表名稱 計算公式 說明
首頁表項位址 pgd_offset_k(vir_addr)

#define PGDIR_SHIFT 21

#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)

#define pgd_offset(mm, addr) ((mm)->pgd+pgd_index(addr))

#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)

首頁表項 __pmd(phys | type->prot_sect) #define __pmd(x) (x)
一級頁表項位址 pmd = (pmd_t *)pgd_offset_k(vir_addr); 同"首頁表項位址"
一級頁表項

pte = alloc_bootmem_low_pages(1024 * sizeof(pte_t));

__pmd_populate(pmd, __pa(pte) | type->prot_l1);

static inline void __pmd_populate(pmd_t *pmdp,

unsigned long pmdval){

pmdp[0] = __pmd(pmdval);

pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));

flush_pmd_entry(pmdp);

}

二級頁表項位址 pte = alloc_bootmem_low_pages(1024 * sizeof(pte_t)); 通過Bootmem機制申請1個頁面大小的記憶體。
二級頁表項

pte = pte_offset_kernel(pmd, addr);

set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);

#define __pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

#define pte_offset_kernel(dir,addr) (pmd_page_vaddr(*(dir)) + __pte_index(addr))

#define __pgprot(x) (x)

#define pgprot_val(x) (x)

#define __pte(x) (x)

#define pfn_pte(pfn,prot) (__pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot)))

對于pgd_offset_k宏的說明如下:

  • init_mm.pgd類型為pgd_t類型,是以首頁表總是以2個頁表項為一組,它的大小為2 * sizeof(unsigned long) = 8。
  • 虛拟位址首先右移21位,是為了取高11位作為2個頁表項為一組這裡資料類型的索引值,在計算(mm)->pgd+pgd_index(addr)時,是指針的相加,等價于(pgd_t *)((unsigned long)init_mm.pgd + 8 * ((addr) >> 21))。
  • 由于每個頁表項的大小為4,是以對應于單個頁表項的大小,其索引值為8 * ((addr) >> 21) / 4 = 2 * ((addr),位址則為8 * ((addr) >> 21)。右移21然後乘以2,相當于取高12位,并将最後位置0,再乘以4則到達了位址。相當于乘以8。這裡取的總是偶數索引,也即create_mapping傳遞給alloc_init_section的pgd參數永遠指向偶數索引。
  • 如果需要取奇數索引的頁表項怎麼辦呢?該值将在alloc_init_section中通過(addr & SECTION_SIZE)對它進行修正。
  • 首頁表項的基位址init_mm.pgd加上索引值就是首頁表項的位址。

對__pmd_populate内聯函數的說明如下:

  • 它同時處理兩個一級頁表項pmd[0]和pmd[1]。
  • pmd[0]的值即為傳入的pmdval,也即通過Bootmem機制擷取的位址pte轉化為實體位址後加上保護标志。
  • pmd[1]的值是pmd[0]的值的偏移,它偏移了256個PTE頁表項,由于每個PTE頁表項也是4位元組,是以偏移的的實體位址為256 * sizeof(pte_t)。
  • 調用flush_pmd_entry清空TLB中的頁面Cache,以使得新頁表起作用。
#define PTRS_PER_PTE		512
#define pmd_val(x)      (x)

static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
        unsigned long ptr;

        ptr = pmd_val(pmd) & ~(PTRS_PER_PTE * sizeof(void *) - 1);
        ptr += PTRS_PER_PTE * sizeof(void *);

        return __va(ptr);
}
      

12.9. set_pte_ext

set_pte_ext用來填充硬體PTE頁表。在create_mapping中被調用,通過一個循環,被傳入的實體頁幀和大小以PAGE_SIZE步進,進行二級頁表的計算和填充。

do {		
		set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
		pfn++;
	} while (pte++, addr += PAGE_SIZE, addr != end);
      

它通過調用特定系統的pte函數完成,對于ARMv6來說,定義如下:

arch/arm/include/asm/pgtable.h
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)

arch/arm/include/asm/cpu-single.h
#define cpu_set_pte_ext			__cpu_fn(CPU_NAME,_set_pte_ext)
      
arch/arm/mm/proc-v6.S

ENTRY(cpu_v6_set_pte_ext)
#ifdef CONFIG_MMU
        armv6_set_pte_ext cpu_v6
#endif
        mov     pc, lr
      

cpu_v6_set_pte_ext函數中引用了armv6_set_pte_ext宏,并傳入cpu_v6參數。該宏定義如下:

arch/arm/mm/proc-macros.S

.macro  armv6_set_pte_ext pfx
str     r1, [r0], #-2048                @ linux version
      

根據ATPCS規則,C語言函數在調用彙編語言時,分别通過r0-r2來依次傳遞參數。是以這裡的r0代表的是pte參數,也即二級頁表的位址;r1為通過pfn_pte計算出的二級頁表項;r2為0。這裡的str指令首先将人r1存入r0所指向的位址,也即填充二級頁表項,然後将r0的值減去2048,相當于下移了2048 / 4 = 512項。這裡給出一個一二級頁表的全圖:

圖 70. 核心頁表布局全圖

頁表機制

注意圖中二級頁表是在注冊中斷向量時的一個執行個體。注冊的虛拟位址為0xffff0000,實體位址為0x50740000,大小為0x1000。傳遞給r0的值即為0xc0741fc0,r1的值為0x5074034b。在經過str操作之後,r0的值為0xc07417c0,相當于下移了512個頁表項。

arch/arm/include/asm/pgtable-hwdef.h
#define PTE_TYPE_MASK		(3 << 0)
#define PTE_EXT_AP0     (1 << 4)

bic     r3, r1, #0x000003fc
bic     r3, r3, #PTE_TYPE_MASK
orr     r3, r3, r2
orr     r3, r3, #PTE_EXT_AP0 | 2
      

bic位清除指令首先将r1中0x3fc對應的位清0,然後對PTE_TYPE_MASK指定的最後兩位清0,也即對0x3ff指定的最後11位清零,對于值為0x5074034b的r1來說,存入r3的值為0x50740000。orr按位邏輯或指令通過将r3中的值與r2位或操作放入r3,由于r2的值為0,是以r3的值此時保持不變。最後的orr将r3的值加上PTE_EXT_AP0權限位,或上2是為了指定目前是小頁表(4K),此時r3的值為0x50740012。

arch/arm/include/asm/pgtable.h
#define L_PTE_MT_MASK           (0x0f << 2)

adr     ip, \pfx\()_mt_table
and     r2, r1, #L_PTE_MT_MASK
ldr     r2, [ip, r2]
      

adr僞指令将cpu_v6_mt_table的位址裝入ip寄存器,然後取r1中0x5074034b的L_PTE_MT_MASK位作為索引值,這裡為8。由于表中每一項的大小為4位元組,是以[ip, 8]對應表中的第3項,也即L_PTE_MT_WRITETHROUGH。

arch/arm/include/asm/pgtable.h
#define L_PTE_MT_WRITETHROUGH	(0x02 << 2)	/* 0010 */

/*
 * The ARMv6 and ARMv7 set_pte_ext translation function.
 *
 * Permission translation:
 *  YUWD  APX AP1 AP0	SVC	User
 *  0xxx   0   0   0	no acc	no acc
 *  100x   1   0   1	r/o	no acc
 *  10x0   1   0   1	r/o	no acc
 *  1011   0   0   1	r/w	no acc
 *  110x   0   1   0	r/w	r/o
 *  11x0   0   1   0	r/w	r/o
 *  1111   0   1   1	r/w	r/w
 */
	.macro	armv6_mt_table pfx
\pfx\()_mt_table:
	.long	0x00						@ L_PTE_MT_UNCACHED
	.long	PTE_EXT_TEX(1)					@ L_PTE_MT_BUFFERABLE
	.long	PTE_CACHEABLE					@ L_PTE_MT_WRITETHROUGH
	.long	PTE_CACHEABLE | PTE_BUFFERABLE			@ L_PTE_MT_WRITEBACK
	.long	PTE_BUFFERABLE					@ L_PTE_MT_DEV_SHARED
	.long	0x00						@ unused
	.long	0x00						@ L_PTE_MT_MINICACHE (not present)
	.long	PTE_EXT_TEX(1) | PTE_CACHEABLE | PTE_BUFFERABLE	@ L_PTE_MT_WRITEALLOC
	.long	0x00						@ unused
	.long	PTE_EXT_TEX(1)					@ L_PTE_MT_DEV_WC
	.long	0x00						@ unused
	.long	PTE_CACHEABLE | PTE_BUFFERABLE			@ L_PTE_MT_DEV_CACHED
	.long	PTE_EXT_TEX(2)					@ L_PTE_MT_DEV_NONSHARED
	.long	0x00						@ unused
	.long	0x00						@ unused
	.long	0x00						@ unused
	.endm
      

首先測試二級頁表項0x5074034b中的L_PTE_WRITE和L_PTE_DIRTY标志位,如果設定了L_PTE_WRITE,但沒有L_PTE_DIRTY,那麼設定那麼設定PTE_EXT_APX到r3代表的硬體二級頁表項0x50740021中。顯然這裡不會設定該位。

arch/arm/include/asm/pgtable.h
#define L_PTE_DIRTY             (1 << 6)
#define L_PTE_WRITE             (1 << 7)

arch/arm/include/asm/pgtable-hwdef.h
#define PTE_EXT_APX		(1 << 9)	/* v6 */

tst     r1, #L_PTE_WRITE
tstne   r1, #L_PTE_DIRTY
orreq   r3, r3, #PTE_EXT_APX
      

如果Linux版本的二級頁表項設定了L_PTE_USER标志,r3被置PTE_EXT_AP1。如果r3包含PTE_EXT_APX标志,那麼同時清除PTE_EXT_APX和 PTE_EXT_AP0。

#define L_PTE_USER              (1 << 8)

tst     r1, #L_PTE_USER
orrne   r3, r3, #PTE_EXT_AP1
tstne   r3, #PTE_EXT_APX
bicne   r3, r3, #PTE_EXT_APX | PTE_EXT_AP0
      

如果r1沒有L_PTE_EXEC标志,則設定PTE_EXT_XN。

tst     r1, #L_PTE_EXEC
orreq   r3, r3, #PTE_EXT_XN

orr     r3, r3, r2
      

然後再加上L_PTE_MT_WRITETHROUGH标志。然後根據L_PTE_YOUNG标志,确定是否加上L_PTE_PRESENT标志。

tst     r1, #L_PTE_YOUNG
tstne   r1, #L_PTE_PRESENT
moveq   r3, #0

str     r3, [r0]
mcr     p15, 0, r0, c7, c10, 1          @ flush_pte
.endm
      

str指令将最終的硬體PTE頁表值存放到低位址的二級頁表中。是以硬體使用的二級頁表總是位于低位址處,而高位址處的512項PTE是留給Linux自己使用的。

12.10. Sandbox

表 26. Memory Hierarchy

位于哪裡[a]
[a]到底位于哪裡呢?
<figure><title>核心RAM布局</title><graphic fileref="images/kernelmap.gif"/></figure>
      

10 0=110 0=1

上一頁   下一頁
11. 核心初始化2  起始頁  13. 記憶體管理

繼續閱讀