include指令用于在程序中引入外部文件或库,扩展功能并复用代码,提升开发效率与模块化程度。
在Linux系统中,虚拟地址到物理地址的转换依赖于页表(Page Table)这一核心机制,以下是详细转换原理和操作步骤,结合Linux内核实现逻辑说明:
核心概念
- 虚拟地址(VA)
进程使用的内存地址(如0x7ffeeb3a8000
),由CPU的MMU(内存管理单元)管理。 - 物理地址(PA)
实际内存芯片上的硬件地址(如0x2abf1000
)。 - 页表作用
存储虚拟页到物理页帧的映射关系,结构为多级树形(通常4级),由内核动态维护。
页表层级结构(以4级页表为例)
Linux使用四级页表划分虚拟地址:
| 层级 | 名称 | 作用 | 字段长度(x86_64) |
|———-|——————|——————————|————————|
| 1 | PGD (Page Global Directory) | 顶级页表 | 9 bits |
| 2 | P4D (Page 4th Directory) | 第四级目录(通常与PGD合并)| 9 bits |
| 3 | PUD (Page Upper Directory) | 上层页目录 | 9 bits |
| 4 | PMD (Page Middle Directory) | 中间页目录 | 9 bits |
| 5 | PTE (Page Table Entry) | 页表项,指向物理页帧 | 9 bits |
| – | 页内偏移 | 定位物理页内具体位置 | 12 bits |
虚拟地址结构(64位系统):[ 63:48 ] | PGD (9) | P4D (9) | PUD (9) | PMD (9) | PTE (9) | Offset (12) ]
转换步骤详解
假设虚拟地址为0x7ffeeb3a8000
:
-
获取当前进程页表基址
从CPU的CR3
寄存器(x86架构)读取PGD
基址(进程切换时由内核更新)。// 内核代码示例(arch/x86/include/asm/pgtable.h) pgd_t *pgd = pgd_offset(mm, address); // mm为进程内存描述符
-
逐级解析页表
按层级偏移量索引下一级页表:p4d_t *p4d = p4d_offset(pgd, address); pud_t *pud = pud_offset(p4d, address); pmd_t *pmd = pmd_offset(pud, address); pte_t *pte = pte_offset_kernel(pmd, address);
-
获取物理页帧号(PFN)
从PTE中提取物理页基址:unsigned long pfn = pte_pfn(*pte); // 从PTE获取页帧号
-
合成物理地址
物理地址 =(pfn << PAGE_SHIFT) | page_offset
PAGE_SHIFT
= 12(4KB页大小)page_offset
= 虚拟地址低12位
示例计算:
若虚拟地址0x7ffeeb3a8000
的PTE值为0x800000002abf1007
:
- 物理页帧号(PFN) =
0x2abf1
(取bit[51:12]) - 页内偏移 =
0x000
(低12位) - 物理地址 =
(0x2abf1 << 12) + 0x000 = 0x2abf1000
实际操作:内核模块示例
通过内核模块打印虚拟地址对应的物理地址:
static void print_phys_addr(unsigned long vaddr) {
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
unsigned long pfn;
pgd = pgd_offset(current->mm, vaddr); // 获取PGD
if (pgd_none(*pgd)) goto invalid;
p4d = p4d_offset(pgd, vaddr);
if (p4d_none(*p4d)) goto invalid;
pud = pud_offset(p4d, vaddr);
if (pud_none(*pud)) goto invalid;
pmd = pmd_offset(pud, vaddr);
if (pmd_none(*pmd)) goto invalid;
pte = pte_offset_kernel(pmd, vaddr);
if (!pte || pte_none(*pte)) goto invalid;
pfn = pte_pfn(*pte); // 从PTE提取PFN
pr_info("Virtual: 0x%lx → Physical: 0x%llx\n", vaddr, (pfn << PAGE_SHIFT) | (vaddr & ~PAGE_MASK));
return;
invalid:
pr_info("Address 0x%lx not mapped\n", vaddr);
}
static int __init my_init(void) {
unsigned long vaddr = __builtin_return_address(0); // 获取当前函数返回地址
print_phys_addr(vaddr);
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
输出示例:Virtual: 0x7ffeeb3a8000 → Physical: 0x2abf1000
关键注意事项
- 用户态地址转换
需在进程上下文(如内核模块)中操作,直接访问current->mm
。 - 地址有效性检查
使用access_ok()
验证用户地址合法性,避免解引用非法地址。 - 大页(HugePage)处理
若PMD指向大页(2MB/1GB),直接通过pmd_pfn(*pmd)
获取PFN。 - ARM架构差异
ARMv8使用TTBR0_EL1
寄存器存储PGD基址,页表层级可能为3/4级。
为什么需要页表?
- 内存隔离:每个进程拥有独立虚拟地址空间。
- 物理内存复用:不同进程可映射相同物理页(共享库、零页)。
- 延迟分配:物理页在首次访问时分配(缺页异常)。
引用说明
- Linux内核源码:
arch/x86/include/asm/pgtable.h
(页表操作API)mm/memory.c
(地址转换核心逻辑)
- 权威文献:
- Understanding the Linux Kernel, 3rd Edition (O’Reilly) – Chapter 2, 8
- Intel® 64 and IA-32 Architectures Software Developer’s Manual – Volume 3A, Section 4.3
- 内核文档:
- Documentation/x86/x86_64/mm.rst(地址空间布局)
基于Linux 5.15内核版本及x86_64架构,实际实现可能因架构或内核版本调整,建议参考最新内核文档或源码验证。
- Documentation/x86/x86_64/mm.rst(地址空间布局)
原创文章,发布者:酷番叔,转转请注明出处:https://cloud.kd.cn/ask/8682.html