Linux中paging_init页表初始化函数的实现
进行页表初始化paging_init
void __init paging_init(void)
{
#ifdef CONFIG_X86_PAEset_nx();if (nx_enabled)printk("NX (Execute Disable) protection: active\n");
#endifpagetable_init();load_cr3(swapper_pg_dir);#ifdef CONFIG_X86_PAE/** We will bail out later - printk doesn't work right now so* the user would just see a hanging kernel.*/if (cpu_has_pae)set_in_cr4(X86_CR4_PAE);
#endif__flush_tlb_all();kmap_init();zone_sizes_init();
}
1. 代码详细解析
1.1. PAE和NX功能初始化
#ifdef CONFIG_X86_PAEset_nx();if (nx_enabled)printk("NX (Execute Disable) protection: active\n");
#endif
CONFIG_X86_PAE
:内核配置选项,启用物理地址扩展- 只有在启用PAE时才执行这段代码
set_nx();
- 检测和启用NX功能
- 检查CPU是否支持NX(No Execute)特性
- 设置全局变量
nx_enabled
if (nx_enabled)printk("NX (Execute Disable) protection: active\n");
- 如果NX功能可用,打印激活信息
- 提供启动时的调试信息
1.2. 页表初始化
pagetable_init();
这是页表初始化的核心函数,主要完成:
// pagetable_init() 内部大致流程:
1. 初始化内核页表(swapper_pg_dir)
2. 建立内核空间的内存映射
3. 设置固定映射区域
4. 准备早期内存分配所需的页表结构
1.3. 加载页表基址
load_cr3(swapper_pg_dir);
CR3寄存器:
- x86架构的页表基址寄存器
- 包含当前活动页表的物理地址
效果:激活新初始化的内核页表,CPU开始使用新的地址转换规则。
1.4. 启用PAE模式
#ifdef CONFIG_X86_PAE/** We will bail out later - printk doesn't work right now so* the user would just see a hanging kernel.*/if (cpu_has_pae)set_in_cr4(X86_CR4_PAE);
#endif
条件检查:
if (cpu_has_pae)
- 检查CPU是否硬件支持PAE
cpu_has_pae
在启动早期通过CPUID检测设置
设置CR4寄存器:
set_in_cr4(X86_CR4_PAE);
X86_CR4_PAE
:CR4寄存器的第5位(PAE启用位)- 启用物理地址扩展,允许访问超过4GB的物理内存
1.5. 刷新TLB
__flush_tlb_all();
TLB刷新必要性:
- 修改页表或CR3后必须刷新TLB
- 否则CPU可能使用缓存的旧地址转换
1.6. 高端内存映射初始化
kmap_init();
- 初始化高端内存的临时映射机制
- 建立
kmap_atomic
等函数所需的页表结构 - 允许内核临时映射高端内存页面进行访问
1.7. 内存区域大小初始化
zone_sizes_init();
- 初始化内存管理区的页面计数
- 设置
ZONE_DMA
、ZONE_NORMAL
、ZONE_HIGHMEM
的大小 - 为后续的伙伴系统分配器做准备
检测和启用CPU的NX功能set_nx
static void __init set_nx(void)
{unsigned int v[4], l, h;if (cpu_has_pae && (cpuid_eax(0x80000000) > 0x80000001)) {cpuid(0x80000001, &v[0], &v[1], &v[2], &v[3]);if ((v[3] & (1 << 20)) && !disable_nx) {rdmsr(MSR_EFER, l, h);l |= EFER_NX;wrmsr(MSR_EFER, l, h);nx_enabled = 1;__supported_pte_mask |= _PAGE_NX;}}
}
1.代码详细解析
1.1. 变量声明
unsigned int v[4], l, h;
v[4]
:存储CPUID返回值的数组v[0]
= EAX,v[1]
= EBX,v[2]
= ECX,v[3]
= EDX
l, h
:MSR(模型特定寄存器)的低32位和高32位
1.2. 前置条件检查
if (cpu_has_pae && (cpuid_eax(0x80000000) > 0x80000001)) {
第一个条件:cpu_has_pae
- 检查CPU是否支持物理地址扩展
- NX功能需要PAE模式,因为只有在64位页表项中才有NX位
第二个条件:cpuid_eax(0x80000000) > 0x80000001
cpuid_eax(0x80000000) // 获取最大扩展CPUID功能号
- CPUID 0x80000000:查询CPU支持的扩展功能范围
- 如果返回值
> 0x80000001
,表示支持0x80000001
功能 - CPUID 0x80000001:包含AMD特性位,包括NX支持信息
1.3. 查询CPU特性
cpuid(0x80000001, &v[0], &v[1], &v[2], &v[3]);
CPUID 0x80000001 功能:
这个CPUID调用返回扩展特性位:
寄存器 内容
EDX (v[3]) 扩展特性标志位位20: NX/XD (Execute Disable) 支持
1.4. NX特性位检查
if ((v[3] & (1 << 20)) && !disable_nx) {
第一个条件:v[3] & (1 << 20)
- 检查EDX寄存器的第20位
- 如果该位为1,表示CPU硬件支持NX功能
第二个条件:!disable_nx
disable_nx
是全局变量,通过内核参数设置
1.5. 启用NX功能
rdmsr(MSR_EFER, l, h);
l |= EFER_NX;
wrmsr(MSR_EFER, l, h);
- MSR_EFER:模型特定寄存器
- 控制CPU的扩展功能
操作步骤:
- 读取当前值:
rdmsr(MSR_EFER, l, h)
- 设置NX位:
l |= EFER_NX
- 写回寄存器:
wrmsr(MSR_EFER, l, h)
1.6. 设置内核状态
nx_enabled = 1;
__supported_pte_mask |= _PAGE_NX;
- 设置全局变量,表示NX功能已启用
- 其他代码可以通过检查这个变量来知道NX是否可用
页表项掩码更新:
__supported_pte_mask
:定义内核支持的页表项位掩码_PAGE_NX
:NX位在页表项中的位置
页表初始化的核心函数pagetable_init
static void __init pagetable_init (void)
{unsigned long vaddr;pgd_t *pgd_base = swapper_pg_dir;#ifdef CONFIG_X86_PAEint i;/* Init entries of the first-level page table to the zero page */for (i = 0; i < PTRS_PER_PGD; i++)set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif/* Enable PSE if available */if (cpu_has_pse) {set_in_cr4(X86_CR4_PSE);}/* Enable PGE if available */if (cpu_has_pge) {set_in_cr4(X86_CR4_PGE);__PAGE_KERNEL |= _PAGE_GLOBAL;__PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;}kernel_physical_mapping_init(pgd_base);remap_numa_kva();/** Fixed mappings, only the page table structure has to be* created - mappings will be set by set_fixmap():*/vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;page_table_range_init(vaddr, 0, pgd_base);permanent_kmaps_init(pgd_base);#ifdef CONFIG_X86_PAE/** Add low memory identity-mappings - SMP needs it when* starting up on an AP from real-mode. In the non-PAE* case we already have these mappings through head.S.* All user-space mappings are explicitly cleared after* SMP startup.*/pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
#endif
}
1. 代码详细解析
1.1. 变量声明和初始化
unsigned long vaddr;
pgd_t *pgd_base = swapper_pg_dir;#ifdef CONFIG_X86_PAEint i;
vaddr
:虚拟地址变量,用于后续计算pgd_base
:指向swapper_pg_dir
,这是内核的主页全局目录swapper_pg_dir
:内核初始页表的根目录
1.2. PAE模式下的PGD初始化
#ifdef CONFIG_X86_PAE/* Init entries of the first-level page table to the zero page */for (i = 0; i < PTRS_PER_PGD; i++)set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif
将PGD所有条目初始化为指向零页面
-
PTRS_PER_PGD
:PGD中的条目数量(PAE模式下通常是4个) -
empty_zero_page
:全零的物理页面,用于处理缺页 -
__pa(empty_zero_page)
:获取零页面的物理地址 -
_PAGE_PRESENT
:设置页面存在位 -
将所有未使用的PGD条目指向安全的零页面
-
如果访问未映射的地址,会访问零页面而不是随机内存
-
零页面全为0,读取返回0,写入触发页错误
1.3. 启用PSE(Page Size Extension)
/* Enable PSE if available */if (cpu_has_pse) {set_in_cr4(X86_CR4_PSE);}
PSE功能:
- 允许使用4MB大页面(而不仅仅是4KB)
- 减少TLB缺失,提高性能
- 通过设置CR4寄存器的PSE位启用
1.4. 启用PGE(Page Global Enable)
/* Enable PGE if available */if (cpu_has_pge) {set_in_cr4(X86_CR4_PGE);__PAGE_KERNEL |= _PAGE_GLOBAL;__PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;}
PGE功能:
- 全局页面:在任务切换时不被TLB刷新
- 提高内核代码的性能(内核代码在所有进程间共享)
- 更新内核页面的标志位,包含
_PAGE_GLOBAL
1.5. 内核物理映射初始化
kernel_physical_mapping_init(pgd_base);
核心功能:建立内核直接映射
- 将物理内存直接映射到内核虚拟地址空间
1.6. NUMA内存重映射
remap_numa_kva();
NUMA支持:
- 非统一内存访问:多处理器系统中内存访问时间不一致
- 为NUMA系统重新映射内核虚拟地址空间
- 优化内存访问性能
1.7. 固定映射区域初始化
/** Fixed mappings, only the page table structure has to be* created - mappings will be set by set_fixmap():*/vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;page_table_range_init(vaddr, 0, pgd_base);
固定映射:
- 特殊用途:为需要固定虚拟地址的设备保留
- 计算范围:
__fix_to_virt(__end_of_fixed_addresses - 1)
获取最后一个固定映射地址 - 初始化页表:
page_table_range_init
为固定映射区域创建页表结构
1.8. 永久内核映射初始化
permanent_kmaps_init(pgd_base);
永久内核映射:
- 为
kmap()
函数建立永久内核映射区域 - 允许内核临时映射高端内存页面
1.9. PAE模式下的低内存恒等映射
#ifdef CONFIG_X86_PAE/** Add low memory identity-mappings - SMP needs it when* starting up on an AP from real-mode. In the non-PAE* case we already have these mappings through head.S.* All user-space mappings are explicitly cleared after* SMP startup.*/pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
#endif
恒等映射的重要性:
1. BSP启动 → 建立完整页表(包括内核直接映射)
2. 设置临时恒等映射 → pgd_base[0] = pgd_base[768]
3. 唤醒APs → APs通过恒等映射执行启动代码
4. APs切换到保护模式 → 使用正常的内核页表
5. 清除恒等映射 → 恢复用户空间隔离
6. SMP系统就绪 → 所有处理器正常运行
建立内核直接内存映射kernel_physical_mapping_init
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{unsigned long pfn;pgd_t *pgd;pmd_t *pmd;pte_t *pte;int pgd_idx, pmd_idx, pte_ofs;pgd_idx = pgd_index(PAGE_OFFSET);pgd = pgd_base + pgd_idx;pfn = 0;for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {pmd = one_md_table_init(pgd);if (pfn >= max_low_pfn)continue;for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;/* Map with big pages if possible, otherwise create normal page tables. */if (cpu_has_pse) {unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;if (is_kernel_text(address) || is_kernel_text(address2))set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));elseset_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));pfn += PTRS_PER_PTE;} else {pte = one_page_table_init(pmd);for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {if (is_kernel_text(address))set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));elseset_pte(pte, pfn_pte(pfn, PAGE_KERNEL));}}}}
}
1. 代码详细解析
1.1. 变量声明和初始化
unsigned long pfn;pgd_t *pgd;pmd_t *pmd;pte_t *pte;int pgd_idx, pmd_idx, pte_ofs;pgd_idx = pgd_index(PAGE_OFFSET);pgd = pgd_base + pgd_idx;pfn = 0;
变量说明:
pfn
:物理页框号,从0开始pgd, pmd, pte
:各级页表指针pgd_idx, pmd_idx, pte_ofs
:各级页表的索引pgd_index(PAGE_OFFSET)
:计算内核空间起始地址在PGD中的索引
1.2. 外层循环:遍历PGD
for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {pmd = one_md_table_init(pgd);if (pfn >= max_low_pfn)continue;
PGD循环:
- 从
pgd_idx
到PTRS_PER_PGD-1
- 覆盖整个内核虚拟地址空间
PMD表初始化:
pmd = one_md_table_init(pgd);
- 为当前PGD条目分配和初始化PMD表
- 返回指向PMD表的指针
提前终止检查:
if (pfn >= max_low_pfn)continue;
如果所有物理内存都已映射,跳过剩余的PMD处理。
1.3. 内层循环:遍历PMD
for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
PMD循环:
- 遍历PMD中的所有条目
- 继续直到处理完所有物理内存或PMD结束
地址计算:
address = pfn * PAGE_SIZE + PAGE_OFFSET;
计算当前物理页面对应的内核虚拟地址。
1.4. 大页映射处理(PSE支持)
/* Map with big pages if possible, otherwise create normal page tables. */if (cpu_has_pse) {unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;if (is_kernel_text(address) || is_kernel_text(address2))set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));elseset_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));pfn += PTRS_PER_PTE;}
大页优势:
- 普通页:4KB,需要512个PTE条目映射2MB
- 大页:2MB,一个PMD条目直接映射2MB
地址范围检查:
address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;
计算2MB大页的结束地址,用于检查是否包含内核代码。
内核代码特殊处理:
if (is_kernel_text(address) || is_kernel_text(address2))
如果大页包含内核代码区域,需要设置可执行权限
大页设置:
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC)); // 可执行大页
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE)); // 普通大页
pfn += PTRS_PER_PTE; // 跳过512个页面(2MB)
1.5. 普通页映射处理(无PSE支持)
} else {pte = one_page_table_init(pmd);for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {if (is_kernel_text(address))set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));elseset_pte(pte, pfn_pte(pfn, PAGE_KERNEL));}}
PTE表初始化:
pte = one_page_table_init(pmd);
为当前PMD条目分配和初始化PTE表
PTE级别循环:
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++)
遍历PTE表中的所有条目(通常512个),为每个物理页面创建映射
内核代码检测:
if (is_kernel_text(address))set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); // 可执行页面
elseset_pte(pte, pfn_pte(pfn, PAGE_KERNEL)); // 普通页面
在NUMA系统中重新映射内核虚拟地址空间remap_numa_kva
void __init remap_numa_kva(void)
{void *vaddr;unsigned long pfn;int node;for (node = 1; node < numnodes; ++node) {for (pfn=0; pfn < node_remap_size[node]; pfn += PTRS_PER_PTE) {vaddr = node_remap_start_vaddr[node]+(pfn<<PAGE_SHIFT);set_pmd_pfn((ulong) vaddr,node_remap_start_pfn[node] + pfn,PAGE_KERNEL_LARGE);}}
}
1. 代码详细解析
1.1. 变量声明
void *vaddr;unsigned long pfn;int node;
变量说明:
vaddr
:虚拟地址指针,用于计算映射的目标地址pfn
:页框号(Page Frame Number),用于遍历物理页面node
:NUMA节点编号,用于遍历所有节点
1.2. 外层循环:遍历NUMA节点
for (node = 1; node < numnodes; ++node) {
循环参数:
node = 1
:从节点1开始(节点0通常是启动节点,已经映射好了)node < numnodes
:遍历到最后一个节点numnodes
:系统中NUMA节点的总数
NUMA系统:
- 多处理器架构:将系统划分为多个节点
- 非统一访问:本地内存访问快,远程内存访问慢
1.3. 内层循环:遍历节点中的页面
for (pfn=0; pfn < node_remap_size[node]; pfn += PTRS_PER_PTE) {
循环参数:
pfn=0
:从节点的第一个页面开始pfn < node_remap_size[node]
:遍历节点中需要重映射的所有页面pfn += PTRS_PER_PTE
:每次增加512个页面
1.4. 计算虚拟地址
vaddr = node_remap_start_vaddr[node]+(pfn<<PAGE_SHIFT);
地址计算:
node_remap_start_vaddr[node] // 节点重映射的起始虚拟地址
pfn << PAGE_SHIFT // 页框号转换为字节偏移(×4096)
1.5. 设置大页映射
set_pmd_pfn((ulong) vaddr,node_remap_start_pfn[node] + pfn,PAGE_KERNEL_LARGE);
函数参数:
set_pmd_pfn(虚拟地址, 物理页框号, 页面标志)
映射效果:
虚拟地址 vaddr → 物理地址 (node_remap_start_pfn[node]) + (pfn × 4096)
使用2MB大页映射
1.6. 为什么需要重映射?
问题:
- 内核默认使用统一的虚拟地址空间
- 但物理内存分布在不同的NUMA节点上
- 远程内存访问速度较慢
解决方案:
- 为每个NUMA节点创建优化的虚拟地址映射
- 使得CPU可以高效访问本地内存
- 减少远程内存访问的开销
2. 关键数据结构
NUMA重映射信息:
// 节点重映射起始虚拟地址
node_remap_start_vaddr[node]// 节点重映射起始物理页框号
node_remap_start_pfn[node]// 节点需要重映射的页面数量
node_remap_size[node]
这些数据的来源:
- 在系统启动时通过
setup_memory
函数检测 - 由架构特定的NUMA初始化代码计算
为指定的虚拟地址范围初始化页表结构page_table_range_init
static void __init page_table_range_init (unsigned long start, unsigned long end, pgd_t *pgd_base)
{pgd_t *pgd;pmd_t *pmd;int pgd_idx, pmd_idx;unsigned long vaddr;vaddr = start;pgd_idx = pgd_index(vaddr);pmd_idx = pmd_index(vaddr);pgd = pgd_base + pgd_idx;for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {if (pgd_none(*pgd))one_md_table_init(pgd);pmd = pmd_offset(pgd, vaddr);for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) {if (pmd_none(*pmd))one_page_table_init(pmd);vaddr += PMD_SIZE;}pmd_idx = 0;}
}
1. 代码详细解析
1.1. 变量声明和初始化
pgd_t *pgd;pmd_t *pmd;int pgd_idx, pmd_idx;unsigned long vaddr;vaddr = start;pgd_idx = pgd_index(vaddr);pmd_idx = pmd_index(vaddr);pgd = pgd_base + pgd_idx;
pgd_t *pgd
:当前PGD条目指针pmd_t *pmd
:当前PMD条目指针pgd_idx, pmd_idx
:当前在PGD和PMD中的索引vaddr
:当前处理的虚拟地址
初始化计算:
vaddr = start; // 从起始地址开始
pgd_idx = pgd_index(vaddr); // 计算起始地址的PGD索引
pmd_idx = pmd_index(vaddr); // 计算起始地址的PMD索引
pgd = pgd_base + pgd_idx; // 获取起始PGD指针
1.2. 外层循环:遍历PGD
for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {
循环条件:
pgd_idx < PTRS_PER_PGD
:确保不超出PGD表范围vaddr != end
:确保没有处理完整个地址范围pgd++, pgd_idx++
:移动到下一个PGD条目
1.3. PGD级别初始化
if (pgd_none(*pgd))one_md_table_init(pgd);
惰性初始化:
- 检查PGD条目是否为空(未初始化)
- 如果为空,调用
one_md_table_init
分配和初始化PMD表 - 如果已初始化,直接使用现有的PMD表
1.4. 获取PMD指针
pmd = pmd_offset(pgd, vaddr);
计算当前虚拟地址对应的PMD条目指针。
1.5. 内层循环:遍历PMD
for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) {
pmd_idx < PTRS_PER_PMD
:不超出PMD表范围(通常512个条目)vaddr != end
:没有处理完地址范围pmd++, pmd_idx++
:移动到下一个PMD条目
1.6. PMD级别初始化
if (pmd_none(*pmd))one_page_table_init(pmd);
PTE表分配:
- 检查PMD条目是否为空
- 如果为空,调用
one_page_table_init
分配和初始化PTE表 - 如果已初始化,跳过PTE表分配
1.7. 地址递增和循环控制
vaddr += PMD_SIZE;}pmd_idx = 0;}
地址递增:
vaddr += PMD_SIZE; // 通常2MB,移动到下一个PMD管理的区域
PMD索引重置:
pmd_idx = 0; // 当切换到下一个PGD时,PMD索引从0开始
初始化永久内核映射permanent_kmaps_init
void __init permanent_kmaps_init(pgd_t *pgd_base)
{pgd_t *pgd;pmd_t *pmd;pte_t *pte;unsigned long vaddr;vaddr = PKMAP_BASE;page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);pgd = swapper_pg_dir + pgd_index(vaddr);pmd = pmd_offset(pgd, vaddr);pte = pte_offset_kernel(pmd, vaddr);pkmap_page_table = pte;
}
1. 代码详细解析
1.1. 变量声明
pgd_t *pgd;pmd_t *pmd;pte_t *pte;unsigned long vaddr;
变量说明:
pgd_t *pgd
:页全局目录指针pmd_t *pmd
:页中间目录指针pte_t *pte
:页表条目指针unsigned long vaddr
:虚拟地址变量
1.2. 设置永久内核映射基地址
vaddr = PKMAP_BASE;
PKMAP_BASE 定义:
- 永久内核映射区域的起始虚拟地址
#define PKMAP_BASE (0xff800000UL)
1.3. 初始化页表范围
page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
参数分解:
vaddr
:起始地址 = PKMAP_BASEvaddr + PAGE_SIZE*LAST_PKMAP
:结束地址pgd_base
:页表根目录
计算映射范围:
PAGE_SIZE * LAST_PKMAP // 永久映射区域的总大小
1.4. 获取PGD指针
pgd = swapper_pg_dir + pgd_index(vaddr);
1.5. 获取PMD指针
pmd = pmd_offset(pgd, vaddr);
通过PGD条目找到对应的PMD表指针
1.6. 获取PTE指针
pte = pte_offset_kernel(pmd, vaddr);
关键操作:
pte_offset_kernel(pmd, vaddr) // 计算PKMAP_BASE对应的PTE条目
返回的PTE:
指向永久内核映射区域第一个页面的页表条目
1.7. 保存PTE表指针
pkmap_page_table = pte;
这个指针是永久内核映射机制的核心入口点,后续的kmap
函数依赖它
1.8. 为什么需要永久内核映射?
32位系统的限制:
- 虚拟地址空间只有4GB
- 内核直接映射通常只有896MB
- 物理内存可能超过4GB(使用PAE)
- 需要机制来访问高端内存
PKMAP工作机制:
- 永久内核映射使用固定的虚拟地址区域
- 通过页表动态映射不同的物理页面
- 类似"窗口",通过这个窗口访问高端内存
初始化PMD页表one_md_table_init
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{pmd_t *pmd_table;#ifdef CONFIG_X86_PAEpmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));if (pmd_table != pmd_offset(pgd, 0))BUG();
#elsepmd_table = pmd_offset(pgd, 0);
#endifreturn pmd_table;
}
1. 代码详细解析
1.1. 函数声明和变量
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{pmd_t *pmd_table;
参数和变量:
pgd_t *pgd
:输入的PGD条目指针pmd_t *pmd_table
:返回的PMD表指针__init
:表示这是初始化函数,完成后内存会被释放
1.2. PAE模式下的PMD表初始化
#ifdef CONFIG_X86_PAEpmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
内存分配:
alloc_bootmem_low_pages(PAGE_SIZE)
- 在低端内存中分配一个页面(4KB)
- 用于存储PMD表
- 返回虚拟地址,转换为
pmd_t *
类型
为什么需要分配?
在PAE模式下:
- 页表项是64位的(8字节)
- 每个PMD表需要4KB空间存储512个条目
- 需要动态分配内存
1.3. 设置PGD条目
set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
操作分解:
__pa(pmd_table) // PMD表的物理地址
__pa(pmd_table) | _PAGE_PRESENT // 组合物理地址和存在位
__pgd(...) // 创建PGD条目值
set_pgd(pgd, ...) // 设置PGD条目
设置后的PGD条目:
PGD条目指向: PMD表的物理地址 | PRESENT位
1.4. 一致性验证
if (pmd_table != pmd_offset(pgd, 0))BUG();
验证逻辑:
pmd_offset(pgd, 0) // 根据PGD计算PMD表应该的位置
- 检查实际分配的PMD表位置与通过PGD计算的位置是否一致
- 如果不一致,触发内核BUG(严重错误)
1.5. 非PAE模式下的PMD处理
#elsepmd_table = pmd_offset(pgd, 0);
#endifreturn pmd_table;
}
非PAE模式:
pmd_table = pmd_offset(pgd, 0);
- 在非PAE模式下,PMD表是预分配的
- 直接通过
pmd_offset
计算PMD表位置 - 不需要动态分配内存
初始化PTE页表one_page_table_init
static pte_t * __init one_page_table_init(pmd_t *pmd)
{if (pmd_none(*pmd)) {pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));if (page_table != pte_offset_kernel(pmd, 0))BUG();return page_table;}return pte_offset_kernel(pmd, 0);
}
1. 代码详细解析
1.1. 函数声明
static pte_t * __init one_page_table_init(pmd_t *pmd)
{
参数和返回值:
pmd_t *pmd
:输入的PMD条目指针- 返回值:
pte_t *
- PTE表的指针 __init
:初始化函数,完成后内存会被释放
1.2. PMD空值检查
if (pmd_none(*pmd)) {
- 避免重复初始化:如果PMD已指向有效的PTE表,直接返回
- 惰性初始化:只在需要时分配PTE表,节省内存
1.3. PTE表内存分配
pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
内存分配:
alloc_bootmem_low_pages(PAGE_SIZE)
- 在低端内存中分配一个页面(4KB)
- 用于存储PTE表(页表条目数组)
- 返回虚拟地址,转换为
pte_t *
类型
1.4. 设置PMD条目
set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
操作分解:
__pa(page_table) // PTE表的物理地址
__pa(page_table) | _PAGE_TABLE // 组合物理地址和页表标志
__pmd(...) // 创建PMD条目值
set_pmd(pmd, ...) // 设置PMD条目
_PAGE_TABLE标志:
// 包含多个页表相关标志位,如:
_PAGE_PRESENT // 页面存在
_PAGE_RW // 可读写
_PAGE_USER // 用户可访问
_PAGE_ACCESSED // 已访问
设置后的PMD条目:
PMD条目指向: PTE表的物理地址 | 页表标志位
1.5. 一致性验证
if (page_table != pte_offset_kernel(pmd, 0))BUG();
验证逻辑:
pte_offset_kernel(pmd, 0) // 通过PMD计算PTE表应该的位置
- 检查实际分配的PTE表位置与通过PMD计算的位置是否一致
- 如果不一致,触发内核BUG
1.6. 返回PTE表指针
return page_table;}
返回新分配的PTE表指针,供调用者继续初始化具体的页表条目。
1.7. 已初始化情况的处理
return pte_offset_kernel(pmd, 0);
}
如果PMD已初始化:
pte_offset_kernel(pmd, 0)
- 直接通过PMD条目计算PTE表的虚拟地址
- 返回现有的PTE表指针