前言
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() 为空函数;
之所以保留接口,是为了保持跨架构一致性和语义清晰。
浙公网安备 33010602011771号