前言

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 映射后访问。

如下图所示:

对比点用户态页表 (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() 为空函数;

之所以保留接口,是为了保持跨架构一致性和语义清晰。