1、pgd_addr_end
根據當前虛擬地址 addr 和目標結束地址 end,計算當前 PGD 項 能夠覆蓋的最大虛擬地址范圍的結束地址 next。
- 如果 addr 和 end 跨越多個 PGD 項(即 end 超出當前 PGD 項的地址范圍),則返回當前 PGD 項的地址邊界。
- 如果 end 在當前 PGD 項的地址范圍內,則返回 end。
- 在建立頁表映射時,內核需要逐個處理每個 PGD 項對應的地址范圍。通過 pgd_addr_end,可以確定當前 PGD 項需要處理的地址范圍 [addr, next),并更新 addr 為 next,以便處理下一個 PGD 項。
頁表建立參考
do {next = pgd_addr_end(addr, end); // 計算當前 PGD 項的地址范圍alloc_init_pud(pgd, addr, next, phys, prot, alloc); // 初始化 PUD 頁表phys += next - addr; // 更新物理地址偏移
} while (pgd++, addr = next, addr != end);
- 流程解釋:
- 循環處理每個 PGD 項:通過 do-while 循環,逐個處理 PGD 項。
- 計算當前 PGD 項的地址范圍:通過 pgd_addr_end 確定當前 PGD 項的地址范圍 [addr, next)。
- 初始化下級頁表:調用 alloc_init_pud 為當前 PGD 項分配并初始化 PUD 頁表。
- 更新物理地址偏移:根據當前處理的地址范圍大小(next - addr)調整物理地址 phys。
- 移動到下一個 PGD 項:更新 addr 為 next,并繼續處理下一個 PGD 項,直到 addr == end。
2、pgd_offset_k
pgd_offset_k(addr)
是 Linux 內核中用于 獲取內核頁全局目錄(PGD)中對應虛擬地址 addr
的頁表項地址 的宏。它是內核頁表操作的核心宏之一,主要用于內核虛擬地址空間的映射初始化(如 early_fixmap_init
)和頁表建立(如 create_mapping
)等場景。
1. 宏的定義與展開
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
init_mm
:內核的全局內存描述符(mm_struct
),表示內核的地址空間(與進程的mm
分離)。pgd_offset(mm, addr)
:根據mm
的 PGD 基地址和addr
計算對應的 PGD 項地址。
進一步展開:
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
(mm)->pgd
:內核 PGD 的基地址(init_mm.pgd
)。pgd_index(addr)
:從addr
中提取 PGD 項的索引。
2. pgd_index(addr)
的計算
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
PGDIR_SHIFT
:定義 PGD 項的位移量,決定 PGD 項覆蓋的地址范圍。例如:- 對于 ARM64(4 級頁表),
PGDIR_SHIFT = 39
,每個 PGD 項覆蓋2^(48-39) = 512GB
地址空間。
- 對于 ARM64(4 級頁表),
PTRS_PER_PGD
:PGD 項的數量(通常為1 << (PGDIR_SHIFT - VA_BITS_SHIFT)
,例如 512 項)。- 作用:通過右移
addr
并掩碼,提取 PGD 項的索引。
3. 典型使用場景
3.1 早期內核映射初始化(early_fixmap_init
)
在內核啟動階段,early_fixmap_init
函數會使用 pgd_offset_k
初始化固定映射區域(fixmap
)的頁表:
pgd_t *pgd = pgd_offset_k(FIXADDR_START);
__pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
- 作用:將
bm_pud
(預分配的 PUD 頁表)的物理地址寫入 PGD 項中,建立從FIXADDR_START
到bm_pud
的映射。 - 后續步驟:通過
pud_offset
、pmd_offset
、pte_offset
逐級填充頁表。
3.2 通用頁表建立(create_mapping
)
在內核中,create_mapping
函數會通過 pgd_offset_k
遍歷虛擬地址范圍并建立頁表:
pgd_t *pgd = pgd_offset_k(virt_addr);
do {next = pgd_addr_end(addr, end);alloc_init_pud(pgd, addr, next, phys, prot);phys += next - addr;addr = next;
} while (pgd++, addr != end);
- 作用:逐個處理每個 PGD 項的地址范圍,初始化下級頁表(PUD → PMD → PTE)。
4. ARM64 架構示例
假設:
addr = 0xffff7ffffabfe000
(FIXADDR_START
)init_mm.pgd = 0xffff800000ef0000
PGDIR_SHIFT = 39
,PTRS_PER_PGD = 512
4.1 計算 pgd_index(addr)
pgd_index(addr) = (0xffff7ffffabfe000 >> 39) & 0x1ff = 0xff
- 解釋:
addr
的高 9 位(0x1ff
)即為 PGD 項的索引。
4.2 計算 pgd_offset_k(addr)
pgd_offset_k(addr) = init_mm.pgd + 0xff * sizeof(pgd_t)
- ARM64 中
pgd_t
占 8 字節,因此:pgd_offset_k(addr) = 0xffff800000ef0000 + 0xff * 8 = 0xffff800000ef07f8
- 結果:這是
addr
在內核 PGD 中對應項的虛擬地址。
5. 關鍵點總結
-
目的:
- 快速定位內核虛擬地址
addr
在 PGD 中的項地址。 - 為后續頁表建立(如
PUD
、PMD
、PTE
)提供起點。
- 快速定位內核虛擬地址
-
層級關系:
pgd_offset_k
屬于頁表操作宏的第一級(PGD),后續通過pud_offset
、pmd_offset
、pte_offset
逐級展開。
-
架構依賴:
PGDIR_SHIFT
和PTRS_PER_PGD
的定義依賴于架構(如 ARM64、x86)和頁表層級(4 級或 2 級)。
-
應用場景:
- 早期內核映射(
fixmap
、ioremap
)。 - 動態內存映射(
vmalloc
、ioremap
)。 - 設備樹(DTB)的加載(通過
fixmap
映射物理地址)。
- 早期內核映射(
6. 常見問題
Q1:為什么 pgd_offset_k
使用 init_mm
?
- 原因:內核地址空間是全局的,所有進程共享同一個內核 PGD(
init_mm.pgd
)。通過init_mm
可直接訪問內核頁表。
Q2:如何驗證 pgd_offset_k
的正確性?
- 調試方法:在
early_fixmap_init
中打印pgd_offset_k(addr)
的值,并檢查其是否對應預期的 PGD 項地址(如通過pr_err
輸出)。
Q3:ARM64 中為何需要乘以 8?
- 原因:ARM64 的每個 PGD 項占用 8 字節(64 位),因此索引
0xff
對應的偏移量為0xff * 8 = 0x7f8
。
7. 擴展:頁表層級劃分(以 ARM64 為例)
層級 | 宏 | 地址位移 | 頁表項大小 | 映射范圍 |
---|---|---|---|---|
PGD | pgd_index(addr) | >> 39 | 8 字節 | 512GB |
PUD | pud_index(addr) | >> 30 | 8 字節 | 1GB |
PMD | pmd_index(addr) | >> 21 | 8 字節 | 2MB |
PTE | pte_index(addr) | >> 12 | 8 字節 | 4KB |
總結
pgd_offset_k(addr)
是內核頁表操作的基礎,通過計算虛擬地址 addr
在 PGD 中的項地址,為后續的頁表建立提供起點。理解其工作原理對于調試內核內存管理、分析頁表初始化流程至關重要。