【Note】《深入理解Linux内核》 Chapter 5 :内存地址的表示——Linux虚拟内存体系结构详解
《深入理解Linux内核》 Chapter 5 :内存地址的表示——Linux虚拟内存体系结构详解
摘要: 物理地址与虚拟地址的关系,用户态与内核态地址空间划分,页表结构及其在32位和64位体系中的实现,线性地址与段机制,Linux页表维护与映射接口分析
一、引言
Linux 内核基于现代操作系统的核心设计原则:虚拟内存(Virtual Memory)。这一机制不仅为每个进程提供了独立的地址空间,提高了安全性和稳定性,也极大简化了内存管理和资源共享。本章深入探讨了内存地址表示机制的核心概念,主要包括:
- 物理地址与虚拟地址的映射关系
- 虚拟地址结构:线性地址、页地址
- Linux对用户空间和内核空间的划分
- 页表的层级结构和访问方式
- 分页机制与段机制的结合
- 实现中关键结构与内核API
深入理解 Linux 如何建立进程隔离与统一内存管理机制,为后续页表操作、内存分配与调页机制打下理论基础。
二、物理地址与虚拟地址
2.1 物理地址(Physical Address)
- 是内存芯片上的实际地址,由 CPU 通过内存总线访问;
- 通常由内存控制器和硬件平台定义;
- 操作系统管理整个物理内存的使用,避免进程直接访问。
2.2 虚拟地址(Virtual Address)
- 是进程或内核中代码看到的地址;
- 每个进程看到的虚拟地址空间是隔离的;
- 通过内核维护的页表映射到真实物理地址。
目的:通过虚拟地址,操作系统实现了地址空间的隔离、权限控制、内存共享和需求分页等机制。
三、内核空间与用户空间
3.1 地址空间划分(以32位为例)
在32位体系结构中(如 x86),虚拟地址为32位,即最多表示 4GB 空间。Linux 对这4GB空间进行了划分:
区域 | 范围 | 描述 |
---|---|---|
用户空间 | 0x00000000 ~ 0xBFFFFFFF | 进程可见,仅能访问自己的用户空间 |
内核空间 | 0xC0000000 ~ 0xFFFFFFFF | 所有内核代码共享,不对用户可见 |
- 高端内存(High Memory):不直接映射入内核虚拟空间的物理内存;
- 内核空间对所有进程共享,因此访问时需注意并发与上下文保护。
3.2 64位架构划分(x86_64)
在x86_64上,地址空间更广泛:
- 虚拟地址宽度常为48位,支持 256TB 地址空间;
- Linux 通常将内核空间放在高地址区(如从
0xffff800000000000
开始); - 用户空间仍然从 0 开始,进程各自独立。
#define TASK_SIZE 0x00007ffffffff000 // 用户空间最大地址(x86_64)
四、线性地址与分页机制
4.1 段机制与线性地址
尽管 Intel x86 支持段机制(段寄存器、段描述符等),Linux 实际上将段机制简化为平坦模型:
- 所有段(CS、DS、SS)都从 0 开始,长度覆盖全部地址空间;
- 线性地址(Linear Address)= 段基址 + 虚拟地址;
- 简化后,线性地址与虚拟地址几乎等价。
4.2 分页机制
分页机制将线性地址划分为页目录 + 页表项 + 页内偏移等多级结构。
32位 x86 分页结构:
- 页大小:4KB(2^12)
- 线性地址结构:
位段范围 | 作用 |
---|---|
31~22 (10位) | 页目录项索引 PDE |
21~12 (10位) | 页表项索引 PTE |
11~0 (12位) | 页内偏移 |
页表结构说明:
- 每个页表或目录项大小为 4 字节(32位);
- 每页 4KB,可存 1024 个项;
- 整个页表结构可映射 1024 * 1024 * 4KB = 4GB 地址空间。
64位分页结构(x86_64):
- 四级页表结构:PGD → PUD → PMD → PTE;
- 每级使用9位地址偏移(加上12位页偏移,共48位);
- 页大小仍为4KB或更大(如2MB、1GB HugePage);
五、内核页表结构与数据结构
5.1 内核关键结构
pgd_t
:页全局目录项(Page Global Directory)pud_t
:页上层目录项pmd_t
:页中间目录项pte_t
:页表项
每个结构由typedef
定义,包含实际硬件页项及辅助宏。
5.2 每个进程的页表
- 每个进程结构体
task_struct
包含mm_struct *mm
指针; mm_struct
内含pgd_t *pgd
,指向该进程页表根;active_mm
是当前激活的页表,在内核线程中使用。
struct mm_struct {pgd_t *pgd;...
};
5.3 虚拟地址到物理地址映射过程
- 从虚拟地址提取 PGD 索引 → 获取PGD项;
- 使用 PUD 索引 → 获取 PUD;
- 然后 PMD → PTE;
- 最后 PTE 中存储物理页帧号(PFN);
- 加上页内偏移,得到物理地址。
5.4 内核页表宏定义
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
六、虚拟地址映射方式
Linux提供不同的虚拟地址映射方式:
6.1 恒定映射(恒等映射)
在启动阶段,内核将物理地址映射到固定虚拟地址区域(如 PAGE_OFFSET + phys_addr
),常用于低端内存。
- 示例:
__va(p)
/__pa(v)
宏可在虚拟与物理地址间转换。
6.2 高端内存映射(Highmem)
由于32位系统虚拟地址有限,不能映射所有物理内存。内核使用以下机制:
kmap()
/kunmap()
:临时将高端页映射入内核空间;kmap_atomic()
:不可被中断打断的高速映射;- 用于访问超过直接映射范围的页帧。
6.3 ioremap 映射外设内存
I/O设备(PCI、MMIO)的物理地址不能通过常规内存页访问,需使用 ioremap()
创建虚拟映射:
void __iomem *vaddr = ioremap(phys_addr, size);
这种映射不可被缓存,常用于寄存器访问。
七、Linux地址宏与辅助工具
常用宏
宏/函数 | 功能 |
---|---|
__pa(vaddr) | 虚拟地址转物理地址 |
__va(paddr) | 物理地址转虚拟地址 |
virt_to_phys() | 与__pa类似,但适用于内核地址 |
phys_to_virt() | 物理地址映射回虚拟地址 |
page_address() | 页结构体转为虚拟地址 |
八、内核如何初始化页表
- boot阶段:引导加载器加载内核后,启动代码(如
arch/x86/boot/compressed/head_64.S
)初始化临时页表; - setup_arch():设置内核页表映射;
- paging_init():创建初始页表,包括内核空间、IO区映射;
- memblock:记录系统物理内存布局,辅助建立映射;
- 内核页表重定位:64位内核将自身映射到高地址段。
九、多种内存页大小支持(HugeTLB)
Linux 支持更大页大小(如 2MB、1GB),减少页表项数量,提升TLB命中率,适合大内存数据库、图像处理等应用。
- 通过
mmap()
+MAP_HUGETLB
使用; - 内核支持 Transparent Huge Pages(THP)自动使用大页。
十、小结
第五章深入剖析了 Linux 内核的虚拟地址管理体系:
- 区分了物理地址与虚拟地址,说明其在内核中的转换关系;
- 探讨了用户空间与内核空间的划分及其意义;
- 详细介绍了 x86 32/64位 的多级页表结构;
- 描述了从虚拟地址到物理地址的页表查找过程;
- 阐明了高端内存映射、高速访问、IO映射等机制;
- 给出了重要结构体(
pgd_t
,mm_struct
)与宏接口; - 简要说明了内核启动时页表的初始化与扩展流程。
本章是理解 Linux 内存管理、页表操作、缺页中断处理与内核模块开发的前提基础。
十一、实践建议
- 查看进程地址空间
cat /proc/<pid>/maps
- 使用
pagemap
分析页表
cat /proc/<pid>/pagemap
- 调试内核页表
- 使用
cr3
,pgd
,pte
调试命令(gdb / qemu); - 在模块中通过
virt_to_phys
查看映射关系;
- 编写实验模块
- 使用
__get_free_page()
分配页并观察其物理地址; kmap_atomic()
访问高端页并打印虚拟地址。