Linux 进程内存模型与虚拟内存、物理内存页映射
本笔记从程序的编译、加载、运行开始,系统总结 Linux 进程内存布局,并进一步解释虚拟地址如何通过 MMU、TLB 和多级页表映射到物理内存,以及缺页异常、按需分页、写时复制、Swap 和物理页管理的工作机制。
说明:32 位部分主要采用经典 x86 Linux 教学模型;64 位页表部分以常见 x86-64、4 KiB 页面为例。ARM64 的基本原理相同,但页表级数、页面大小、寄存器和页表项格式存在差异。
第一部分:程序从源代码到进程
一、程序、可执行文件与进程
首先区分三个概念:
- 程序源代码:程序员编写的 C/C++ 等代码。
- 可执行文件:编译、汇编、链接后生成的文件,例如 ELF。
- 进程:程序被操作系统执行时形成的运行实体。
完整过程:
C/C++ 源代码
|
v
编译 Compiler
|
v
汇编 Assembly
|
v
目标文件 Object File
|
v
链接 Linker
|
v
ELF 可执行文件
|
v
execve() 加载程序
|
v
建立进程虚拟地址空间
|
v
CPU 开始执行
例如:
int global_a = 100;
int global_b;
int main(void)
{
int a = 10;
return 0;
}
编译:
gcc test.c -o test
执行:
./test
执行后,操作系统为程序建立运行环境,程序的代码、数据、堆、栈等通过虚拟地址空间组织。
需要注意:可执行文件不等于进程。一个可执行文件可以同时运行多个进程实例。
二、ELF 文件并不是整体复制到物理内存
ELF 文件可能包含:
- 机器指令
- 只读数据
- 已初始化数据
- 未初始化数据的描述
- 动态链接信息
- 符号表
- 重定位信息
常见 Section:
| Section | 主要作用 |
|---|---|
| .text | 机器指令 |
| .rodata | 只读数据 |
| .data | 已初始化的可写静态数据 |
| .bss | 零初始化或未显式初始化的静态数据 |
| .symtab | 符号表 |
| .strtab | 字符串表 |
| .dynamic | 动态链接信息 |
ELF 还有 Program Header。
必须区分:
ELF 文件
|
+-- Section:描述文件中的逻辑内容
|
+-- Segment:描述程序加载时的映射单元
|
v
Linux Loader
|
v
虚拟内存区域 VMA
|
v
按需建立物理页映射
Linux 加载 ELF 时主要依据 Program Header 中的可加载 Segment,而不是简单地为每一个 Section 建立独立内存区域。
例如 PT_LOAD 描述的内容可能映射为可执行、只读或可写的虚拟内存区域。
重点:程序加载并不意味着所有代码和数据立即进入物理 RAM。
很多页面可以在首次访问时才加载。
第二部分:Linux 进程虚拟地址空间
三、虚拟地址、逻辑地址与物理地址
现代 Linux 用户程序通常使用虚拟地址(Virtual Address),而不是直接使用物理地址。
三个概念:
- 逻辑地址(Logical Address):程序执行过程中使用的地址概念。
- 线性地址(Linear Address):在 x86 架构中,经过分段机制后得到的地址。
- 物理地址(Physical Address):最终用于访问物理内存或其他物理地址空间资源的地址。
在现代 x86-64 Linux 的常见平坦内存模型中,逻辑地址经过分段处理后通常与线性地址数值一致,因此日常讨论经常将逻辑地址与虚拟地址近似使用。
但严格来说:
x86 逻辑地址
|
v
分段机制 Segmentation
|
v
线性地址 Linear Address
|
v
分页机制 Paging
|
v
物理地址 Physical Address
ARM64 通常不采用传统 x86 式分段机制,其虚拟地址直接经过地址转换机制形成物理地址。
因此,本笔记后续统一使用“虚拟地址”表示程序访问内存时使用的地址。
四、为什么需要虚拟内存
假设计算机有 8 GiB 物理内存。
运行三个进程:
Process A
Process B
Process C
如果所有程序直接使用物理地址,会出现:
- 不同进程容易发生地址冲突。
- 一个进程可能破坏另一个进程的数据。
- 程序必须考虑实际物理内存布局。
- 内存分配与回收复杂。
- 难以实现按需加载、共享内存等机制。
引入虚拟内存后:
Process A 虚拟空间 ──┐
|
Process B 虚拟空间 ──┼──> 页表映射 ──> 物理 RAM
|
Process C 虚拟空间 ──┘
每个进程拥有独立的用户态虚拟地址空间。
例如:
Process A
虚拟地址 0x400000
|
v
物理地址 0x120000
Process B
虚拟地址 0x400000
|
v
物理地址 0x350000
虽然两个进程使用相同虚拟地址,但可以映射到不同物理页面。
虚拟内存带来的核心能力:
- 进程隔离。
- 地址空间抽象。
- 按需分配物理内存。
- 文件映射。
- 共享物理页面。
- 内存访问权限保护。
- 写时复制。
- 支持交换和内存回收。
需要强调:虚拟地址空间很大,不代表机器真的有同等容量的 RAM。
五、32 位 Linux 经典地址空间
32 位地址理论上可以表示:
2^32 Bytes = 4 GiB
经典 32 位 x86 Linux 教学模型:
0xFFFFFFFF
+------------------------+
| |
| Kernel Virtual Space |
| |
| 1 GiB |
| |
+------------------------+ 0xC0000000
| |
| User Virtual Space |
| |
| 3 GiB |
| |
+------------------------+
0x00000000
即:
用户虚拟空间:3 GiB
内核虚拟空间:1 GiB
但是必须注意:
- 3G/1G 是经典配置,不是所有 Linux 的固定规则。
- 还存在 2G/2G 等划分方式。
- 这里划分的是虚拟地址范围,不是物理 RAM。
- 不代表内核独占 1 GiB 物理内存。
- 64 位 Linux 不应使用该固定模型解释。
六、64 位 Linux 地址空间
64 位地址理论上可表示:
2^64 Bytes
但 CPU 不一定实现全部 64 位虚拟地址位。
例如 x86-64 常见:
- 4 级页表:通常使用 48 位规范虚拟地址。
- 5 级页表:通常使用 57 位规范虚拟地址。
CPU 实现的物理地址位数也可能小于 64 位。
因此:
虚拟地址宽度、物理地址宽度和 CPU 通用寄存器宽度不是同一个概念。
Linux 内核会根据架构、硬件能力及配置管理用户和内核虚拟地址空间。
第三部分:进程内存区域
七、经典进程内存布局
高虚拟地址
|
+--------------------------------+
| argv / env / auxv |
+--------------------------------+
| Stack |
| |
| 向低地址增长 |
| ↓ |
+--------------------------------+
| mmap |
| Shared Libraries |
| File Mapping |
| Anonymous Mapping |
+--------------------------------+
| ↑ |
| Heap |
| 向高地址增长 |
+--------------------------------+
| .bss |
+--------------------------------+
| .data |
+--------------------------------+
| .rodata |
+--------------------------------+
| .text |
+--------------------------------+
|
低虚拟地址
这只是经典示意图。
现代 Linux 还受到以下因素影响:
- ASLR
- PIE
- mmap
- 动态链接
- 多线程
- vDSO
- vvar
- CPU 架构
- 内核配置
真实进程布局并不一定严格按照图中顺序排列。
八、Text / Code
Text 区域主要保存程序机器指令。
例如:
int add(int a, int b)
{
return a + b;
}
编译后:
C 代码
|
v
汇编指令
|
v
机器指令
|
v
.text
运行时通常具有:
r-x
权限:
- r:可读。
- w:可写。
- x:可执行。
代码区域通常不允许普通写入。
从分页角度看,代码所在虚拟页面可以映射到具有执行权限的物理页框。
多个运行同一程序的进程可以共享相同的只读代码物理页面。
九、CPU 如何执行代码
CPU 使用程序计数器保存指令执行位置。
常见名称:
| 架构 | 寄存器 |
|---|---|
| x86 | EIP |
| x86-64 | RIP |
| ARM64 | PC |
简化执行过程:
PC / RIP
|
v
指令虚拟地址
|
v
地址转换 MMU / TLB
|
v
物理地址
|
v
Cache / 内存层次
|
v
Fetch
|
v
Decode
|
v
Execute
程序计数器保存的是程序执行所需的指令地址,不是简单地保存一个固定物理 RAM 地址。
实际 CPU 流水线、缓存和地址转换过程可能并行或重叠执行。
十、rodata:只读数据
.rodata 通常保存:
- 字符串字面量。
- 某些 const 对象。
- 编译器生成的只读表。
例如:
void test(void)
{
char *str = "hello";
}
概念上:
Stack / Register
+------------------+
| str |
| | |
+---|--------------+
|
v
只读映射区域
+------------------+
| h e l l o \0 |
+------------------+
需要区分:
- str 是指针变量。
- "hello" 是字符串字面量。
两者是不同对象。
对字符串字面量进行修改属于未定义行为。
十一、Data Segment
.data 通常保存已经显式初始化、需要可写存储的静态存储期对象。
例如:
int global_a = 100;
static int global_b = 200;
void test(void)
{
static int count = 10;
}
通常:
global_a
global_b
count
|
v
.data
这些变量具有静态存储期。
即使 static 局部变量写在函数内部,它也不会随着函数返回而销毁。
十二、BSS
.bss 通常保存:
- 未显式初始化的全局变量。
- 零初始化的全局变量。
- 未显式初始化的 static 变量。
- 零初始化的 static 变量。
例如:
int global_a;
static int global_b;
void test(void)
{
static int count;
}
这些对象在程序启动时被零初始化。
为什么需要 BSS
例如:
char buffer[100 * 1024 * 1024];
这是一个 100 MiB 的全局数组。
如果 ELF 文件直接保存 100 MiB 的零,会浪费磁盘空间。
因此:
ELF 文件
|
+-- 描述需要 100 MiB 零初始化空间
|
v
运行时建立虚拟映射
|
v
访问时提供零初始化页面
BSS 占用运行时地址空间,但不需要在 ELF 文件中保存同等大小的零数据。
后续分页机制会进一步解释:BSS 对应的物理页面可以按需分配。
十三、const 不等于一定在 rodata
例如:
void test(void)
{
const int a = 10;
}
编译器可能:
- 将 a 放入寄存器。
- 将 a 放入栈。
- 将 a 直接优化掉。
而:
static const int table[] = {1, 2, 3};
更可能位于只读映射区域。
所以:
const 是语言层面的修改限制,不等于固定的 ELF 存储位置。
十四、Heap
Heap 主要用于动态内存分配。
C 语言:
malloc()
calloc()
realloc()
free()
C++:
new
delete
例如:
int *p = malloc(1024);
概念上:
Stack
+-------------+
| p |
+------|------+
|
v
动态内存区域
+------------------+
| 1024 Bytes |
+------------------+
p 是指针变量,malloc 返回的是动态分配区域的地址。
二者存储位置不一定相同。
Heap 的增长
传统模型中 Heap 向高虚拟地址增长。
低地址
.text
.data
.bss
Heap
↑
↑
高地址
Linux 可以通过 brk/sbrk 调整 program break。
但现代 malloc 实现也可能通过 mmap 获取内存。
因此:
malloc 分配的对象不一定全部来自传统 brk Heap。
十五、Memory Mapping Area
mmap 区域可以用于:
- 共享库映射。
- 文件映射。
- 匿名内存映射。
- 共享内存。
- 大块动态内存分配。
例如:
Process
|
+-- executable
+-- heap
+-- libc.so
+-- libm.so
+-- anonymous mmap
+-- stack
mmap 的重要特点是建立虚拟地址区域与文件或匿名内存对象之间的关系。
它不意味着所有页面已经立即装入物理内存。
十六、Stack
Stack 主要与以下内容有关:
- 函数调用。
- 部分局部变量。
- 参数传递。
- 返回地址。
- 保存寄存器状态。
经典栈通常向低虚拟地址增长。
高地址
+------------------+
| Stack |
| ↓ |
| ↓ |
+------------------+
↑
↑
Heap
低地址
实际栈布局由架构、ABI、编译器及操作系统共同决定。
十七、Stack Frame
函数调用时,程序通常需要保存调用状态。
这部分逻辑结构称为 Stack Frame(栈帧)。
例如:
int add(int a, int b)
{
int result = a + b;
return result;
}
int main(void)
{
int x = add(10, 20);
return 0;
}
概念上:
高地址
+--------------------+
| main Stack Frame |
+--------------------+
| add Stack Frame |
| 参数 / 局部状态 |
| 返回地址等 |
+--------------------+
低地址
add 返回后,其调用帧通常不再需要。
递归函数
func(3)
|
v
func(2)
|
v
func(1)
同一个函数可以同时存在多个调用帧。
不能理解成“每个函数永远只有一个栈帧”。
编译器优化
开启优化:
gcc -O2 test.c -o test
编译器可能:
- 将变量放进寄存器。
- 内联函数。
- 消除变量。
- 省略帧指针。
- 完全消除某些函数调用。
因此普通局部变量不一定实际位于栈中。
十八、参数和环境变量
Linux 程序启动时,初始栈中通常包含:
- argc
- argv
- envp
- Auxiliary Vector(auxv)
例如:
./test hello world
对应:
argc = 3
argv[0] = "./test"
argv[1] = "hello"
argv[2] = "world"
环境变量例如:
PATH=/usr/bin:/bin
HOME=/home/user
LANG=zh_CN.UTF-8
十九、ASLR
ASLR:
Address Space Layout Randomization。
地址空间布局随机化。
可能随机化:
- Stack。
- Heap。
- mmap。
- Shared Libraries。
- PIE Executable。
目的之一是增加内存漏洞利用的难度。
同一个程序多次运行时,部分虚拟地址可能发生变化。
需要区分:
- ASLR 改变虚拟地址空间的布局。
- 页表负责虚拟地址到物理地址的映射。
二者属于不同层面的机制。
二十、指针变量与内存位置
例如:
int a = 10;
int *p = &a;
如果 a、p 都是普通局部变量:
p:Stack / Register
a:Stack / Register
再例如:
char *p = "hello";
通常:
p:Stack / Register
"hello":只读映射区域
再例如:
char *p = malloc(100);
通常:
p:Stack / Register
p 指向的数据:Heap / mmap 动态内存
最重要的规律:
指针变量存在哪里,与指针指向的数据存在哪里,是两个不同问题。
第四部分:虚拟内存如何映射物理内存
二十一、前面的内存模型到底描述了什么
前面看到:
.text
.rodata
.data
.bss
Heap
mmap
Stack
这些区域主要描述进程的虚拟地址空间布局。
并不是物理 RAM 中存在固定排列的:
物理 Text 区
物理 Data 区
物理 Heap 区
物理 Stack 区
实际情况是:
进程虚拟地址空间
+------------------+
| Stack |
+------------------+
| mmap |
+------------------+
| Heap |
+------------------+
| BSS |
+------------------+
| Data |
+------------------+
| Text |
+------------------+
|
v
页表映射
|
v
物理 RAM
+------------------+
| Page Frame 20 |
+------------------+
| Page Frame 3 |
+------------------+
| Page Frame 91 |
+------------------+
| Page Frame 7 |
+------------------+
虚拟地址连续,并不意味着对应物理地址连续。
这是理解分页机制最关键的前提。
二十二、Page 与 Page Frame
分页系统中有两个核心概念。
Page:虚拟页
Page 是虚拟地址空间按照固定粒度划分的页面。
例如使用 4 KiB 页面:
虚拟地址空间
Page 0:0x0000 ~ 0x0FFF
Page 1:0x1000 ~ 0x1FFF
Page 2:0x2000 ~ 0x2FFF
Page 3:0x3000 ~ 0x3FFF
每页大小:
4 KiB = 4096 Bytes = 2^12 Bytes
Page Frame:物理页框
物理内存也按照页面粒度划分。
例如:
物理 RAM
Frame 0
Frame 1
Frame 2
Frame 3
...
虚拟页可以映射到物理页框。
Virtual Page 0 ---> Physical Frame 7
Virtual Page 1 ---> Physical Frame 2
Virtual Page 2 ---> Physical Frame 9
Virtual Page 3 ---> Physical Frame 1
虚拟页号与物理页框号不必相同。
物理页框也不必连续。
注意:4 KiB 是常见页面大小,不是所有架构和配置的唯一选择。
二十三、为什么分页能解决物理内存碎片问题
假设一个进程需要 12 KiB 内存。
页面大小为 4 KiB。
则需要:
12 KiB / 4 KiB = 3 Pages
虚拟地址空间:
+------------+
| Page 0 |
+------------+
| Page 1 |
+------------+
| Page 2 |
+------------+
物理内存可能是:
Frame 0:其他进程
Frame 1:当前进程 Page 2
Frame 2:空闲
Frame 3:当前进程 Page 0
Frame 4:其他进程
Frame 5:当前进程 Page 1
映射关系:
Page 0 ---> Frame 3
Page 1 ---> Frame 5
Page 2 ---> Frame 1
进程看到连续的 12 KiB 虚拟空间。
物理 RAM 中却不需要连续的 12 KiB 空间。
分页减少了连续物理内存分配的需求,但仍可能存在页内碎片,且大页或某些设备分配仍可能要求连续物理页框。
二十四、页表 Page Table
页表用于描述虚拟页面如何映射到物理页框。
概念上:
| 虚拟页号 VPN | 物理页框号 PFN | 状态 |
|---|---|---|
| 0 | 7 | 有效 |
| 1 | 2 | 有效 |
| 2 | 9 | 有效 |
| 3 | — | 未映射 |
VPN:
Virtual Page Number。
PFN:
Page Frame Number。
页表项(PTE)不仅包含映射信息,还可能包含:
- Present:页面是否存在于当前硬件映射中。
- Read/Write:写权限。
- User/Supervisor:用户态访问权限。
- Execute Disable / NX:禁止执行。
- Accessed:访问标志。
- Dirty:修改标志。
- 物理页框地址信息。
具体位定义依 CPU 架构而异。
二十五、MMU
MMU:
Memory Management Unit。
内存管理单元。
负责执行硬件地址转换及权限检查等工作。
简化过程:
CPU
|
| 产生虚拟地址
v
MMU
|
| 查询 TLB / 页表
v
物理地址
|
v
Cache / RAM / 内存系统
必须区分:
- 操作系统负责建立和维护页表等内存管理结构。
- MMU 根据当前页表配置进行硬件地址转换。
- TLB 用于缓存地址转换结果。
不是操作系统在每一次普通内存访问时都亲自计算物理地址。
二十六、虚拟地址如何拆分
以 32 位虚拟地址、4 KiB 页面为例。
页面大小:
4 KiB = 2^12 Bytes
因此页内偏移需要 12 位。
剩余:
32 - 12 = 20 位
用于表示虚拟页号。
32-bit Virtual Address
+--------------------+------------+
| Virtual Page Number| Page Offset|
| 20 bits | 12 bits |
+--------------------+------------+
地址转换时:
- 虚拟页号用于查找物理页框。
- 页内偏移保持不变。
公式:
Virtual Address = VPN × PageSize + Offset
Physical Address = PFN × PageSize + Offset
二十七、地址转换计算实例
假设:
页面大小:4 KiB
虚拟地址:0x00001234
由于:
4 KiB = 0x1000
所以:
VPN = 0x00001234 >> 12
= 0x1
Offset = 0x234
假设页表中:
VPN 0x1 ---> PFN 0x8
那么:
Physical Address
= 0x8 × 0x1000 + 0x234
= 0x8234
完整过程:
Virtual Address
0x00001234
|
+-- VPN = 0x1
|
+-- Offset = 0x234
|
v
查询页表
|
v
PFN = 0x8
|
v
Physical Address
0x00008234
重点:页表改变的是页框映射,页内偏移保持不变。
二十八、虚拟地址连续,物理地址可以不连续
例如:
虚拟地址空间
0x1000 ~ 0x1FFF ---> Frame 8
0x2000 ~ 0x2FFF ---> Frame 3
0x3000 ~ 0x3FFF ---> Frame 15
虚拟地址:
0x1FFF
0x2000
彼此连续。
但对应物理地址可能是:
0x8FFF
0x3000
完全不连续。
CPU 和程序通过虚拟地址工作,不需要知道实际物理页框的排列顺序。
第五部分:多级页表
二十九、为什么不能简单使用一个巨大页表
假设:
- 32 位虚拟地址。
- 4 KiB 页面。
- 每个页表项 4 Bytes。
虚拟页面数量:
2^32 / 2^12 = 2^20
即:
1,048,576 个虚拟页面
如果采用单级、完整数组式页表:
2^20 × 4 Bytes = 4 MiB
每个进程都可能需要约 4 MiB 的完整页表空间。
大量进程会造成浪费。
因此需要多级页表。
三十、经典 x86 32 位二级页表
在传统 32 位 x86 非 PAE 分页模型中:
32-bit Virtual Address
+----------+----------+------------+
| 10 bits | 10 bits | 12 bits |
+----------+----------+------------+
| | |
| | +-- 页内偏移
| +--------------- 页表索引
+-------------------------- 页目录索引
转换过程:
虚拟地址
|
v
Page Directory
|
v
Page Table
|
v
Physical Frame
|
v
加上 Page Offset
|
v
物理地址
如果某个虚拟地址范围完全没有映射,可以不创建对应的下级页表。
这就是多级页表节省空间的重要原因。
三十一、x86-64 四级页表
以常见的 48 位规范虚拟地址、4 KiB 页面为例。
地址划分:
48-bit Virtual Address
+-------+-------+-------+-------+------------+
| PML4 | PDPT | PD | PT | Offset |
| 9 bit | 9 bit | 9 bit | 9 bit | 12 bit |
+-------+-------+-------+-------+------------+
转换过程:
Virtual Address
|
v
PML4
|
v
PDPT
|
v
Page Directory
|
v
Page Table
|
v
Page Frame
|
v
Physical Address
每级索引 9 位:
2^9 = 512
每个页表通常包含 512 个 8 字节表项:
512 × 8 = 4096 Bytes
正好为 4 KiB。
x86-64 的 48 位规范地址还要求高位满足规范地址的符号扩展规则,不能把任意 64 位数都当作合法虚拟地址。
三十二、x86-64 五级页表
部分 x86-64 CPU 支持 LA57,即五级分页。
概念上:
Virtual Address
|
v
PML5
|
v
PML4
|
v
PDPT
|
v
PD
|
v
PT
|
v
Physical Frame
在 4 KiB 页面配置下,通常支持 57 位规范虚拟地址。
是否启用取决于 CPU、Linux 内核及启动配置。
三十三、页表本身存在哪里
页表不是抽象的数学公式。
页表结构本身也需要占用物理内存。
物理 RAM
+---------------------+
| 普通数据页面 |
+---------------------+
| 代码页面 |
+---------------------+
| 页表页面 |
+---------------------+
| 内核数据页面 |
+---------------------+
在 x86-64 上,CR3 寄存器包含当前地址转换所使用的顶级页表基址等相关控制信息。
CPU 根据该信息进行页表遍历。
ARM64 则使用 TTBR0_EL1、TTBR1_EL1 等寄存器配置不同地址范围的转换表基址。
三十四、进程切换时页表如何变化
假设:
Process A ---> Page Table A
Process B ---> Page Table B
当操作系统进行进程上下文切换时,如果切换了地址空间,就需要让 CPU 使用相应的地址转换上下文。
Process A
|
v
Page Table A
|
v
CPU 执行 A
发生上下文切换
Process B
|
v
Page Table B
|
v
CPU 执行 B
现代 CPU 可能使用 PCID、ASID 等机制区分地址空间,避免每次切换都完全清空 TLB。
因此不能简单认为每次进程切换都必须刷新所有 TLB 项。
第六部分:TLB 与地址转换性能
三十五、为什么需要 TLB
如果 CPU 每次访问内存都要进行完整的四级页表遍历,会产生额外开销。
因此 CPU 使用:
TLB(Translation Lookaside Buffer)。
它是一种用于缓存地址转换信息的硬件结构。
CPU 产生虚拟地址
|
v
TLB
|
+----+----+
| |
Hit Miss
| |
v v
使用缓存 页表遍历
| |
| v
| 得到映射
| |
+----+----+
|
v
物理地址
TLB Hit
TLB 命中。
可以直接使用缓存的转换结果,并进行相应权限检查。
TLB Miss
TLB 未命中。
硬件通常需要遍历页表。
如果映射有效,获取转换结果并可能填入 TLB。
注意:
TLB Miss 不等于 Page Fault。
TLB Miss 只说明转换缓存未命中。
Page Fault 则表示地址转换或访问权限检查遇到了需要异常处理的情况。
三十六、TLB 与 CPU Cache 的区别
| 对比 | TLB | CPU Cache |
|---|---|---|
| 主要缓存 | 地址转换信息 | 指令或数据 |
| 解决问题 | 加快虚拟地址转换 | 减少较慢内存访问 |
| 典型未命中处理 | 页表遍历 | 访问下级缓存或内存 |
| 是否直接存储普通变量内容 | 通常不是 | 是 |
因此:
TLB:帮助找到地址
Cache:帮助快速取得数据
这是概念性区分,真实 CPU 的缓存层次和地址转换流水线更加复杂。
第七部分:缺页异常与按需分页
三十七、Page Fault 是什么
Page Fault:
缺页异常。
当 CPU 访问某个虚拟地址,而当前页表状态无法直接完成合法访问时,可能产生缺页异常。
原因可能包括:
- 虚拟页尚未分配物理页框。
- 文件映射页面尚未载入。
- 页面被换出。
- 访问违反页面权限。
- 写入触发 COW。
因此:
Page Fault 不一定代表程序错误。
它可能是 Linux 正常内存管理流程的一部分。
三十八、按需分页 Demand Paging
假设程序:
#include <stdlib.h>
int main(void)
{
char *p = malloc(100 * 1024 * 1024);
if (p == NULL)
return 1;
p[0] = 1;
free(p);
return 0;
}
申请 100 MiB 动态内存,不意味着立刻消耗 100 MiB 的新物理 RAM。
可能发生:
malloc(100 MiB)
|
v
分配器取得或管理虚拟地址区域
|
v
尚未为所有页面分配物理页框
|
v
访问 p[0]
|
v
触发缺页异常
|
v
Linux 分配或建立物理页映射
|
v
程序继续执行
实际行为还取决于分配器、已有内存、内核配置及页面状态。
三十九、缺页异常处理流程
CPU 访问虚拟地址
|
v
页表转换失败或权限异常
|
v
Page Fault Exception
|
v
进入 Linux 内核
|
v
检查地址所属 VMA
|
+-- 地址非法
| |
| v
| 产生相应错误
| 用户进程可能收到 SIGSEGV
|
+-- 地址合法
|
v
检查缺页原因
|
+-- 匿名页面
| |
| v
| 分配/映射零页或新页面
|
+-- 文件映射
| |
| v
| 查找页缓存/读取文件
|
+-- COW
| |
| v
| 建立私有可写副本
|
+-- Swap
|
v
换入页面
|
v
更新相关页表
|
v
重新执行故障指令
上述是典型情况的简化模型。某些错误也可能产生 SIGBUS 等其他结果。
四十、Minor Fault 与 Major Fault
Linux 通常区分:
Minor Page Fault
处理缺页时不需要为该页面执行磁盘 I/O。
例如:
- 分配新的匿名页面。
- 页面已经存在于页缓存。
- 某些写时复制操作。
Major Page Fault
处理缺页时需要执行相应的存储 I/O。
例如需要从文件或 Swap 读取页面。
可以查看:
/usr/bin/time -v ./test
输出中可能包含:
Major page faults
Minor page faults
注意:Major Fault 不等于所有磁盘 I/O;普通文件读写也可能产生 I/O,而不通过缺页异常触发。
第八部分:Heap、Stack、BSS 如何映射物理内存
四十一、BSS 的实际物理页分配
例如:
char buffer[100 * 1024 * 1024];
int main(void)
{
buffer[0] = 1;
return 0;
}
逻辑过程:
ELF 描述 BSS 大小
|
v
Linux 建立零初始化虚拟映射
|
v
程序访问 buffer[0]
|
v
可能发生写缺页
|
v
分配可写物理页面
|
v
写入数据
不需要因为 BSS 声明了 100 MiB,就立即为整个数组分配 100 MiB 独立物理页。
四十二、Heap 的实际物理页分配
例如:
char *p = malloc(4096);
malloc 返回的通常是用户虚拟地址。
malloc()
|
v
返回虚拟地址
|
v
访问虚拟页面
|
v
MMU 查询页表
|
v
物理页框
注意:
malloc 是用户态内存分配接口。
它通常依赖分配器管理 brk/mmap 获得的虚拟内存区域,而不是每调用一次 malloc 就必然向内核申请一个物理页。
四十三、Stack 的实际物理页分配
Stack 是虚拟地址空间中的一段映射区域。
它也通过页表映射到物理页框。
例如:
void test(void)
{
char buffer[4096];
buffer[0] = 1;
}
如果编译器实际为 buffer 分配栈存储,那么访问相应栈页面时,可能触发物理页的按需分配。
栈增长也受到:
- VMA 规则。
- 栈大小限制。
- Guard Page。
- 操作系统资源限制。
等因素约束。
不能理解为栈可以无限增长。
四十四、Text 为什么可以共享物理页
假设运行同一个程序两次:
./test &
./test &
两个进程拥有独立虚拟地址空间。
但是它们的只读可执行文件页面可能映射到同一份物理页缓存页面。
Process A
Virtual Text Page
|
|
+------+
|
v
Physical Code Frame
^
+------+
|
|
Process B
Virtual Text Page
好处:
- 节约物理 RAM。
- 避免重复加载相同代码。
- 保持进程虚拟地址空间独立。
是否实际共享取决于映射对象、页面状态及系统机制。
第九部分:写时复制 COW
四十五、为什么 fork 不立即复制所有物理内存
Linux 中:
fork();
可以创建子进程。
如果父进程拥有大量内存,fork 时立即复制所有物理页面会非常昂贵。
因此 Linux 通常使用:
Copy-On-Write(COW)。
写时复制。
四十六、COW 工作过程
fork 之后:
Parent Process
Virtual Page A
|
|
+--------+
|
v
Physical Frame X
^
+--------+
|
|
Child Process
Virtual Page A
父子进程暂时共享某些物理页面。
相关映射受到写保护。
当子进程修改数据时:
Child 写入页面
|
v
触发写保护异常
|
v
Linux 判断需要 COW
|
v
分配新的物理页框
|
v
复制原页面内容
|
v
修改子进程页表
|
v
子进程写入新页面
结果:
Parent Virtual Page
|
v
Physical Frame X
Child Virtual Page
|
v
Physical Frame Y
注意:实际实现中,如果页面已经不存在其他需要保留的共享引用,内核可能通过优化避免不必要的数据复制。
四十七、COW 与普通共享内存的区别
COW:
- 初始可能共享物理页面。
- 写入时建立私有副本。
- 修改通常不会反映到另一进程。
共享内存:
- 多个进程映射同一个共享对象。
- 写入通常可被其他进程观察。
- 需要考虑并发同步。
两者虽然都涉及多个虚拟地址映射到同一个物理页框,但语义不同。
第十部分:Swap 与物理内存回收
四十八、Swap 是什么
Swap 是 Linux 用于管理部分可换出内存页面的后备存储机制。
当系统存在内存压力时,内核可能将符合条件的匿名内存页面换出到 Swap。
物理 RAM
|
| 页面换出
v
Swap
|
| 页面换入
v
物理 RAM
Swap 可以是:
- Swap Partition。
- Swap File。
- 其他内核支持的交换后端。
并非所有内存页面都能直接交换。
例如内核中的某些页面、锁定页面和特殊设备映射具有不同管理规则。
四十九、Swap 与虚拟地址的关系
假设某个虚拟页面原本映射到物理页框。
Virtual Page A
|
v
Physical Frame 10
系统将该页面换出后:
Virtual Page A
|
v
Swap 中的页面数据
此时原来的物理页框可以被回收用于其他用途。
再次访问时:
CPU 访问 Virtual Page A
|
v
缺页异常
|
v
Linux 定位 Swap 数据
|
v
读入物理页面
|
v
恢复页表映射
|
v
继续执行
Swap 并不是把整个进程虚拟地址空间复制到磁盘。
它主要围绕可交换页面进行管理。
五十、文件页与匿名页
Linux 中常见两类页面:
File-backed Page
有文件作为后备对象。
例如:
- ELF 代码页面。
- 共享库页面。
- mmap 文件映射。
干净的文件页在需要时通常可以直接丢弃,之后再从文件重新加载。
脏文件页在回收前可能需要写回。
Anonymous Page
没有普通文件作为直接后备对象。
例如:
- Heap 中的匿名内存。
- Stack 中的匿名内存。
- 匿名 mmap。
匿名页面在需要保留内容并回收物理内存时,可能依赖 Swap 等机制。
五十一、Page Cache
Page Cache 是 Linux 用于缓存文件内容的重要机制。
磁盘文件
|
v
Page Cache
|
v
进程读取 / 文件映射
例如多个进程映射同一个共享库:
Process A
|
v
共享库文件页缓存
^
|
Process B
Page Cache 可以减少重复磁盘读取。
需要区分:
- Page Cache 是 Linux 管理的文件页缓存。
- CPU Cache 是处理器硬件缓存。
- TLB 是地址转换缓存。
它们不是同一个东西。
第十一部分:Linux 内核如何管理物理内存
五十二、物理内存不是由用户程序直接管理
用户程序通常通过:
malloc()
mmap()
申请或映射虚拟内存。
真正的物理页框由 Linux 内核管理。
概念上:
用户程序
|
v
malloc / mmap
|
v
虚拟内存管理
|
v
缺页处理 / 页面分配
|
v
Linux 物理内存分配器
|
v
Physical Page Frame
五十三、Buddy Allocator
Linux 使用伙伴分配器管理空闲物理页块。
其基本思想是按照 2 的幂次管理连续页块。
例如:
Order 0:1 Page
Order 1:2 Pages
Order 2:4 Pages
Order 3:8 Pages
...
当需要较小页块时,可以拆分较大页块。
当相邻伙伴页块均空闲时,可以尝试合并。
8 Pages
|
v
4 + 4
|
v
2 + 2 + 4
|
v
1 + 1 + 2 + 4
伙伴分配器主要解决物理页块分配问题。
五十四、Slab / SLUB
内核经常需要分配小对象,例如:
- 内核数据结构。
- 文件系统对象。
- 进程相关结构。
- 网络对象。
如果每个小对象都直接分配完整物理页,会产生浪费。
因此 Linux 还使用对象分配器,例如 SLUB。
概念上:
Buddy Allocator
|
v
分配物理页面
|
v
SLUB
|
v
管理小型内核对象
Buddy 与 SLUB 解决的问题不同:
- Buddy:管理物理页块。
- SLUB:管理内核对象分配。
用户态 malloc 则属于另一层的内存分配机制。
五十五、NUMA
在多 CPU 插槽或复杂服务器中,物理内存可能采用 NUMA 架构。
NUMA:
Non-Uniform Memory Access。
不同 CPU 节点访问不同内存节点的延迟可能不同。
CPU Node 0 ---> Local Memory 0
|
+-------> Remote Memory 1
CPU Node 1 ---> Local Memory 1
|
+-------> Remote Memory 0
Linux 会根据 NUMA 策略尝试优化物理页分配。
因此即使虚拟地址空间完全相同,实际物理页面放置位置也可能影响程序性能。
第十二部分:大页 Huge Page
五十六、为什么需要大页
普通 4 KiB 页面在管理大容量内存时会产生大量页面和页表项。
例如:
1 GiB / 4 KiB = 262144 Pages
如果采用 2 MiB 大页:
1 GiB / 2 MiB = 512 Pages
页面数量明显减少。
大页的好处可能包括:
- 减少页表开销。
- 提高 TLB 覆盖范围。
- 降低部分地址转换成本。
但大页也可能带来内存浪费、分配困难和管理成本。
五十七、常见页面大小
以常见 x86-64 配置为例:
| 页面类型 | 大小 |
|---|---|
| 普通页 | 4 KiB |
| 大页 | 2 MiB |
| 超大页 | 1 GiB |
Linux 还支持 Transparent Huge Pages(THP)等机制。
ARM64 可以根据硬件和配置采用不同的基础页面粒度及大块映射。
因此不要认为所有 Linux 系统都固定采用 4 KiB 页面。
第十三部分:VMA 与页表的区别
五十八、VMA 是什么
VMA:
Virtual Memory Area。
它描述进程虚拟地址空间中的一段连续区域及其属性。
例如:
VMA 1:代码区域
VMA 2:数据区域
VMA 3:Heap
VMA 4:共享库
VMA 5:Stack
VMA 可以描述:
- 虚拟地址起止范围。
- 访问权限。
- 文件映射关系。
- 匿名映射属性。
- 共享或私有映射属性。
五十九、VMA 不等于页表
两者最重要的区别:
| 对比 | VMA | Page Table |
|---|---|---|
| 管理层面 | 虚拟地址区域 | 页面级地址转换 |
| 主要描述 | 范围和属性 | 虚拟页到物理页框的映射 |
| 主要维护者 | Linux 内核 | Linux 内核建立,MMU 使用 |
| 是否必须立即拥有物理页 | 否 | 有效叶子映射通常指向物理页框 |
| 粒度 | 一段地址范围 | 页或大页 |
例如:
VMA:0x100000 ~ 0x10FFFF
|
v
16 个 4 KiB 虚拟页
|
v
各自的页表映射状态
|
v
部分已映射物理页
部分尚未分配物理页
因此:
VMA 说明某个地址范围应该如何使用;页表描述当前硬件如何转换其中的地址。
第十四部分:从程序启动到物理页映射的完整流程
六十、ELF 加载到执行的完整过程
C/C++ Source
|
v
Compiler / Assembler / Linker
|
v
ELF Executable
|
v
execve()
|
v
Linux 加载 ELF
|
v
读取 Program Header
|
v
建立进程虚拟地址空间
|
+-- Text VMA
+-- Data VMA
+-- BSS 映射
+-- Heap
+-- mmap
+-- Stack
|
v
准备页表和地址转换上下文
|
v
设置入口地址和初始栈
|
v
CPU 开始执行
|
v
访问虚拟地址
|
v
TLB 查询
|
+-- 命中
| |
| v
| 地址转换
|
+-- 未命中
|
v
页表遍历
|
+-- 有效映射
| |
| v
| 得到物理地址
|
+-- 缺页/权限异常
|
v
Linux 内核
|
v
处理缺页或错误
|
v
更新映射
|
v
重新执行
需要注意:execve 会替换当前进程的程序映像,并不必然创建一个新的 PID。通常 shell 先 fork,再由子进程 execve 执行目标程序。
六十一、一个变量从 C 代码到物理 RAM
例如:
#include <stdio.h>
#include <stdlib.h>
int global_a = 100;
int global_b;
int main(void)
{
int local_a = 10;
int *p = malloc(sizeof(int));
if (p == NULL)
return 1;
*p = 20;
printf("%d %d %d\n", global_a, local_a, *p);
free(p);
return 0;
}
首先从程序内存区域理解:
global_a
|
v
.data
global_b
|
v
.bss
local_a
|
v
Stack / Register
p 指针变量
|
v
Stack / Register
p 指向的动态对象
|
v
Heap / mmap
再从物理映射角度理解:
变量对应的虚拟地址
|
v
确定所属虚拟页面
|
v
MMU / TLB / Page Table
|
v
得到物理页框
|
v
通过页内偏移访问对象
注意:编译器可能将部分变量优化到寄存器中,因此并不是每个 C 变量都必然具有实际内存地址。
第十五部分:如何在 Linux 中验证
六十二、查看 ELF 结构
查看 ELF Header:
readelf -h ./test
查看 Section:
readelf -S ./test
查看 Program Header:
readelf -l ./test
重点观察:
.text
.rodata
.data
.bss
PT_LOAD
六十三、查看进程虚拟地址布局
查看当前 Shell:
cat /proc/$$/maps
查看指定进程:
cat /proc/<PID>/maps
可能看到:
[heap]
[stack]
[vdso]
[vvar]
libc.so
ld-linux.so
maps 主要展示虚拟内存映射区域。
它不能直接告诉普通用户每个虚拟页面实际对应哪个物理页框。
六十四、查看进程内存统计
cat /proc/<PID>/status
重点观察:
- VmSize:虚拟内存大小。
- VmRSS:常驻物理内存统计。
- VmData:数据区域相关虚拟内存统计。
- VmStk:栈相关虚拟内存统计。
- VmSwap:交换空间相关统计。
还可以:
cat /proc/<PID>/smaps
查看更详细的映射内存信息。
例如:
- Size
- Rss
- Pss
- Shared_Clean
- Private_Dirty
- Anonymous
- Swap
其中:
RSS 是进程常驻内存统计,并不是进程独占的物理内存总量。
PSS 则按比例分摊共享页面,更适合某些场景下估算进程内存占用。
六十五、查看系统物理内存
free -h
或者:
cat /proc/meminfo
重点观察:
MemTotal
MemFree
MemAvailable
Cached
SwapTotal
SwapFree
MemFree 不等于系统真正可供应用使用的全部内存。
Linux 会利用空闲 RAM 缓存文件等数据,因此 MemAvailable 往往更适合粗略判断可用内存情况。
六十六、查看页面大小
getconf PAGESIZE
可能输出:
4096
表示当前系统普通页面大小为 4096 Bytes。
六十七、查看 Page Fault 统计
/usr/bin/time -v ./test
也可以:
perf stat -e page-faults,minor-faults,major-faults ./test
perf 是否可用取决于安装情况和系统权限。
六十八、查看虚拟地址与物理页映射
Linux 提供:
/proc/<PID>/pagemap
它可以用于查询进程虚拟页面的部分映射状态。
但是:
- 读取需要按照内核接口解析二进制条目。
- PFN 信息受到权限限制。
- 现代 Linux 通常不允许普通用户随意获取物理页框号。
- 不同内核版本可能存在接口差异。
因此不应将 pagemap 简单理解成普通文本页表。
查看内核相关文档:
Documentation/admin-guide/mm/pagemap.rst
六十九、ARM64 如何理解页表
ARM64 与 x86-64 的核心目标相同:
Virtual Address
|
v
Translation Tables
|
v
Physical Address
但 ARM64 有自己的机制,例如:
- TTBR0_EL1
- TTBR1_EL1
- TCR_EL1
- Translation Table Descriptor
- ASID
- TLB
- Translation Fault
- Permission Fault
典型情况下:
用户态虚拟地址
|
v
TTBR0_EL1 所配置的转换表
|
v
物理地址
内核高地址映射通常由 TTBR1_EL1 所配置的转换表管理。
具体地址划分及页表级数由系统配置决定。
ARM64 可以使用 4 KiB、16 KiB、64 KiB 等不同基础页面粒度,不能机械套用 x86-64 的四级、每级 9 位模型。
第十六部分:常见错误认识
七十、错误一:虚拟内存就是 Swap
错误。
虚拟内存是一整套地址空间抽象和映射机制。
Swap 只是部分页面的后备存储与换入换出机制。
没有 Swap,Linux 依然可以使用虚拟内存和分页。
七十一、错误二:malloc 100 MiB 就立即占用 100 MiB RAM
不一定。
malloc 可能只管理或获取相应虚拟地址范围。
物理页面可能按需分配,也可能复用已有页面。
七十二、错误三:Heap 是物理内存中的固定区域
错误。
Heap 是进程虚拟地址空间中的逻辑区域。
它的页面可以映射到分散的物理页框。
七十三、错误四:两个进程不能共享物理内存
错误。
两个进程可以共享:
- 只读代码页面。
- 共享库页面。
- 共享文件映射。
- 显式共享内存。
- COW 尚未分离的页面。
进程隔离并不意味着完全不能共享物理页。
七十四、错误五:TLB Miss 就是缺页异常
错误。
TLB Miss 表示地址转换缓存未命中。
Page Fault 表示当前访问无法直接通过正常页表转换或权限检查完成。
七十五、错误六:Page Fault 一定导致程序崩溃
错误。
很多 Page Fault 是正常的按需分页、COW 或换入过程。
只有无法合法处理的异常才可能导致 SIGSEGV、SIGBUS 等结果。
七十六、错误七:虚拟地址连续意味着物理地址连续
错误。
分页机制允许连续虚拟页映射到分散物理页框。
七十七、错误八:所有页面都固定为 4 KiB
错误。
页面大小取决于架构、配置及映射类型。
七十八、错误九:每个变量一定存储在 RAM 中
错误。
编译器可能将变量:
- 放在寄存器中。
- 优化成常量。
- 完全消除。
即使存在内存对象,也不意味着其页面此刻一定常驻物理 RAM。
七十九、错误十:用户程序可以随意访问物理地址
错误。
普通用户程序主要通过虚拟地址访问内存。
硬件与内核共同实施地址转换和权限保护。
第十七部分:最终完整知识模型
八十、第一层:程序文件
源代码
|
v
ELF 文件
|
+-- .text
+-- .rodata
+-- .data
+-- .bss 描述
+-- Program Header
这一层主要解决:
程序在磁盘上如何组织。
八十一、第二层:进程虚拟地址空间
Process Virtual Address Space
|
+-- Text
+-- rodata
+-- Data
+-- BSS
+-- Heap
+-- mmap
+-- Stack
这一层主要解决:
程序运行时如何组织代码、数据和内存对象。
八十二、第三层:虚拟页面
Virtual Address Space
|
v
Virtual Pages
|
+-- VPN
+-- Offset
这一层主要解决:
如何把虚拟地址空间划分为可管理的页面。
八十三、第四层:页表与 MMU
Virtual Page
|
v
Page Table
|
v
MMU / TLB
|
v
Physical Frame
这一层主要解决:
虚拟地址如何转换为物理地址,以及访问权限如何受到控制。
八十四、第五层:物理内存管理
Linux Kernel
|
+-- Buddy Allocator
+-- SLUB
+-- Page Reclaim
+-- Page Cache
+-- Swap
+-- NUMA
|
v
Physical RAM
这一层主要解决:
真实物理页面如何分配、复用、回收和管理。
八十五、完整总图
C / C++ 程序
|
v
ELF 可执行文件
|
v
execve()
|
v
Linux 创建程序映像
|
v
进程虚拟地址空间
|
+----------------+----------------+
| | |
v v v
Text/Data Heap/mmap Stack
| | |
+----------------+----------------+
|
v
Virtual Pages
|
v
Virtual Address
|
v
MMU / TLB
|
v
Page Tables
|
+------------+------------+
| |
v v
映射有效 缺页/权限异常
| |
| v
| Linux Kernel
| |
| +-------+-------+
| | |
| v v
| 分配物理页 文件/Swap/COW
| | |
| +-------+-------+
| |
| v
| 更新页表
| |
+------------+------------+
|
v
Physical Address
|
v
Cache / Memory
|
v
CPU 使用数据
这张图需要理解成逻辑关系图,而不是严格的硬件时序图:TLB 查询与页表遍历是地址转换的不同路径,Linux 内核通常只在需要软件介入时处理异常。
第十八部分:最终总结
八十六、必须掌握的核心概念
| 概念 | 核心含义 |
|---|---|
| ELF | 程序的可执行文件格式 |
| Section | ELF 内部逻辑内容 |
| Segment | ELF 加载映射单元 |
| VMA | 虚拟地址范围及其属性 |
| Virtual Address | 程序使用的虚拟地址 |
| Physical Address | 物理地址空间中的地址 |
| Page | 虚拟页面 |
| Page Frame | 物理页框 |
| VPN | 虚拟页号 |
| PFN | 物理页框号 |
| Page Table | 虚拟页到物理页框的映射结构 |
| PTE | 页表项 |
| MMU | 硬件地址转换单元 |
| TLB | 地址转换缓存 |
| Page Fault | 地址转换或权限异常 |
| Demand Paging | 按需建立页面映射 |
| COW | 写时复制 |
| Swap | 可交换页面的后备存储机制 |
| Page Cache | 文件页面缓存 |
| Buddy | 物理页块分配器 |
| SLUB | 内核对象分配器 |
| Huge Page | 大页面映射 |
| ASLR | 地址空间布局随机化 |
八十七、最终需要记住的十句话
- ELF 文件是磁盘上的程序组织形式,进程是程序运行时的实体。
- Text、Data、BSS、Heap、mmap、Stack 主要描述进程虚拟地址空间中的不同区域。
- 每个进程通常拥有独立的用户态虚拟地址空间。
- 虚拟内存不是物理 RAM,也不等于 Swap。
- 虚拟地址通过页表和 MMU 转换为物理地址。
- 页表映射以页面为单位,虚拟页与物理页框不必连续。
- TLB 缓存地址转换结果,提高访问效率。
- Page Fault 可能是正常的按需分页过程,并不一定是程序错误。
- Linux 可以通过 COW、共享页面、Page Cache、Swap 等机制提高内存利用率。
- 用户程序管理的是虚拟地址中的对象,Linux 内核负责协调实际物理页面的分配、映射和回收。
八十八、一句话建立完整认识
程序编译成 ELF 后,Linux 根据加载信息建立进程虚拟地址空间;代码、数据、堆、栈和映射区域通过虚拟页面组织;CPU 使用虚拟地址访问内存,MMU 借助 TLB 和页表完成地址转换;当页面尚未建立有效映射时,Linux 可以通过缺页异常按需分配、加载或恢复物理页面;最终由内核管理真实物理 RAM,实现进程隔离、内存共享和高效内存利用。
参考资料
- 原始学习文章:操作系统原理(四):内存
- Linux Kernel Documentation:Memory Management
- Linux Kernel Documentation:Page Tables
- Linux Kernel Documentation:pagemap
- Linux man-pages:mmap(2)
- Linux man-pages:proc_pid_maps(5)
浙公网安备 33010602011771号