第五十九章:内存宫殿
元婴初期涉及内核源码:
一
林小源在内景中走了很久,渐渐注意到一个规律。
每一块悬浮大陆都有自己的"形状"——有的方正,有的狭长,有的紧挨着另一块,有的孤零零地飘在远处。他之前以为这些形状是随机的,但走近了才发现,每一块大陆的边缘都刻着精确的数字—— 和 。
"你在数什么?"mm_struct 的声音从背后传来。
"我在看这些大陆的大小。"林小源指着最近的一块,"这块从 0x00400000 到 0x00401000,正好 4KB。但旁边那块从 0x00800000 到 0x00810000,有 64KB。"
"它们不只是大小不同。"mm_struct 说,"它们是不同的 VMA。"
林小源已经听过这个词了。但这次 mm_struct 的语气不一样——不是随口一提,而是要正式介绍。
"VMA,Virtual Memory Area。"mm_struct 说,"每一块大陆,就是一个 VMA。它描述的不是物理内存——那是页表的事。它描述的是虚拟地址的布局:这段地址是什么用途、有什么权限、关联了什么文件。"
林小源蹲下来,仔细看脚边那块大陆的表面。上面不只有地址,还有几行发光的标志——、,但没有 。
"代码段。"mm_struct 说,"只读,可执行。你的指令就住在这里。"
他走向下一块大陆。标志不同——、,没有 。
"数据段。可读写,不可执行。"
再下一块——、、。
"栈。"
林小源回头看了一眼来时的路。所有的大陆排列得整整齐齐,从低地址到高地址,像一座被精确规划过的城市。
"每个进程都有这样一座宫殿。"mm_struct 说,"VMA 就是宫殿里的房间。每个房间有不同的用途、不同的规矩。你不能在代码段里写数据,不能在数据段里执行指令,不能——"
"不能去没有房间的地方。"林小源接话。
mm_struct 没有回应,但林小源感觉到它满意了。
二
林小源在宫殿中走了一阵,发现有些大陆上刻着额外的信息——一个文件名,一个偏移量。
"那是什么?"
"文件映射。"mm_struct 说,"有些 VMA 不只是地址范围——它们关联了一个文件。 指向那个文件, 记录文件内的偏移。当你访问这段虚拟地址时,内核会从文件中读取对应的页面。"
林小源摸了摸那块大陆的表面。确实——地底下的页表冰层里,对应位置的 PTE 指向的不是普通物理页,而是文件缓存。
"那没有关联文件的 VMA 呢?"
"匿名映射。"mm_struct 说,"堆、栈、mmap(MAP_ANONYMOUS)——这些 VMA 没有关联文件。它们的物理页是内核按需分配的。"
林小源站起来,环顾四周。他现在能分辨了——有文件名的大陆是文件映射,没有文件名的是匿名映射。整座宫殿因此分成了两个区域,像城市的两个街区。
"页 fault 的时候,"林小源忽然想到,"内核怎么知道该去哪找 VMA?"
mm_struct 的声音带了一丝赞许:"好问题。VMA 存在红黑树里。当页 fault 发生时,内核拿 fault address 去红黑树里搜索——O(log n) 的时间复杂度。如果找到了,就知道这个地址合法,可以用 VMA 的信息来处理 fault。如果找不到——"
"SIGSEGV。"
"对。VMA 是页 fault 的指南针。没有它,内核连一个地址该不该访问都判断不了。"
三
林小源在宫殿的最高处俯瞰,注意到了一个有趣的细节。
VMA 不只是用红黑树存储——它们还串成了一条链表。红黑树用于快速查找,链表用于顺序遍历。
"为什么要两种结构?"他问。
"因为有两种需求。"mm_struct 说,"页 fault 需要快速查找一个地址属于哪个 VMA——用红黑树。/proc/PID/maps 需要按地址顺序列出所有 VMA——用链表。两种需求,两种数据结构,各司其职。"
林小源看着脚下的宫殿。红黑树的结构隐藏在大陆深处,像地下的根系;链表则像地面上的街道,把所有大陆串在一起。
"每个 VMA 对应 maps 文件里的一行。"mm_struct 继续说,"地址范围、权限、偏移、设备号、inode、文件名——所有信息都在 VMA 里。你想知道一个进程的内存布局?看 maps。想知道 maps 里的信息从哪来?看 VMA。"
林小源深吸一口气。他现在看到的不再是一堆零散的悬浮大陆——而是一座有结构、有秩序、有管理规则的宫殿。每一块大陆都是一个房间,每个房间都有名字和规矩,所有房间通过红黑树和链表连接在一起。
"他忽然明白过来,轻声说——进程的地址空间不是一片荒地,是一座宫殿。"
"宫殿是会变的。"mm_struct 的声音低沉下来," 会新建房间, 会拆掉房间, 会改变房间的规矩。宫殿每天都在变化——但只要红黑树和链表还在,内核就永远知道每个地址的归属。"
/*
* vm_area_struct 描述一个虚拟内存区域:
*
* 关键字段:
* vm_start — 区域起始地址
* vm_end — 区域结束地址
* vm_flags — 标志位(VM_READ, VM_WRITE, VM_EXEC, VM_SHARED)
* vm_pgoff — 文件映射的偏移(页数)
* vm_file — 映射的文件(如果有)
* vm_ops — 操作函数表
* vm_next — 下一个 VMA(链表)
*
* VMA 的类型:
* 匿名映射 — 没有关联文件(堆、栈、mmap 匿名)
* 文件映射 — 关联一个文件(mmap 文件映射)
*
* VMA 的标志位:
* VM_READ — 可读
* VM_WRITE — 可写
* VM_EXEC — 可执行
* VM_SHARED — 共享映射
* VM_STACK — 栈区域
*/
#define VM_READ 0x00000001
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
#define VM_STACK 0x00000010
struct vm_area_struct {
unsigned long vm_start;
unsigned long vm_end;
unsigned long vm_flags;
unsigned long vm_pgoff;
const char *name;
struct vm_area_struct *vm_next;
};
const char *flags_to_str(unsigned long flags) {
static char buf[32];
buf[0] = 0;
if (flags & VM_READ) strcat(buf, "R");
if (flags & VM_WRITE) strcat(buf, "W");
if (flags & VM_EXEC) strcat(buf, "X");
if (flags & VM_SHARED) strcat(buf, "S");
if (flags & VM_STACK) strcat(buf, "T");
return buf;
}
printf("=== VMA — 虚拟内存区域 ===\n\n");
/* 模拟一个进程的 VMA 列表 */
struct vm_area_struct vmas[] = {
{ 0x00400000, 0x00401000, VM_READ | VM_EXEC, 0, "code", NULL },
{ 0x00600000, 0x00601000, VM_READ | VM_WRITE, 0, "data", NULL },
{ 0x00800000, 0x00810000, VM_READ | VM_WRITE, 0, "heap", NULL },
{ 0x7F000000, 0x7F001000, VM_READ | VM_WRITE, 0, "mmap-anon", NULL },
{ 0x7FFF0000, 0x80000000, VM_READ | VM_WRITE | VM_STACK, 0, "stack", NULL },
};
int nr = sizeof(vmas) / sizeof(vmas[0]);
/* 链接 VMA */
for (int i = 0; i < nr - 1; i++)
vmas[i].vm_next = &vmas[i + 1];
printf("进程的 VMA 列表:\n");
printf("%-18s %-18s %-8s %s\n", "vm_start", "vm_end", "flags", "name");
printf("%-18s %-18s %-8s %s\n", "---", "---", "---", "---");
struct vm_area_struct *vma = &vmas[0];
while (vma) {
printf("0x%016lx 0x%016lx %-8s %s\n",
vma->vm_start, vma->vm_end,
flags_to_str(vma->vm_flags), vma->name);
vma = vma->vm_next;
}
printf("\n--- VMA 的作用 ---\n");
printf("1. 描述进程地址空间的布局\n");
printf("2. 记录每个区域的权限\n");
printf("3. 关联文件映射(如果有)\n");
printf("4. 提供页 fault 处理的上下文\n");
printf("5. 支持 /proc/PID/maps 输出\n");
printf("\n--- /proc/PID/maps ---\n");
printf("每个 VMA 对应 maps 文件中的一行:\n");
printf("地址范围 权限 偏移 设备 inode 文件名\n");
printf("00400000-00401000 r-xp 00000000 08:01 12345 /bin/ls\n");#include <stdio.h>
#include <string.h>
/*
* vm_area_struct 描述一个虚拟内存区域:
*
* 关键字段:
* vm_start — 区域起始地址
* vm_end — 区域结束地址
* vm_flags — 标志位(VM_READ, VM_WRITE, VM_EXEC, VM_SHARED)
* vm_pgoff — 文件映射的偏移(页数)
* vm_file — 映射的文件(如果有)
* vm_ops — 操作函数表
* vm_next — 下一个 VMA(链表)
*
* VMA 的类型:
* 匿名映射 — 没有关联文件(堆、栈、mmap 匿名)
* 文件映射 — 关联一个文件(mmap 文件映射)
*
* VMA 的标志位:
* VM_READ — 可读
* VM_WRITE — 可写
* VM_EXEC — 可执行
* VM_SHARED — 共享映射
* VM_STACK — 栈区域
*/
#define VM_READ 0x00000001
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
#define VM_STACK 0x00000010
struct vm_area_struct {
unsigned long vm_start;
unsigned long vm_end;
unsigned long vm_flags;
unsigned long vm_pgoff;
const char *name;
struct vm_area_struct *vm_next;
};
const char *flags_to_str(unsigned long flags) {
static char buf[32];
buf[0] = 0;
if (flags & VM_READ) strcat(buf, "R");
if (flags & VM_WRITE) strcat(buf, "W");
if (flags & VM_EXEC) strcat(buf, "X");
if (flags & VM_SHARED) strcat(buf, "S");
if (flags & VM_STACK) strcat(buf, "T");
return buf;
}
int main() {
printf("=== VMA — 虚拟内存区域 ===\n\n");
/* 模拟一个进程的 VMA 列表 */
struct vm_area_struct vmas[] = {
{ 0x00400000, 0x00401000, VM_READ | VM_EXEC, 0, "code", NULL },
{ 0x00600000, 0x00601000, VM_READ | VM_WRITE, 0, "data", NULL },
{ 0x00800000, 0x00810000, VM_READ | VM_WRITE, 0, "heap", NULL },
{ 0x7F000000, 0x7F001000, VM_READ | VM_WRITE, 0, "mmap-anon", NULL },
{ 0x7FFF0000, 0x80000000, VM_READ | VM_WRITE | VM_STACK, 0, "stack", NULL },
};
int nr = sizeof(vmas) / sizeof(vmas[0]);
/* 链接 VMA */
for (int i = 0; i < nr - 1; i++)
vmas[i].vm_next = &vmas[i + 1];
printf("进程的 VMA 列表:\n");
printf("%-18s %-18s %-8s %s\n", "vm_start", "vm_end", "flags", "name");
printf("%-18s %-18s %-8s %s\n", "---", "---", "---", "---");
struct vm_area_struct *vma = &vmas[0];
while (vma) {
printf("0x%016lx 0x%016lx %-8s %s\n",
vma->vm_start, vma->vm_end,
flags_to_str(vma->vm_flags), vma->name);
vma = vma->vm_next;
}
printf("\n--- VMA 的作用 ---\n");
printf("1. 描述进程地址空间的布局\n");
printf("2. 记录每个区域的权限\n");
printf("3. 关联文件映射(如果有)\n");
printf("4. 提供页 fault 处理的上下文\n");
printf("5. 支持 /proc/PID/maps 输出\n");
printf("\n--- /proc/PID/maps ---\n");
printf("每个 VMA 对应 maps 文件中的一行:\n");
printf("地址范围 权限 偏移 设备 inode 文件名\n");
printf("00400000-00401000 r-xp 00000000 08:01 12345 /bin/ls\n");
return 0;
}道藏笔记
内核启示
VMA 描述的是进程地址空间中一段连续的虚拟区域——它不管物理内存,只管虚拟地址的布局。每个 VMA 记录着起止地址(/)、权限标志(///),如果有文件映射的话还记着偏移量和文件指针。
VMA 分两种:匿名映射没有关联文件(堆、栈、匿名 mmap),文件映射则绑定了一个文件。内核用红黑树存 VMA,O(log n) 查找——页 fault 时靠它来判断地址是否合法。同时 VMA 还串成一条链表,方便按地址顺序遍历,/proc/PID/maps 就是从这条链表读出来的。
页 fault 时内核先查 VMA 确认地址合法,再查页表做翻译。VMA 是进程地址空间的"蓝图"——页表是"施工"。
内存宫殿之试
本章把进程地址空间划成一片片宫殿,每一段虚拟内存区域叫什么?