文章目錄
- 前言
- 一、walk_pte_range
- 二、pte_offset_kernel
- 三、pte_offset_map
- 3.1 highmem
- 3.2 no highmem
前言
pte_offset_kernel() 用於內核頁表,因為內核頁表頁恆在線性映射區,可直接訪問;
pte_offset_map() 用於用户頁表,因為用户頁表頁不保證在線性映射區,需要臨時 kmap 映射後訪問。 (對於64位架構,沒有高端內存,比如x86_64/ARM64,其實pte_offset_map就等效於pte_offset_kernel)。
一、walk_pte_range
比如:
// linux/v6.14/source/mm/pagewalk.c
static int walk_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
struct mm_walk *walk)
{
pte_t *pte;
int err = 0;
spinlock_t *ptl;
//no_vma 情況下的處理
if (walk->no_vma) {
/*
* pte_offset_map() might apply user-specific validation.
* Indeed, on x86_64 the pmd entries set up by init_espfix_ap()
* fit its pmd_bad() check (_PAGE_NX set and _PAGE_RW clear),
* and CONFIG_EFI_PGT_DUMP efi_mm goes so far as to walk them.
*/
//如果當前內存描述符是 init_mm(內核初始 mm)或地址是內核空間(addr >= TASK_SIZE),使用 pte_offset_kernel 直接獲取 PTE 指針。
if (walk->mm == &init_mm || addr >= TASK_SIZE)
pte = pte_offset_kernel(pmd, addr);
//否則,使用 pte_offset_map 映射用户頁表項(可能涉及頁表頁的分配或映射)。
else
pte = pte_offset_map(pmd, addr);
if (pte) {
//如果 pte 有效,調用 walk_pte_range_inner 對該 PTE 範圍執行實際操作。
err = walk_pte_range_inner(pte, addr, end, walk);
//如果是用户空間頁表,操作完成後需要調用 pte_unmap 解除映射。
if (walk->mm != &init_mm && addr < TASK_SIZE)
pte_unmap(pte);
}
} else { //有 VMA 上下文的情況(加鎖訪問)
//這裏是用户態普通內存空間的正常頁表遍歷路徑:
pte = pte_offset_map_lock(walk->mm, pmd, addr, &ptl);
if (pte) {
err = walk_pte_range_inner(pte, addr, end, walk);
pte_unmap_unlock(pte, ptl);
}
}
if (!pte)
walk->action = ACTION_AGAIN;
return err;
}
walk_pte_range(),它負責在一個 PMD(Page Middle Directory)層級下,遍歷對應的 PTE(Page Table Entry)範圍。
在給定的 pmd_t(頁中目錄)範圍 [addr, end) 內,對應的所有 PTE(頁表項)執行 walk_pte_range_inner(),並處理內核態或用户態頁表的訪問方式差異。
我們可以看到對於內核空間的地址直接調用 pte_offset_kernel 即可。
而對於用户空間的地址調用 pte_offset_map/pte_offset_map_lock,獲取 PTE 後還需要調用pte_unmap/pte_unmap_unlock 。
二、pte_offset_kernel
內核頁表始終在線性映射區中,Linux 內核在啓動時會建立直接映射(direct mapping / linear mapping),即物理內存的所有頁表頁都映射到內核虛擬地址空間的固定偏移上。
比如:
因此:
對於 init_mm(內核自身的 mm_struct),所有頁表頁都恆在線性映射區中。
訪問某個頁表項,只需要簡單地用指針算偏移即可,不需要映射。
所以:
pte = pte_offset_kernel(pmd, addr);
只是一個普通的指針算術操作,直接返回內核可訪問的虛擬地址。
// linux/v6.14/source/include/linux/pgtable.h
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
pte_offset_kernel() 用於內核頁表,因為內核頁表頁恆在線性映射區,可直接訪問;
三、pte_offset_map
3.1 highmem
對於有高端內存的情況下,比如32位x86:
用户進程的頁表頁(struct page 對應的物理頁)屬於 用户 mm_struct 管理的內存,它們雖然由內核分配,但不保證在線性映射區中可直接訪問。
為了訪問這些頁表,內核必須:
根據頁表物理頁得到 struct page *page;
通過 kmap_local_page(page)(或老版本的 kmap_atomic())建立臨時內核映射;
得到一個臨時的虛擬地址來讀寫 PTE 內容;
操作完成後調用 kunmap_local() 解除映射。
而 pte_offset_map() 正是做了這些步驟的封裝:
pte_t *pte_offset_map(pmd_t *pmd, unsigned long addr)
{
pte_t *pte;
struct page *page = pmd_page(*pmd);
pte = (pte_t *)kmap_local_page(page);
return &pte[pte_index(addr)];
}
// linux/v6.14/source/include/linux/mm.h
pte_t *___pte_offset_map(pmd_t *pmd, unsigned long addr, pmd_t *pmdvalp);
static inline pte_t *__pte_offset_map(pmd_t *pmd, unsigned long addr,
pmd_t *pmdvalp)
{
pte_t *pte;
__cond_lock(RCU, pte = ___pte_offset_map(pmd, addr, pmdvalp));
return pte;
}
static inline pte_t *pte_offset_map(pmd_t *pmd, unsigned long addr)
{
return __pte_offset_map(pmd, addr, NULL);
}
// linux/v6.14/source/mm/pgtable-generic.c
___pte_offset_map();
-->__pte_map();
}
// linux/v6.14/source/include/linux/pgtable.h
#ifdef CONFIG_HIGHPTE
#define __pte_map(pmd, address) \
((pte_t *)kmap_local_page(pmd_page(*(pmd))) + pte_index((address)))
#define pte_unmap(pte) do { \
kunmap_local((pte)); \
rcu_read_unlock(); \
} while (0)
在高端內存(highmem)配置的系統中,這一步是必須的;否則內核根本無法直接訪問用户進程的頁表頁。
pte_offset_map() 用於用户頁表,因為用户頁表頁不保證在線性映射區,需要臨時 kmap 映射後訪問。
如下圖所示:
|
對比點
|
用户態頁表 ( |
內核頁表 ( |
|
頁表頁位置
|
用户 mm_struct 管理區,不保證線性映射
|
永遠在內核線性映射區
|
|
是否可直接訪問
|
否
|
是
|
|
是否需臨時映射
|
是(通過 kmap_local_page)
|
否
|
|
典型使用場景
|
遍歷用户進程頁表
|
遍歷 init_mm
|
|
調用對應解除函數
|
|
無需
|
3.2 no highmem
在 x86_64 架構且無高端內存(no highmem) 的系統上,pte_offset_map() 理論上不再需要真正“建立臨時映射”.
在 32 位時代,內核虛擬地址空間有限(通常 3G/1G 分割),所以部分物理內存(高端內存 highmem)不能永久映射到內核空間,訪問這部分物理頁就需要臨時 kmap 映射。
而在 x86_64 架構 上:
內核地址空間非常大(通常 128 TB),
Linux 從啓動階段就建立了完整的 直接映射(linear/direct mapping),
所有物理頁(包括頁表頁)都永久映射到高半區。
因此:
在 x86_64 上,“高端內存”概念不存在。
所有頁表頁都可以通過固定偏移直接訪問。
// linux/v6.14/source/include/linux/pgtable.h
#ifdef CONFIG_HIGHPTE
#define __pte_map(pmd, address) \
((pte_t *)kmap_local_page(pmd_page(*(pmd))) + pte_index((address)))
#define pte_unmap(pte) do { \
kunmap_local((pte)); \
rcu_read_unlock(); \
} while (0)
#else
static inline pte_t *__pte_map(pmd_t *pmd, unsigned long address)
{
return pte_offset_kernel(pmd, address);
}
static inline void pte_unmap(pte_t *pte)
{
rcu_read_unlock();
}
#endif
static inline pte_t *__pte_map(pmd_t *pmd, unsigned long address)
{
return pte_offset_kernel(pmd, address);
}
static inline void pte_unmap(pte_t *pte)
{
rcu_read_unlock();
}
pte_offset_map實際上就是調用pte_offset_kernel。
可以看到:
在 x86_64 上,pte_offset_map() 直接調用 pte_offset_kernel();
pte_unmap() 是空操作。
所以實際上並不會執行任何 kmap,也不建立臨時映射。
如下圖所示:
|
架構 / 配置
|
是否有高端內存
|
|
實際行為
|
|
x86_64 (默認)
|
❌ 無
|
❌ 否
|
直接返回內核可訪問的 PTE 指針
|
|
x86 (32 位, 有 highmem)
|
✅ 是
|
✅ 是
|
調用 |
在 x86_64 無高端內存的系統 上:
pte_offset_map() 不會真正建立臨時映射;
它只是簡單地調用 pte_offset_kernel();
pte_unmap() 為空函數;
之所以保留接口,是為了保持跨架構一致性和語義清晰。