i386 虚拟地址空间布局(右键新标签页查看大图): 当 i386 架构支持 32-bit 页表,其支持 32 位线性地址空间和 32 位物理地址空间,因此 i386 架构虚拟地址空间长度为 4G. 其中用户空间长度 3Gig,内核空间占据末端 1Gig,并且支持 4MiB 和 4KiB 两种尺寸的物理页. 虚拟地址空间各区域的范围:
- 用户空间(Userspace): 每个进程 [0, 3G) 的虚拟内存区域,其中 STACK_TOP 是用户空间堆栈的栈底. 每个进程都只能看到自己的 3G 线性地址空间,无法看到其他进程的 3G 用户空间.
- 内核空间(Kernel Space): 内核线程使用的虚拟地址空间,其占据末端 1G 空间,起始虚拟地址为 0xC0000000, 内核空间又被划分成不同功能区域.
- 线性映射区(Direct Mapping Area): 线性映射区是内核将 0xC0000000 开始的 896M 虚拟内存映射到物理起始地址 0 的 896M 物理内存上,形成了虚拟连续且物理也连续的空间。
- 内核镜像区(Kernel Text Mapping): 该区域用于映射内核镜像,其范围是 [PAGE_OFFSET, PAGE_OFFSET + KERNEL_IMAGE_SIZE).
- 高端内存(high_memroy): 高端内存是没有被线性区映射的物理内存,内核访问高端内存需要先建立页表再访问,其范围 [high_memory, 4G).
- VMALLOC 区域: VMALLOC 分配器管理的区域,区域特点是虚拟地址连续但物理地址不连续的区域,其位于高端内存起始地址 VMALLOC_OFFSET 之后的地方,那么其范围: [VMALLOC_START, VMALLOC_END)
- MODULE 区域: 内核模块自身使用虚拟内存区域,该区域与 VMALLOC 区域重叠,因此内核也是为 MODULE 动态创建页表并映射到随机物理内存上.
- PKMAP: 该区域预先分配一些物理页,以备在内核运行期间特殊情况使用,如异常处理、中断处理和其他紧急内核任务, 其范围是 [PKMAP_BASE, PKMAP_BASE + PAGE_SIZE).
- LDT Remap for PTI Area: 该区域是用于重新映射 LDT 的一个特殊区域,并用于实现页表隔离机制(Page Table Isolation), 用于减轻特定 CPU 漏洞(Spectre 和 Meltdown)的攻击.
- CPU Entry Area: 该区域为每个 CPU 提供了一个独立的、预分配的内存段,其中包含了 CPU 启动和初始化所需的代码,以确保每个 CPU 可以正确进入操作系统的执行环境,其范围是:
- FIXADDR Area: 固定映射区域,区域的虚拟内存用作特殊的目的,例如 Permanent mapping Area 用于永久(持久)映射使用, 又如 KMAP 区域用于临时映射,满足特定需求,其范围是 [FIXADDR_START, FIXADDR_TOP)
- Tail Hole: 最后 4K 区域预留作为 Hole.
X86 虚拟地址空间布局(右键新标签页查看大图): 当 X86-64 架构支持 4 级页表,其支持 48 位线性地址空间和 52 位物理地址空间,用户空间长度达 128TiB,内核空间占据末端 128TiB,并且支持 1Gig、2MiB 和 4KiB 三种尺寸的物理页. 下图虚拟地址空间各区域的范围:
- 用户空间(Userspace): 每个进程 [0, 128TB) 的虚拟内存区域,其中 STACK_TOP 是用户空间堆栈的栈底. 每个进程都只能看到自己的 128TB 线性地址空间,无法看到其他进程的 128TB 用户空间.
- 内核空间(Kernel Space): 内核线程使用的虚拟地址空间,其占据末端 128T 空间,起始虚拟地址为 0xffff800000000000, 内核空间又被划分成不同功能区域.
- Guard Hole: 内核空间第一个区域,与 Non-Canonical 区域相连,大小为 8TiB。其作用是为内核空间创建一个保护边界,以便检测内存越界访问和栈溢出等错误。当程序访问 Guard Hole 区域时就会触发硬件异常,其范围是: [GUARD_HOLE_BASE_ADDR, LDT_BASE_ADDR)
- LDT Remap for PTI Area: 该区域是用于重新映射 LDT 的一个特殊区域,并用于实现页表隔离机制(Page Table Isolation), 用于减轻特定 CPU 漏洞(Spectre 和 Meltdown)的攻击. 其范围是: [LDT_BASE_ADDR, LDT_END_ADDR)
- 线性映射区(Direct Mapping Area): 线性映射区是内核将 PAGE_OFFSET 起始的虚拟内存连续建立页表,映射到连续的物理内存上,形成了虚拟连续且物理也连续的空间。其大小为 64TiB,对应 [PAGE_OFFSET, PAGE_OFFSET + 64TB).
- VMALLOC 区域: VMALLOC 分配器管理的区域,区域特点是虚拟地址连续但物理地址不连续的区域,长度为 32TiB 且范围: [VMALLOC_START, VMALLOC_END)
- SPARSE VMEMMAP Area: 该区域是 SPARSE 内存映射区域,用于存储物理页的 struct page 数据结构. 其范围是: [VMEMMAP_START, VMEMMAP_START + 1TB)
- KSAN Shadow Memory: 该区域是在内核运行时跟踪每个分配的内存块,并为每个字节分配一个影子字节,该字节的值表示分配的内存是否被访问过以及如何访问的信息,以此 KSAN 可以在内核中检查到访问越界或使用未初始化内存的问题. 其范围是: [KASAN_SHADOW_START, KASAN_SHADOW_END)
- CPU Entry Area: 该区域为每个 CPU 提供了一个独立的、预分配的内存段,其中包含了 CPU 启动和初始化所需的代码,以确保每个 CPU 可以正确进入操作系统的执行环境,其范围是: [CPU_ENTRY_AREA_BASE, CPU_ENTRY_AREA_BASE + 0.5T)
- Fixup Stacks: ESP fixup 堆栈固定区域,其范围是: [0xffffff0000000000, 0xffffff0000000000 + 0.5T)
- EFI Region 映射区域: 区域的作用就是在内核中映射这些 EFI 内存映射寄存器,以便内核能够访问和操作这些信息. EFI 内存映射寄存器是一些用于访问系统内存映射信息的寄存器。它们包含了有关物理内存、I/O 端口、PCI 设备等映射的信息,操作系统和引导加载程序可以通过这些寄存器获得硬件映射信息. 区域的范围是: [EFI_VA_END, EFI_VA_START)
- 内核镜像区(Kernel Text Mapping): 该区域用于映射内核镜像,其范围是 [__START_KERNEL_map, __START_KERNEL_map + KERNEL_IMAGE_SIZE).
- MODULE 区域: 内核模块自身使用虚拟内存区域,该区域的范围是: [MODULES_VADDR, MODULES_END)
- FIXADDR Area: 固定映射区域,区域的虚拟内存用作特殊的目的,例如 Permanent mapping Area 用于永久(持久)映射使用, 又如 KMAP 区域用于临时映射,满足特定需求,其范围是 [FIXADDR_START, VSYSCALL_ADDR)
- Leacy SYSCALL: 特殊的系统调用区域,已提供给所有进程共同使用,其范围是: [VSYSCALL_ADDR, VSYSCALL_ADDR + PAGE_SIZE)
- Tail Hole: 最后 2M 区域预留作为 Hole.
X86 虚拟地址空间布局(右键新标签页查看大图): 当 X86-64 架构支持 5 级页表,其支持 57 位线性地址空间和 52 位物理地址空间,用户空间长达 64PB,内核空间占据末端 64PB,并且支持 1Gig、2MiB 和 4KiB 三种尺寸的物理页. 下图虚拟地址空间各区域的范围:
- 用户空间(Userspace): 每个进程 [0, 64PB) 的虚拟内存区域,其中 STACK_TOP 是用户空间堆栈的栈底. 每个进程都只能看到自己的 64PB 线性地址空间,无法看到其他进程的 64PB 用户空间.
- 内核空间(Kernel Space): 内核线程使用的虚拟地址空间,其占据末端 64PB 空间,起始虚拟地址为 0xff00000000000000, 内核空间又被划分成不同功能区域.
- Guard Hole: 内核空间第一个区域,与 Non-Canonical 区域相连,大小为 4PB。其作用是为内核空间创建一个保护边界,以便检测内存越界访问和栈溢出等错误。当程序访问 Guard Hole 区域时就会触发硬件异常,其范围是: [GUARD_HOLE_BASE_ADDR, LDT_BASE_ADDR)
- LDT Remap for PTI Area: 该区域是用于重新映射 LDT 的一个特殊区域,并用于实现页表隔离机制(Page Table Isolation), 用于减轻特定 CPU 漏洞(Spectre 和 Meltdown)的攻击. 其范围是: [LDT_BASE_ADDR, LDT_END_ADDR)
- 线性映射区(Direct Mapping Area): 线性映射区是内核将 PAGE_OFFSET 起始的虚拟内存连续建立页表,映射到连续的物理内存上,形成了虚拟连续且物理也连续的空间。其大小为 32PB,对应 [PAGE_OFFSET, PAGE_OFFSET + 32PB).
- VMALLOC 区域: VMALLOC 分配器管理的区域,区域特点是虚拟地址连续但物理地址不连续的区域,长度为 12.5B 且范围: [VMALLOC_START, VMALLOC_END)
- SPARSE VMEMMAP Area: 该区域是 SPARSE 内存映射区域,用于存储物理页的 struct page 数据结构. 其范围是: [VMEMMAP_START, VMEMMAP_START + 0.5PB)
- KSAN Shadow Memory: 该区域是在内核运行时跟踪每个分配的内存块,并为每个字节分配一个影子字节,该字节的值表示分配的内存是否被访问过以及如何访问的信息,以此 KSAN 可以在内核中检查到访问越界或使用未初始化内存的问题. 其范围是: [KASAN_SHADOW_START, KASAN_SHADOW_END)
- CPU Entry Area: 该区域为每个 CPU 提供了一个独立的、预分配的内存段,其中包含了 CPU 启动和初始化所需的代码,以确保每个 CPU 可以正确进入操作系统的执行环境,其范围是: [CPU_ENTRY_AREA_BASE, CPU_ENTRY_AREA_BASE + 0.5T)
- Fixup Stacks: ESP fixup 堆栈固定区域,其范围是: [0xffffff0000000000, 0xffffff0000000000 + 0.5T)
- EFI Region 映射区域: 区域的作用就是在内核中映射这些 EFI 内存映射寄存器,以便内核能够访问和操作这些信息. EFI 内存映射寄存器是一些用于访问系统内存映射信息的寄存器。它们包含了有关物理内存、I/O 端口、PCI 设备等映射的信息,操作系统和引导加载程序可以通过这些寄存器获得硬件映射信息. 区域的范围是: [EFI_VA_END, EFI_VA_START)
- 内核镜像区(Kernel Text Mapping): 该区域用于映射内核镜像,其范围是 [__START_KERNEL_map, __START_KERNEL_map + KERNEL_IMAGE_SIZE).
- MODULE 区域: 内核模块自身使用虚拟内存区域,该区域的范围是: [MODULES_VADDR, MODULES_END)
- FIXADDR Area: 固定映射区域,区域的虚拟内存用作特殊的目的,例如 Permanent mapping Area 用于永久(持久)映射使用, 又如 KMAP 区域用于临时映射,满足特定需求,其范围是 [FIXADDR_START, VSYSCALL_ADDR)
- Leacy SYSCALL: 特殊的系统调用区域,已提供给所有进程共同使用,其范围是: [VSYSCALL_ADDR, VSYSCALL_ADDR + PAGE_SIZE)
- Tail Hole: 最后 2M 区域预留作为 Hole.
用户空间(Userspace) 是用户进程运行时的虚拟内存,当进程运行时会将虚拟内存划分成不同的区域,每个区域用于存储不同的数据或执行特定的任务。每个区域的具体含义如下:
- [0x0000000, __executable_start) 区域为预留区域
- [__executable_start, 0x00007FFFFFFFFFFF) 用户进程可以使用的虚拟内存
- .text 区域存储了进程的代码段
- .data 区域存储了进程的数据段
- .bss 区域为进程的 .bss 段, 初始化为 0 的数据加载到该段
- Heap 进程的堆,向高地址(向上)生长,可以通过 malloc() 和 brk() 为进程小粒度的虚拟内存
- MMAP 区域通过 mmap() 和 malloc() 向进程提供大块连续的虚拟内存,其向低地址(向下)生长,分配的虚拟内存可以用来映射共享库或者文件等.
- Stack 区域为进程的堆栈,其向下生长,栈底是栈区域的起始地址,栈顶是栈最新扩展的区域,栈利用先进后出的模式存储进程运行时的临时数据
- argv/environ 区域用于存储进程运行时的参数和环境变量相关的信息.
- Kernel Space 为进程看到的内核空间,不同架构存在位置差异,在 IA32 架构内核空间和用户空间相邻,但在有的 X86 架构两者并不相邻,但 PAGE_OFFSET 之后的虚拟内存就是内核空间.
系统初始化正常运行时,系统存在多个用户进程同时运行的情况,由于进程隔离性的存在,每个进程都有各自的地址空间,并且每个进程只看得到自己的虚拟内存和内核空间,因此进程运行时会认为系统只有自己和内核线程在运行。由于该特性的存在,所有用户进程看到内核空间都是一致的,因此在进程切换的时候内核空间不需要切换,另外对于不同的进程就算用户空间虚拟地址相同,但两个虚拟内存对于的内容完全不是一个东西.