深入理解Linux内核页表映射分页机制

文摘   2024-10-08 14:29   陕西  

作者简介:王柔柔,西安邮电大学研一在读,导师为性能工程实验室舒新峰教授。操作系统和Linux内核爱好者,热衷于探索Linux内核和eBPF技术。

前言

操作系统的核心任务是对系统资源的管理,而重中之重就是对CPU和内存的管理。为了使进程摆脱系统内存的制约,用户进程运行在虚拟内存之上,每个用户进程都拥有完整的虚拟地址空间,互不干涉。而实现虚拟内存的关键就在于建立虚拟地址与物理地址之间的映射,因为无论如何数据终究要存储到物理内存中才能被记录下来。

如上图所示,进程1和进程2都拥有完整的虚拟地址空间,虚拟地址空间又分为了用户空间内核空间,对于不同的进程面对的都是同一个内核其内核空间的地址对于的物理地址都是一样的,因而进程1和进程2中内核空间的VA K地址都映射到了物理内存的PA K地址

一、分页机制

1.分页机制的核心思想

就是解除线性地址和物理地址的一一对应关系,使线性地址连续,而物理地址不连续。使连续的线性地址可以与任意物理内存地址相关联,即从虚拟页面到物理页面的映射。而这个翻译过程由内存管理单元(MMU)完成,MMU接收CPU发出的虚拟地址,将其翻译为物理地址后发送给内存。内存管理单元按照该物理地址进行相应访问后读出或写入相关数据,如下图所示:

2.分页机制寻址原理

通过查页表,对于每个程序,内存管理单元MMU都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。每当为一个虚拟页面寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系。当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。

二级页表模式:

(1)取虚拟地址高10位*4+页目录表(PDE)得到页表的物理地址

(2)取虚拟地址中间10位*4+页表(PTE)得到页地址

(3)取虚拟地址最后12位+页地址得到最终的物理地址页目录项、页表项结构:

3.启动分页机制,按顺序做好三件事

(1)准备好页目录表及页表

(2)将页表地址写入控制寄存器cr3

(3)寄存器cr0的PG位置1,开启页表机制

二、x86架构下页表分页机制源码分析

1.页表结构

源码:arch/x86/include/asm/pgtable_type.h

这段代码是 Linux 内核中用于定义 x86 架构下页表相关的宏和数据结构的头文件,它定义了页表项的各个标志位、页表项的格式以及一些辅助函数和宏。

页表项标志位定义

#define _PAGE_BIT_PRESENT       /* 表示页面是否存在 */
#define _PAGE_BIT_RW /* 表示页面是否可写 */
#define _PAGE_BIT_USER /* 示页面是否用户可访问 */

页表项的组合

#define _PAGE_SHARED
#define _PAGE_KERNEL
#define _PAGE_KERNEL_EXEC

页表项的类型定义

typedef struct{unsigned long pgd;}pgd_t;/* 顶级页表目录 */
typedef struct{unsigned long pud;}pud_t;/* 高层页表目录 */
typedef struct{unsigned long pmd;}pmd_t;/* 中层页表目录 */
typedef struct{unsigned long pte;}pte_t;/* 最底层页表条目 */

2.对页表的基本操作

源码:arch/x86/mm/pgtable/pgtable.c

这段代码包含了页表分配、释放、设置访问标志、清除访问标志等功能。这些操作是内存管理的基本操作,通过这些操作,内核可以高效地管理虚拟内存和物理内存之间的映射关系。

页表分配和释放

定义了页表分配和释放的函数。页表分配是指为进程或内核分配新的页表项。这通常在进程创建或需要新的虚拟内存区域时进行。分配页表项时,内核会从物理内存中分配一块内存,并将其初始化为页表项。

   pgd_t *pgd_alloc(struct mm_struct *mm)
  {
      pgd_t *pgd;
      pmd_t *u_pmds[MAX_PREALLOCATED_USER_PMDS];
      pmd_t *pmds[MAX_PREALLOCATED_PMDS];
//PGD 分配:  
      pgd = _pgd_alloc();//调用 _pgd_alloc() 函数分配一个新的 PGD。如果分配失败,跳转到 out 标签.

      if (pgd == NULL)
          goto out;
//设置 PGD:
      mm->pgd = pgd;//将分配的 PGD 赋值给当前进程的内存管理结构。
//预分配 PMD:
      if (sizeof(pmds) != 0 &&
              preallocate_pmds(mm, pmds, PREALLOCATED_PMDS) != 0)
          goto out_free_pgd;//如果 pmds 数组的大小不为 0,调用 preallocate_pmds() 函数为当前进程预分配 PMD(页中间目录)。如果失败,跳转到 out_free_pgd 标签。
//预分配用户 PMD:
      if (sizeof(u_pmds) != 0 &&
              preallocate_pmds(mm, u_pmds, PREALLOCATED_USER_PMDS) != 0)
          goto out_free_pmds;
//并行虚拟化 PGD 分配:
      if (paravirt_pgd_alloc(mm) != 0)
          goto out_free_user_pmds;//调用 paravirt_pgd_alloc() 进行并行虚拟化的 PGD 分配。
// 加锁:
      spin_lock(&pgd_lock);//获取 PGD 锁以保护后续操作。
//PGD 构造:
      pgd_ctor(mm, pgd);//调用 pgd_ctor() 初始化 PGD。
//预填充 PMD:
      if (sizeof(pmds) != 0)
          pgd_prepopulate_pmd(mm, pgd, pmds);//如果 pmds 不为空,调用 pgd_prepopulate_pmd() 预填充 PMD。
//预填充用户 PMD:
      if (sizeof(u_pmds) != 0)
          pgd_prepopulate_user_pmd(mm, pgd, u_pmds);
//解锁:
      spin_unlock(&pgd_lock);//释放 PGD 锁。
//返回 PGD:
      return pgd;

  out_free_user_pmds:
      if (sizeof(u_pmds) != 0)
          free_pmds(mm, u_pmds, PREALLOCATED_USER_PMDS);
  out_free_pmds:
      if (sizeof(pmds) != 0)
          free_pmds(mm, pmds, PREALLOCATED_PMDS);
  out_free_pgd:
      _pgd_free(pgd);
  out:
      return NULL;
  }

  void pgd_free(struct mm_struct *mm, pgd_t *pgd)
  {
      pgd_mop_up_pmds(mm, pgd);
      pgd_dtor(pgd);//调用 pgd_dtor() 进行 PGD 的析构。
      paravirt_pgd_free(mm, pgd);//调用 paravirt_pgd_free() 释放并行虚拟化的 PGD。
      _pgd_free(pgd);//调用 _pgd_free() 释放 PGD。
  }

页表级别的释放

页表级别的释放函数用于释放不同级别的页表项,定义了页表级别的释放函数,如 pmd_free_tlb、pud_free_tlb 等。这些函数确保了内存的高效利用和释放。

   #if CONFIG_PGTABLE_LEVELS > 2
  void ___pmd_free_tlb(struct mmu_gather *tlb, pmd_t *pmd)
  {
      struct ptdesc *ptdesc = virt_to_ptdesc(pmd);//获取 PMD 描述符,使用 virt_to_ptdesc 函数将 PMD 转换为相应的页表描述符.
      paravirt_release_pmd(__pa(pmd) >> PAGE_SHIFT);// 释放 PMD,用 paravirt_release_pmd 函数释放 PMD。__pa(pmd) 将虚拟地址转换为物理地址,>> PAGE_SHIFT 用于获取页框号。
      #ifdef CONFIG_X86_PAE
      tlb->need_flush_all = 1;
      #endif//如果启用了 PAE,设置 tlb->need_flush_all 为 1,表示需要刷新所有 TLB 条目。
      pagetable_pmd_dtor(ptdesc);//调用 pagetable_pmd_dtor 函数对 PMD 进行析构,释放与 PMD 相关的资源。
      paravirt_tlb_remove_table(tlb, ptdesc_page(ptdesc));//调用 paravirt_tlb_remove_table 函数从 TLB 中移除与 PMD 相关的页表条目。
  }

设置和清除访问标志

定义了设置和清除页表项访问标志的函数,设置访问标志是指修改页表项中的标志位,以控制页面的访问权限。例如,可以设置页面为可读、可写或可执行。设置访问标志时,内核会更新页表项中的相应位。设置和清除页表项访问标志的函数用于管理页表项的访问权限,这些函数确保了内存的安全性和访问控制。

   int ptep_set_access_flags(struct vm_area_struct *vma,
                            unsigned long address, pte_t *ptep,
                            pte_t entry, int dirty)
  {
      int changed = !pte_same(*ptep, entry);//使用 pte_same 函数检查当前页表项与新条目是否相同。如果不同,设置 changed 为 1。

      if (changed && dirty)
          set_pte(ptep, entry);//更新页表项,如果页表项已更改且 dirty 为真,调用 set_pte 函数更新页表项。

      return changed;//返回 changed,指示页表项是否已更改。
  }

  int ptep_test_and_clear_young(struct vm_area_struct *vma,
                                unsigned long addr, pte_t *ptep)
  {
      int ret = 0;

      if (pte_young(*ptep))
          ret = test_and_clear_bit(_PAGE_BIT_ACCESSED,
                                  (unsigned long *) &ptep->pte);//检查页表项的年轻标志,使用 pte_young 函数检查页表项是否被标记为“年轻”。如果是,调用 test_and_clear_bit 函数清除访问标志,并将结果存储在 ret 中。

      return ret;//返回 ret,指示是否成功清除年轻标志。
  }

3.x86 架构相关的两级页表操作

源码:arch/x86/include/asm/pgtable-2level.h

这段代码主要定义了与 x86 架构相关的两级页表操作,包括设置和清除 PTE、PMD 和 PUD 的函数,以及交换条目和 PTE 的编码/解码宏和函数。通过这些定义,内核可以正确地管理和操作两级页表。

头文件保护

使用头文件保护机制,防止重复包含。

   #ifndef _ASM_X86_PGTABLE_2LEVEL_H
  #define _ASM_X86_PGTABLE_2LEVEL_H

错误打印宏

定义了用于打印 PTE 和 PGD 错误的宏。

   #define pte_ERROR(e) \
  pr_err("%s:%d: bad pte %08lx\n", __FILE__, __LINE__, (e).pte_low)
  #define pgd_ERROR(e) \
  pr_err("%s:%d: bad pgd %08lx\n", __FILE__, __LINE__, pgd_val(e))

设置 PTE、PMD 和 PUD 的函数

   static inline void native_set_pte(pte_t *ptep , pte_t pte)
{
*ptep = pte;
}//将传入的 pte 值赋给指向的页表项 *ptep。

static inline void native_set_pmd(pmd_t *pmdp, pmd_t pmd)
{
*pmdp = pmd;
}//将传入的 pmd 值赋给指向的页中间目录 *pmdp。

static inline void native_set_pud(pud_t *pudp, pud_t pud)
{
}//该函数的实现为空,表示当前没有对 PUD 的设置操作。

原子设置 PTE 的函数

   static inline void native_set_pte_atomic(pte_t *ptep, pte_t pte)
{
native_set_pte(ptep, pte);//调用 native_set_pte 来设置页表项。
}

清除 PMD 和 PUD 的函数

   static inline void native_pmd_clear(pmd_t *pmdp)
{
native_set_pmd(pmdp, __pmd(0));//调用 native_set_pmd,将 PMD 设置为 __pmd(0),将 PMD 清空或设置为无效状态。__pmd(0) 通常表示一个无效的 PMD 条目。
}

static inline void native_pud_clear(pud_t *pudp)
{
}

清除 PTE 的函数

   static inline void native_pte_clear(struct mm_struct *mm,
unsigned long addr, pte_t *xp)
{
*xp = native_make_pte(0);//调用 native_make_pte(0),将页表项 *xp 设置为无效状态。native_make_pte(0) 通常会返回一个表示无效页表项的 PTE 值。
}

获取并清除 PTE、PMD 和 PUD 的函数

   #ifdef CONFIG_SMP
static inline pte_t native_ptep_get_and_clear(pte_t *xp)
{
return __pte(xchg(&xp->pte_low, 0));
}//如果启用了 SMP,函数使用 xchg 原子操作将 xp->pte_low 的值交换为 0,并返回原来的 PTE 值。__pte 用于将原始值转换为 PTE 类型。
#else
#define native_ptep_get_and_clear(xp) native_local_ptep_get_and_clear(xp)
#endif//如果未启用 SMP,使用宏定义将调用重定向到 native_local_ptep_get_and_clear

#ifdef CONFIG_SMP
static inline pmd_t native_pmdp_get_and_clear(pmd_t *xp)
{
return __pmd(xchg((pmdval_t *)xp, 0));
}//如果启用了 SMP,函数使用 xchg 原子操作将 xp 的值交换为 0,并返回原来的 PMD 值。__pmd 用于将原始值转换为 PMD 类型。
#else
#define native_pmdp_get_and_clear(xp) native_local_pmdp_get_and_clear(xp)
#endif//如果未启用 SMP,使用宏定义将调用重定向到 native_local_pmdp_get_and_clear。

#ifdef CONFIG_SMP
static inline pud_t native_pudp_get_and_clear(pud_t *xp)
{
return __pud(xchg((pudval_t *)xp, 0));
}//如果启用了 SMP,函数使用 xchg 原子操作将 xp 的值交换为 0,并返回原来的 PUD 值。__pud 用于将原始值转换为 PUD 类型。
#else
#define native_pudp_get_and_clear(xp) native_local_pudp_get_and_clear(xp)
#endif//如果未启用 SMP,使用宏定义将调用重定向到 native_local_pudp_get_and_clear。

交换条目和 PTE 的编码/解码

定义了交换条目和 PTE 的编码/解码宏和函数。

   #define SWP_TYPE_BITS 5//义交换类型所需的位数,这里是 5 位。
#define _SWP_TYPE_MASK ((1U << SWP_TYPE_BITS) - 1)//计算出一个掩码,用于提取交换类型的值。
#define _SWP_TYPE_SHIFT (_PAGE_BIT_PRESENT + 1)//定义交换类型在交换条目中的位移量。
#define SWP_OFFSET_SHIFT (_PAGE_BIT_PROTNONE + 1)//定义交换偏移量在交换条目中的位移量。

#define MAX_SWAPFILES_CHECK() BUILD_BUG_ON(MAX_SWAPFILES_SHIFT > 5)//使用 BUILD_BUG_ON 宏在编译时检查 MAX_SWAPFILES_SHIFT 是否大于 5。如果是,则编译失败。这是为了确保交换文件的数量不会超过可以用位表示的最大值。
#define __swp_type(x)(((x).val >> _SWP_TYPE_SHIFT)& _SWP_TYPE_MASK)//从交换条目 x 中提取交换类型。通过右移和掩码操作获取类型值。
#define __swp_offset(x)((x).val >> SWP_OFFSET_SHIFT)//从交换条目 x 中提取交换偏移量。通过右移操作获取偏移值。
#define __swp_entry(type, offset)((swp_entry_t) { \
(((type) & _SWP_TYPE_MASK) << _SWP_TYPE_SHIFT) \
| ((offset) << SWP_OFFSET_SHIFT) })//创建一个新的交换条目。将类型和偏移量组合成一个 swp_entry_t 结构体。
#define __pte_to_swp_entry(pte)((swp_entry_t) { (pte).pte_low })//将页表项 pte 转换为交换条目。
#define __swp_entry_to_pte(x)((pte_t) { .pte = (x).val })//将交换条目 x 转换为页表项。

交换 PTE 的独占标记

定义了用于存储独占标记的交换 PTE 位。

    #define _PAGE_SWP_EXCLUSIVE	_PAGE_PSE

无反转 PFN

定义了无反转 PFN 的函数。

    static inline u64 protnone_mask(u64 val)
{
return 0;
}

static inline u64 flip_protnone_guard(u64 oldval, u64 val, u64 mask)
{
return val;
}

static inline bool __pte_needs_invert(u64 val)
{
return false;
}

4.x86 架构相关的三级页表操作

源码:arch/x86/include/asm/pgtable-3level.h

这段代码主要定义了与 x86 架构相关的三级页表操作,包括设置和清除 PTE、PMD 和 PUD 的函数,以及交换条目和 PTE 的编码/解码宏和函数。通过这些定义,内核可以正确地管理和操作三级页表。

头文件保护

使用头文件保护机制,防止重复包含。

   #ifndef _ASM_X86_PGTABLE_3LEVEL_H
#define _ASM_X86_PGTABLE_3LEVEL_H

错误打印宏

定义了用于打印 PTE、PMD 和 PGD 错误的宏。

   #define pte_ERROR(e)							
pr_err("%s:%d: bad pte %p(%08lx%08lx)\n",
__FILE__, __LINE__, &(e), (e).pte_high, (e).pte_low)//当检测到无效的 PTE 时,调用此宏以输出详细的错误信息。
#define pmd_ERROR(e)
pr_err("%s:%d: bad pmd %p(%016Lx)\n",
__FILE__, __LINE__, &(e), pmd_val(e))//当检测到无效的 PMD 时,调用此宏以输出详细的错误信息。
#define pgd_ERROR(e)
pr_err("%s:%d: bad pgd %p(%016Lx)\n",
__FILE__, __LINE__, &(e), pgd_val(e))//当检测到无效的 PGD 时,调用此宏以输出详细的错误信息。

交换宏

定义了用于交换 PTE、PMD 和 PUD 的宏。

   #define pxx_xchg64(_pxx, _ptr, _val) ({					
_pxx##val_t *_p = (_pxx##val_t *)_ptr;//将 _ptr 转换为指向特定类型的指针。
_pxx##val_t _o = *_p;//读取指针 _p 指向的当前值,并存储在 _o 中。
do { } while (!try_cmpxchg64(_p, &_o, (_val)));//使用 try_cmpxchg64 函数尝试将 _val 设置为 _p 指向的值。如果当前值与 _o 不同,交换操作将失败,循环将继续尝试,直到成功为止。
native_make_##_pxx(_o);//调用 native_make_##_pxx 函数,将 _o 转换为适当的类型并返回。
})

设置 PTE、PMD 和 PUD 的函数

定义了设置 PTE、PMD 和 PUD 的函数。

   static inline void native_set_pte(pte_t *ptep, pte_t pte)//native_set_pte 用于设置给定的页表项,pte_t *ptep: 指向要设置的页表项的指针,pte_t pte: 要设置的页表项值。
{
WRITE_ONCE(ptep->pte_high, pte.pte_high);//使用 WRITE_ONCE 宏将 pte_high 写入 ptep 指向的页表项的高位部分。
smp_wmb();//调用 smp_wmb(),确保在多处理器环境中写入的顺序。
WRITE_ONCE(ptep->pte_low, pte.pte_low);//使用 WRITE_ONCE 宏将 pte_low 写入 ptep 指向的页表项的低位部分。
}
static inline void native_set_pte_atomic(pte_t *ptep, pte_t pte)//以原子方式设置给定的页表项
{
pxx_xchg64(pte, ptep, native_pte_val(pte));//调用 pxx_xchg64 宏,执行原子交换操作,将 pte 的值设置到 ptep 指向的页表项中。
}
static inline void native_set_pmd(pmd_t *pmdp, pmd_t pmd)//用于设置给定的页中间目录
{
pxx_xchg64(pmd, pmdp, native_pmd_val(pmd));//将 pmd 的值设置到 pmdp 指向的页中间目录中。
}

static inline void native_set_pud(pud_t *pudp, pud_t pud)//设置给定的页上级目录
{
#ifdef CONFIG_PAGE_TABLE_ISOLATION
pud.p4d.pgd = pti_set_user_pgtbl(&pudp->p4d.pgd, pud.p4d.pgd);//设置用户页表
#endif
pxx_xchg64(pud, pudp, native_pud_val(pud));//将 pud 的值设置到 pudp 指向的页上级目录中。
}

清除 PTE、PMD 和 PUD 的函数

定义了清除 PTE、PMD 和 PUD 的函数。

   static inline void native_pte_clear(struct mm_struct *mm, unsigned long addr,pte_t *ptep)//清除给定的页表项(PTE),将其设置为无效状态。
{
WRITE_ONCE(ptep->pte_low, 0);//将 pte_low 设置为 0,清除页表项的低位部分。
smp_wmb();//确保在多处理器环境中写入的顺序
WRITE_ONCE(ptep->pte_high, 0);//将 pte_high 设置为 0,清除页表项的高位部分。
}

static inline void native_pmd_clear(pmd_t *pmdp)//清除给定的页中间目录(PMD),将其设置为无效状态。
{
WRITE_ONCE(pmdp->pmd_low, 0);//将 pmd_low 设置为 0,清除页中间目录的低位部分。
smp_wmb();//确保在多处理器环境中写入的顺序。
WRITE_ONCE(pmdp->pmd_high, 0);//将 pmd_high 设置为 0,清除页中间目录的高位部分。
}

static inline void native_pud_clear(pud_t *pudp)//清除给定的页上级目录(PUD)
{
}

static inline void pud_clear(pud_t *pudp)//清除给定的页上级目录(PUD),将其设置为无效状态。
{
set_pud(pudp, __pud(0));//将 pud 设置为无效状态。
}

获取并清除 PTE、PMD 和 PUD 的函数

根据是否启用 SMP,定义了获取并清除 PTE、PMD 和 PUD 的函数。

   #ifdef CONFIG_SMP
static inline pte_t native_ptep_get_and_clear(pte_t *ptep)//获取并清除给定的页表项(PTE),pte_t *ptep: 指向要获取并清除的页表项的指针。
{
return pxx_xchg64(pte, ptep, 0ULL);将 ptep 指向的页表项的值替换为 0ULL,并返回原来的 PTE 值。
}

static inline pmd_t native_pmdp_get_and_clear(pmd_t *pmdp)//获取并清除给定的页中间目录(PMD)
{
return pxx_xchg64(pmd, pmdp, 0ULL);//将 pmdp 指向的页中间目录的值替换为 0ULL,并返回原来的 PMD 值。
}

static inline pud_t native_pudp_get_and_clear(pud_t *pudp)//获取并清除给定的页上级目录(PUD)
{
return pxx_xchg64(pud, pudp);//将 pudp 指向的页上级目录的值替换为无效值,并返回原来的 PUD 值。
}

三、arm架构下页表分页机制源码分析

1.页表分配

这段代码定义了与 ARM 架构相关的页表分配操作。

源码:arc/arm/include/asm/pgtable.h

定义PGD分配和释放函数

   extern pgd_t *pgd_alloc(struct mm_struct *mm);//为给定的内存管理结构(mm_struct)分配一个新的页全局目录(PGD)。
extern void pgd_free(struct mm_struct *mm, pgd_t *pgd);//释放之前分配的页全局目录(PGD)

定义清理 PTE 表的函数

   static inline void clean_pte_table(pte_t *pte)//清理与页表项相关的缓存
{
clean_dcache_area(pte + PTE_HWTABLE_PTRS, PTE_HWTABLE_SIZE);//pte + PTE_HWTABLE_PTRS:计算要清理的缓存区域的起始地址,调用clean_dcache_area以清理指定区域的缓存。
}

定义 PTE 分配函数

这段代码展示了内核如何为内核空间和用户空间分配页表。

pte_alloc_one_kernel 函数:为内核分配一个新的页表项(PTE)页,使用 __pte_alloc_one_kernel 分配PTE,如果分配成功,调用 clean_pte_table 清理新分配的页表,返回分配的PTE指针。

pte_alloc_one 函数:为用户空间分配一个新的页表项页,使用 __pte_alloc_one 分配一个页,使用 GFP_PGTABLE_USER | PGTABLE_HIGHMEM 标志,如果分配失败,返回 NULL,如果分配的页不是高端内存页,调用 clean_pte_table 清理它,返回分配的页。

clean_pte_table 函数:用于初始化新分配的页表,可能涉及清零或设置特定的初始值。

使用CONFIG_HIGHPTE 允许在某些架构上将页表存储在高端内存中,这可以节省低端内存。

GFP_PGTABLE_USER 是一个特殊的分配标志,用于用户空间页表分配。

PageHighMem 检查是否为高端内存页。

page_address 函数:用于获取页的虚拟地址。

   #define __HAVE_ARCH_PTE_ALLOC_ONE_KERNEL
#define __HAVE_ARCH_PTE_ALLOC_ONE
#define __HAVE_ARCH_PGD_FREE
#include <asm-generic/pgalloc.h>

static inline pte_t *
pte_alloc_one_kernel(struct mm_struct *mm)//为内核分配一个页表项(PTE)
{
pte_t *pte = __pte_alloc_one_kernel(mm);//调用 __pte_alloc_one_kernel(mm) 分配一个页表项。

if (pte)
clean_pte_table(pte);//如果分配成功,调用 clean_pte_table(pte) 清理页表项相关的缓存。

return pte;//返回分配的页表项指针。
}

#ifdef CONFIG_HIGHPTE
#define PGTABLE_HIGHMEM __GFP_HIGHMEM
#else
#define PGTABLE_HIGHMEM 0
#endif

static inline pgtable_t
pte_alloc_one(struct mm_struct *mm)//为用户空间分配一个页表项(PTE),struct mm_struct *mm是指向当前进程的内存管理结构的指针。
{
struct page *pte;

pte = __pte_alloc_one(mm, GFP_PGTABLE_USER | PGTABLE_HIGHMEM);//分配一个页表项,使用用户空间分配标志和高内存标志。
if (!pte)
return NULL;//如果分配失败,返回 NULL。
if (!PageHighMem(pte))
clean_pte_table(page_address(pte));//如果分配的页表项不在高内存中,调用 clean_pte_table(page_address(pte)) 清理页表项相关的缓存。
return pte;//返回分配的页表项指针。
}

定义 PMD 填充函数

__pmd_populate 函数:内部辅助函数,用于填充PMD条目。

pmd_populate_kernel 函数:用于内核空间的PMD填充。

pmd_populate 函数:用于用户空间的PMD填充。

这段代码展示了ARM架构下内核如何管理多级页表结构,主要定义了与 ARM 架构相关的页表分配操作,包括页表的分配和释放、页表的填充等。通过这些定义,内核可以正确地管理和操作 ARM 架构的页表。它是内存管理子系统中关键的一部分,负责维护虚拟内存到物理内存的映射关系。

    static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,pmdval_t prot)//填充给定的页中间目录(PMD),将其设置为指向特定的页表项,pmd_t *pmdp指向要填充的 PMD 的指针,phys_addr_t pte是页表项的物理地址,pmdval_t prot是PMD 的保护标志。
{
pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;//计算 pmdval,将页表项地址加上 PTE_HWTABLE_OFF(页表项的偏移量),并与保护标志进行按位或操作。
pmdp[0] = __pmd(pmdval);//将计算得到的 pmdval 转换为 PMD 类型并赋值给 pmdp[0]。
#ifndef CONFIG_ARM_LPAE
pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));//如果未启用 ARM LPAE(大页表扩展),则填充 pmdp[1],将 pmdval 加上 256 个页表项的大小。
#endif
flush_pmd_entry(pmdp);//刷新 PMD 条目,以确保更新生效。
}

static inline void pmd_populate_kernel(struct mm_struct *mm, pmd_t *pmdp, pte_t *ptep)//在内核空间填充 PMD。
{
__pmd_populate(pmdp, __pa(ptep), _PAGE_KERNEL_TABLE);//调用 __pmd_populate,将页表项的物理地址和内核页表的保护标志 _PAGE_KERNEL_TABLE 传递给它。
}

static inline void
pmd_populate(struct mm_struct *mm, pmd_t *pmdp, pgtable_t ptep)//填充用户空间的 PMD。
{
extern pmdval_t user_pmd_table;//存储用户 PMD 表的保护标志。
pmdval_t prot;

if (__LINUX_ARM_ARCH__ >= 6 && !IS_ENABLED(CONFIG_ARM_LPAE))
prot = user_pmd_table;
else
prot = _PAGE_USER_TABLE;

__pmd_populate(pmdp, page_to_phys(ptep), prot);//调用 __pmd_populate,将页表的物理地址(通过 page_to_phys(ptep) 获取)和保护标志传递给它。
}

2.arm架构相关的两级页表操作

源码:arc/arm/include/asm/pgtable-2level.h

定义两级页表结构

定义了两级页表结构的相关常量和宏。

   #define __PAGETABLE_PMD_FOLDED 1

#define PTRS_PER_PTE 512//每个页表项(PTE)指向的条目数量,通常为 512。
#define PTRS_PER_PMD 1//每个页中间目录(PMD)指向的条目数量,通常为 1。
#define PTRS_PER_PGD 2048//每个页全局目录(PGD)指向的条目数量,通常为 2048。

#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)//硬件表中每个页表项的指针数量。
#define PTE_HWTABLE_OFF (PTE_HWTABLE_PTRS * sizeof(pte_t))//页表项的偏移量,计算为页表项数量乘以每个页表项的大小。
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))//页表项的总大小,计算为页表项数量乘以每个页表项的大小。
#define MAX_POSSIBLE_PHYSMEM_BITS 32//定义最大可能的物理内存位数,通常为 32 位,表示支持的最大物理内存为 4GB。
#define PMD_SHIFT 21//定义PMD的位移量,通常为 21 位
#define PGDIR_SHIFT 21定义PGD 的位移量,通常为 21 位
#define PMD_SIZE (1UL << PMD_SHIFT)//PMD 的大小
#define PMD_MASK (~(PMD_SIZE-1))//用于对齐 PMD 的掩码
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)//PGD 的大小
#define PGDIR_MASK (~(PGDIR_SIZE-1))//用于对齐 PGD 的掩码

#define SECTION_SHIFT 20//定义段的位移量,通常为 20 位。
#define SECTION_SIZE (1UL << SECTION_SHIFT)//段的大小
#define SECTION_MASK (~(SECTION_SIZE-1))//用于对齐段的掩码

#define SUPERSECTION_SHIFT 24//定义超级段的位移量,通常为 24 位。
#define SUPERSECTION_SIZE (1UL << SUPERSECTION_SHIFT)//超级段的大小
#define SUPERSECTION_MASK (~(SUPERSECTION_SIZE-1))//用于对齐超级段的掩码

#define USER_PTRS_PER_PGD (TASK_SIZE / PGDIR_SIZE)//计算用户空间每个进程的页全局目录(PGD)指针数量,TASK_SIZE 是用户空间的大小,PGDIR_SIZE 是每个 PGD 的大小。

定义 Linux PTE

定义了 Linux PTE 的相关常量和宏。

   #define L_PTE_VALID        (_AT(pteval_t, 1) << 0)        /* Valid */
#define L_PTE_PRESENT (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY (_AT(pteval_t, 1) << 7)
#define L_PTE_USER (_AT(pteval_t, 1) << 8)
#define L_PTE_XN (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED (_AT(pteval_t, 1) << 10) /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE (_AT(pteval_t, 1) << 11)

#define L_PTE_SWP_EXCLUSIVE L_PTE_RDONLY

#define L_PTE_MT_UNCACHED (_AT(pteval_t, 0x00) << 2) /* 0000 */
#define L_PTE_MT_BUFFERABLE (_AT(pteval_t, 0x01) << 2) /* 0001 */
#define L_PTE_MT_WRITETHROUGH (_AT(pteval_t, 0x02) << 2) /* 0010 */
#define L_PTE_MT_WRITEBACK (_AT(pteval_t, 0x03) << 2) /* 0011 */
#define L_PTE_MT_MINICACHE (_AT(pteval_t, 0x06) << 2) /* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC (_AT(pteval_t, 0x07) << 2) /* 0111 */
#define L_PTE_MT_DEV_SHARED (_AT(pteval_t, 0x04) << 2) /* 0100 */
#define L_PTE_MT_DEV_NONSHARED (_AT(pteval_t, 0x0c) << 2) /* 1100 */
#define L_PTE_MT_DEV_WC (_AT(pteval_t, 0x09) << 2) /* 1001 */
#define L_PTE_MT_DEV_CACHED (_AT(pteval_t, 0x0b) << 2) /* 1011 */
#define L_PTE_MT_VECTORS (_AT(pteval_t, 0x0f) << 2) /* 1111 */
#define L_PTE_MT_MASK (_AT(pteval_t, 0x0f) << 2)

定义 PUD 操作函数

这段代码定义了一系列与页表管理相关的内联函数和宏,特别是针对PUD(Page Upper Directory)和PMD(Page Middle Directory)级别的操作。这些定义通常用于ARM架构,特别是在不使用三级或四级页表的情况下。

PUD(Page Upper Directory)相关函数:

PMD(Page Middle Directory)相关函数:

这段代码针对的是一个简化的页表结构,可能是两级页表系统(PGD -> PMD -> PTE),PUD级别在这个架构中被忽略或不存在,所有PUD相关操作都是空操作或返回固定值,PMD操作被定义为直接操作页表条目,包括设置、清除、复制等,使用位操作来判断PMD的状态(如large、leaf、bad等),这是一种常见的优化技术,包含了一些架构特定的操作,如flush_pmd_entry和clean_pmd_entry,用于确保页表更改对硬件可见。

   #ifndef __ASSEMBLY__

static inline int pud_none(pud_t pud)
{
return 0;//检查给定的 PUD 是否为空。当前实现总是返回 0,表示 PUD 不为空。
}

static inline int pud_bad(pud_t pud)
{
return 0;//检查给定的 PUD 是否无效。当前实现总是返回 0,表示 PUD 是有效的。
}

static inline int pud_present(pud_t pud)
{
return 1;//检查给定的 PUD 是否存在。当前实现总是返回 1,表示 PUD 存在。
}

static inline void pud_clear(pud_t *pudp)
{
}//清除给定的 PUD。当前实现为空,表示没有具体的清除逻辑。

static inline void set_pud(pud_t *pudp, pud_t pud)
{
}//设置给定的 PUD。当前实现为空,表示没有具体的设置逻辑。

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
return (pmd_t *)pud;//返回指向 PMD 的指针。
}
#define pmd_offset pmd_offset

#define pmd_pfn(pmd)(__phys_to_pfn(pmd_val(pmd) & PHYS_MASK))//从 PMD 中提取物理页框号,使用 pmd_val 获取 PMD 的值,并与物理掩码进行按位与操作。

#define pmd_large(pmd)(pmd_val(pmd) & 2)//检查 PMD 是否为大页
#define pmd_leaf(pmd)(pmd_val(pmd) & 2)//检查 PMD 是否为叶节点。
#define pmd_bad(pmd)(pmd_val(pmd) & 2)//检查 PMD 是否无效。
#define pmd_present(pmd)(pmd_val(pmd))//检查 PMD 是否存在。

#define copy_pmd(pmdpd,pmdps) //复制 PMD 条目并刷新目标 PMD 条目。
do {
pmdpd[0] = pmdps[0];
pmdpd[1] = pmdps[1];
flush_pmd_entry(pmdpd);
} while (0)

#define pmd_clear(pmdp)//清除 PMD 条目,将其设置为无效,并调用 clean_pmd_entry 清理相关缓存。
do {
pmdp[0] = __pmd(0);
pmdp[1] = __pmd(0);
clean_pmd_entry(pmdp);
} while (0)

#define pmd_addr_end(addr,end) (end)//返回 PMD 地址范围的结束地址

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)//设置页表项(PTE)并扩展其属性

#define pmd_hugewillfault(pmd)(0)
#define pmd_thp_or_huge(pmd)(0)//检查 PMD 是否会导致故障或是否为大页。当前实现总是返回 0,表示不会导致故障或不是大页。
#endif /* __ASSEMBLY__ */

3.arm架构相关的三级页表操作

源码:arc/arm/include/asm/pgtable-3level.h

定义三级页表结构

定义了三级页表结构的相关常量和宏。

   #define PTRS_PER_PTE        512//每个页表项(PTE)指向的条目数量,通常为 512。
#define PTRS_PER_PMD 512//每个页中间目录(PMD)指向的条目数量,通常为 512。
#define PTRS_PER_PGD 4//每个页全局目录(PGD)指向的条目数量,通常为 4。

#define PTE_HWTABLE_PTRS (0)//每个页表项的指针数量,这里设置为 0,表示没有硬件表。
#define PTE_HWTABLE_OFF (0)//页表项的偏移量,这里设置为 0。
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u64))//页表项的总大小,计算为页表项数量乘以每个页表项的大小

#define MAX_POSSIBLE_PHYSMEM_BITS 40//定义最大可能的物理内存位数,通常为 40 位,表示支持的最大物理内存为 1TB。

#define PGDIR_SHIFT 30//定义 PGD 的位移量,为 30 位。
#define PMD_SHIFT 21//定义 PMD 的位移量,为 21 位。

#define PMD_SIZE (1UL << PMD_SHIFT)//PMD 的大小,计算为2MB。
#define PMD_MASK (~((1 << PMD_SHIFT) - 1))//用于对齐 PMD 的掩码。
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)//PGD 的大小,为1GB。
#define PGDIR_MASK (~((1 << PGDIR_SHIFT) - 1))//用于对齐 PGD 的掩码。

#define SECTION_SHIFT 21//定义段的位移量,为 21 位。
#define SECTION_SIZE (1UL << SECTION_SHIFT)//段的大小,计算为2MB。
#define SECTION_MASK (~((1 << SECTION_SHIFT) - 1))//用于对齐段的掩码。

#define USER_PTRS_PER_PGD (PAGE_OFFSET / PGDIR_SIZE)//计算用户空间每个进程的页全局目录(PGD)指针数量,PAGE_OFFSET 是用户空间的起始地址,PGDIR_SIZE 是每个 PGD 的大小。

定义大页相关常量

   #define HPAGE_SHIFT         PMD_SHIFT//将大页的位移量定义为 PMD 的位移量。
#define HPAGE_SIZE (_AC(1, UL) << HPAGE_SHIFT)//计算大页的大小。
#define HPAGE_MASK (~(HPAGE_SIZE - 1))// 计算大页的掩码,用于对齐大页地址。
#define HUGETLB_PAGE_ORDER (HPAGE_SHIFT - PAGE_SHIFT)//计算大页的页框顺序

定义 Linux PTE

   #define L_PTE_VALID         (_AT(pteval_t, 1) << 0) //表示页表项有效
#define L_PTE_PRESENT (_AT(pteval_t, 3) << 0)//表示页表项存在
#define L_PTE_USER (_AT(pteval_t, 1) << 6)//表示页表项的访问权限
#define L_PTE_SHARED (_AT(pteval_t, 3) << 8)//表示页表项是共享的。
#define L_PTE_YOUNG (_AT(pteval_t, 1) << 10)//表示页表项是最近被访问。
#define L_PTE_XN (_AT(pteval_t, 1) << 54)//表示页表项不可执行
#define L_PTE_DIRTY (_AT(pteval_t, 1) << 55)//表示页表项已被修改
#define L_PTE_SPECIAL (_AT(pteval_t, 1) << 56)//表示页表项是特殊的
#define L_PTE_NONE (_AT(pteval_t, 1) << 57)//表示页表项没有权限
#define L_PTE_RDONLY (_AT(pteval_t, 1) << 58)//表示页表项是只读的

#define L_PTE_SWP_EXCLUSIVE (_AT(pteval_t, 1) << 7)//表示页表项是交换独占的

#define L_PMD_SECT_VALID (_AT(pmdval_t, 1) << 0)//表示 PMD 节段有效
#define L_PMD_SECT_DIRTY (_AT(pmdval_t, 1) << 55)//表示 PMD 节段已被修改
#define L_PMD_SECT_NONE (_AT(pmdval_t, 1) << 57)//表示 PMD 节段没有权限
#define L_PMD_SECT_RDONLY (_AT(pteval_t, 1) << 58)//表示 PMD 节段是只读的

#define L_PTE_XN_HIGH (1 << (54 - 32))
#define L_PTE_DIRTY_HIGH (1 << (55 - 32))//表示高位的页表项标志

#define L_PTE_MT_UNCACHED (_AT(pteval_t, 0) << 2)//强序列化访问
#define L_PTE_MT_BUFFERABLE (_AT(pteval_t, 1) << 2)//正常非缓存
#define L_PTE_MT_WRITETHROUGH (_AT(pteval_t, 2) << 2)//正常内存写穿透
#define L_PTE_MT_WRITEBACK (_AT(pteval_t, 3) << 2)//正常内存写回
#define L_PTE_MT_WRITEALLOC (_AT(pteval_t, 7) << 2)//正常内存写分配
#define L_PTE_MT_DEV_SHARED (_AT(pteval_t, 4) << 2)//设备共享
#define L_PTE_MT_DEV_NONSHARED (_AT(pteval_t, 4) << 2)//设备非共享
#define L_PTE_MT_DEV_WC (_AT(pteval_t, 1) << 2)//正常非缓存
#define L_PTE_MT_DEV_CACHED (_AT(pteval_t, 3) << 2)//正常内存写回
#define L_PTE_MT_MASK (_AT(pteval_t, 7) << 2)//用于掩码操作

定义 PUD 操作函数

这段代码主要定义了与 ARM 架构相关的三级页表操作,包括页表的结构、页表项的定义、PUD 和 PMD 操作函数等。通过这些定义,内核可以正确地管理和操作 ARM 架构的三级页表。

   #ifndef __ASSEMBLY__

#define pud_none(pud) (!pud_val(pud))//检查 PUD 是否为空
#define pud_bad(pud) (!(pud_val(pud) & 2))//检查 PUD 是否无效
#define pud_present(pud) (pud_val(pud))//检查 PUD 是否存在
#define pmd_table(pmd) ((pmd_val(pmd) & PMD_TYPE_MASK) == PMD_TYPE_TABLE)//检查 PMD 是否指向页表
#define pmd_sect(pmd) ((pmd_val(pmd) & PMD_TYPE_MASK) == PMD_TYPE_SECT)//检查 PMD 是否指向段
#define pmd_large(pmd) pmd_sect(pmd)//检查 PMD 是否为大页
#define pmd_leaf(pmd) pmd_sect(pmd)//检查 PMD 是否为叶节点

#define pud_clear(pudp) //清除 PUD,将其设置为无效,并清理相关缓存
do {
*pudp = __pud(0);
clean_pmd_entry(pudp);
} while (0)

#define set_pud(pudp, pud) //设置 PUD,并刷新相关条目
do {
*pudp = pud;
flush_pmd_entry(pudp);
} while (0)

static inline pmd_t *pud_pgtable(pud_t pud)
{
return __va(pud_val(pud) & PHYS_MASK & (s32)PAGE_MASK);
}

#define pmd_bad(pmd) (!(pmd_val(pmd) & 2))//检查 PMD 是否无效

#define copy_pmd(pmdpd,pmdps) //复制 PMD 条目并刷新目标 PMD
do {
*pmdpd = *pmdps;
flush_pmd_entry(pmdpd);
} while (0)

#define pmd_clear(pmdp) //清除 PMD,将其设置为无效,并清理相关缓存
do {
*pmdp = __pmd(0);
clean_pmd_entry(pmdp);
} while (0)

#define __HAVE_ARCH_PTE_SAME
#define pte_same(pte_a,pte_b) ((pte_present(pte_a) ? pte_val(pte_a) & ~PTE_EXT_NG \
: pte_val(pte_a)) \
== (pte_present(pte_b) ? pte_val(pte_b) & ~PTE_EXT_NG \
: pte_val(pte_b)))

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,__pte(pte_val(pte)|(ext)))

#define pte_huge(pte) (pte_val(pte) && !(pte_val(pte) & PTE_TABLE_BIT))
#define pte_mkhuge(pte) (__pte(pte_val(pte) & ~PTE_TABLE_BIT))

#define pmd_isset(pmd, val) ((u32)(val) == (val) ? pmd_val(pmd) & (val) \
: !!(pmd_val(pmd) & (val)))
#define pmd_isclear(pmd, val) (!(pmd_val(pmd) & (val)))

#define pmd_present(pmd) (pmd_isset((pmd), L_PMD_SECT_VALID))//检查 PMD 是否存在
#define pmd_young(pmd) (pmd_isset((pmd), PMD_SECT_AF))//检查 PMD 是否为年轻状态
#define pte_special(pte) (pte_isset((pte), L_PTE_SPECIAL))
static inline pte_t pte_mkspecial(pte_t pte)
{
pte_val(pte) |= L_PTE_SPECIAL;
return pte;
}

#define pmd_write(pmd) (pmd_isclear((pmd), L_PMD_SECT_RDONLY))//检查 PMD 是否可写
#define pmd_dirty(pmd) (pmd_isset((pmd), L_PMD_SECT_DIRTY))//检查 PMD 是否已被修改

#define pmd_hugewillfault(pmd) (!pmd_young(pmd) || !pmd_write(pmd))
#define pmd_thp_or_huge(pmd) (pmd_huge(pmd) || pmd_trans_huge(pmd))

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
#define pmd_trans_huge(pmd) (pmd_val(pmd) && !pmd_table(pmd))
#endif

#define PMD_BIT_FUNC(fn,op) \
static inline pmd_t pmd_##fn(pmd_t pmd) { pmd_val(pmd) op; return pmd; }

PMD_BIT_FUNC(wrprotect, |= L_PMD_SECT_RDONLY);
PMD_BIT_FUNC(mkold, &= ~PMD_SECT_AF);
PMD_BIT_FUNC(mkwrite_novma, &= ~L_PMD_SECT_RDONLY);
PMD_BIT_FUNC(mkdirty, |= L_PMD_SECT_DIRTY);
PMD_BIT_FUNC(mkclean, &= ~L_PMD_SECT_DIRTY);
PMD_BIT_FUNC(mkyoung, |= PMD_SECT_AF);

#define pmd_mkhuge(pmd) (__pmd(pmd_val(pmd) & ~PMD_TABLE_BIT))

#define pmd_pfn(pmd) (((pmd_val(pmd) & PMD_MASK) & PHYS_MASK) >> PAGE_SHIFT)//从 PMD 中提取物理页框号PFN
#define pfn_pmd(pfn,prot) (__pmd(((phys_addr_t)(pfn) << PAGE_SHIFT) | pgprot_val(prot)))//将 PFN 和保护标志组合成 PMD
#define mk_pmd(page,prot) pfn_pmd(page_to_pfn(page),prot)//从页结构和保护标志创建 PMD

#define pmdp_establish generic_pmdp_establish

static inline pmd_t pmd_mkinvalid(pmd_t pmd)
{
return __pmd(pmd_val(pmd) & ~L_PMD_SECT_VALID);
}

static inline pmd_t pmd_modify(pmd_t pmd, pgprot_t newprot)
{
const pmdval_t mask = PMD_SECT_USER | PMD_SECT_XN | L_PMD_SECT_RDONLY |
L_PMD_SECT_VALID | L_PMD_SECT_NONE;
pmd_val(pmd) = (pmd_val(pmd) & ~mask) | (pgprot_val(newprot) & mask);
return pmd;
}

static inline void set_pmd_at(struct mm_struct *mm, unsigned long addr,
pmd_t *pmdp, pmd_t pmd)
{
BUG_ON(addr >= TASK_SIZE);

if (pmd_val(pmd) & L_PMD_SECT_NONE)
pmd_val(pmd) &= ~L_PMD_SECT_VALID;

if (pmd_write(pmd) && pmd_dirty(pmd))
pmd_val(pmd) &= ~PMD_SECT_AP2;
else
pmd_val(pmd) |= PMD_SECT_AP2;

*pmdp = __pmd(pmd_val(pmd) | PMD_SECT_nG);
flush_pmd_entry(pmdp);
}

#endif /* __ASSEMBLY__ */

四、示例实践

(1)代码功能

编写一个内核模块示例来演示如何在内核中创建一个页表映射。

(2)实现步骤

a.编写内核模块代码。

b.编译内核模块。

c.加载内核模块。

d.检查内核日志以分析结果。

e.卸载内核模块。

(3)代码实现

pagetable_example.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/highmem.h>
#include <linux/slab.h>

static int __init pagetable_example_init(void) {
struct page *page;
void *vaddr;
unsigned long paddr;

// 分配一个物理页
page = alloc_page(GFP_KERNEL);
if (!page) {
pr_err("Failed to allocate page\n");
return -ENOMEM;
}

// 获取物理地址
paddr = page_to_phys(page);
pr_info("Allocated page at physical address: 0x%lx\n", paddr);

// 映射到内核虚拟地址空间
vaddr = vmap(&page, 1, VM_MAP, PAGE_KERNEL);
if (!vaddr) {
pr_err("Failed to map page to virtual address\n");
__free_page(page);
return -ENOMEM;
}

pr_info("Mapped page to virtual address: %p\n", vaddr);

// 写入数据到映射的内存
strcpy(vaddr, "Hello, kernel page!");

// 读取数据
pr_info("Data in mapped page: %s\n", (char *)vaddr);

// 取消映射
vunmap(vaddr);

// 释放物理页
__free_page(page);

return 0;
}

static void __exit pagetable_example_exit(void) {
pr_info("Page table example module unloaded\n");
}

module_init(pagetable_example_init);
module_exit(pagetable_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of page table mapping in the Linux kernel");

编译和加载内核模块

    obj-m += pagetable_example.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

加载内核模块

sudo insmod pagetable_example.ko

检查内核日志以分析结果

卸载内核模块

sudo rmmod pagetable_example

分析结果

物理地址:0x199fe000 是分配的物理页的地址。
虚拟地址:000000007e626547 是映射到内核虚拟地址空间的地址。
数据:Hello, kernel page! 是写入和读取的测试数据。

总结

这个例子展示了如何在内核中分配一个物理页,将其映射到内核虚拟地址空间,进行读写操作,然后取消映射并释放物理页。

五、心得体会

通过对这一部分的学习,我真切的感受到了线性地址和物理地址的映射关系、内存空间的分配,以及在内存中读写信息的过程,对内核也有了进步一的认识,Linux内核页表映射确实是一个深入理解操作系统内存管理机制的重要过程。此次分析的内核源码为Linux6.8.0。


Linux内核之旅
Linux内核之旅
 最新文章