文章目錄

  • 前言
  • 一、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),即物理內存的所有頁表頁都映射到內核虛擬地址空間的固定偏移上。

比如:

Linux內存 --- pte_offset_map/pte_offset_kernel - 實踐_linux

因此:
對於 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 映射後訪問。

如下圖所示:

對比點

用户態頁表 (pte_offset_map)

內核頁表 (pte_offset_kernel)

頁表頁位置

用户 mm_struct 管理區,不保證線性映射

永遠在內核線性映射區

是否可直接訪問



是否需臨時映射

是(通過 kmap_local_page)


典型使用場景

遍歷用户進程頁表

遍歷 init_mm

調用對應解除函數

pte_unmap()

無需

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,也不建立臨時映射。

如下圖所示:

架構 / 配置

是否有高端內存

pte_offset_map() 是否建立臨時映射

實際行為

x86_64 (默認)

❌ 無

❌ 否

直接返回內核可訪問的 PTE 指針

x86 (32 位, 有 highmem)

✅ 是

✅ 是

調用 kmap_local_page() 臨時映射

在 x86_64 無高端內存的系統 上:

pte_offset_map() 不會真正建立臨時映射;

它只是簡單地調用 pte_offset_kernel();

pte_unmap() 為空函數;

之所以保留接口,是為了保持跨架構一致性和語義清晰。