深入理解 Linux 进程地址空间:从虚拟内存、页表映射到 fork 写时拷贝与 VMA 管理
深入理解 Linux 进程地址空间从虚拟内存、页表映射到 fork 写时拷贝与 VMA 管理前言1. 进程地址空间先建立正确的内存模型1.1 一个进程拥有一套虚拟地址视图1.2 经典地址空间布局与每个区域的职责1.3 为什么要在物理内存前增加虚拟地址层2. 一次变量访问究竟经历了什么2.1 从虚拟地址到物理页的完整路径2.2 按需调页、文件后备页与匿名页3. fork 为什么快相同地址、共享页面与写时拷贝3.1 父子进程为什么能打印相同地址却得到不同值3.2 独立与共享的边界以及“进程挂起”时内存去哪了4. 内核如何组织地址空间task_struct、mm_struct 与 VMA4.1 三层结构各自负责什么4.2 为什么按“区间”管理以及 VMA 如何变化5. 从 exec 到退出代码和数据如何真正进入内存5.1 先建立映射再在访问时装入页面5.2 malloc、free、退出与物理内存回收6. 实战观察用 Linux 工具验证地址空间6.1 编写一个可观察的地址布局程序6.2 查看 maps、ELF 段与物理占用总结前言在 C/C 程序里打印一个指针我们会看到类似0x7ffd...的地址查看一个 32 位进程的内存布局又经常能看到“用户空间 3GiB、内核空间 1GiB”的示意图。最容易产生的误解是这个地址就是内存条上的物理位置4GiB 地址空间也意味着进程已经占用了 4GiB 物理内存。事实恰好相反。用户程序通常看到的是一套由操作系统和硬件共同维护的虚拟地址空间。CPU 发出的虚拟地址要经过 MMU、TLB 和页表翻译才能落到物理内存中的某个页框如果目标页尚未装入访问还会触发缺页异常由内核补齐映射后再重试。也正因为中间多了这一层抽象操作系统才能在有限的物理内存上实现进程隔离、权限保护、按需调页、共享内存以及fork的写时拷贝。这套机制看似概念多主线其实很清楚进程看见什么从低地址到高地址的一片虚拟地址空间。内核如何描述task_struct关联mm_struct再由多个 VMA 描述不同虚拟区间。硬件如何访问页表把虚拟页映射到物理页TLB 缓存近期翻译结果。数据何时进入内存创建映射不等于立刻分配物理页许多页面在首次访问时才装入。进程地址空间不是一整块真实物理内存而是进程可使用的虚拟地址范围以及这段范围对应的映射规则、访问权限和后备存储信息。1. 进程地址空间先建立正确的内存模型1.1 一个进程拥有一套虚拟地址视图现代操作系统通常按字节寻址。对 32 位程序而言一个指针最多能表达2^32种地址因此理论虚拟地址范围是0x00000000到0xffffffff总计2^32字节也就是 4GiB。这个结论只描述地址编号的数量不代表进程启动时真的分配了 4GiB 物理内存。进程执行时CPU 产生的是虚拟地址。内核为进程维护页表页表记录“某个虚拟页应该映射到哪个物理页以及允许怎样访问”。因此两个进程都可以访问数值相同的虚拟地址0x111111但它们的页表完全可以把该地址翻译到不同物理页。对进程来说这就像每个人都拿着一份从 0 开始编号的私人地图地图上的门牌号可以相同实际对应的房间却不同。所谓进程的独立性至少包含两层含义内核管理数据独立每个进程有自己的进程描述信息例如 PID、调度状态、打开文件表引用和地址空间指针。虚拟地址映射独立普通进程通常拥有独立的mm_struct和页表修改自己的私有数据不会直接改掉另一个进程的数据。这里还要补一个重要边界同一进程中的多个线程共享地址空间它们通常共享同一个mm_struct所以线程之间可以直接读写同一全局变量、堆对象和映射区线程真正私有的主要是寄存器上下文、内核栈和用户栈等执行现场。进程之间也不是绝对不能共享mmap的共享映射、System V 共享内存等机制都能显式建立共同映射。对比维度虚拟地址物理地址谁直接使用程序指针、CPU 访存指令内存控制器和内核内存管理是否进程私有同一数值可在不同进程中重复出现物理页是全局资源由内核统一分配是否连续VMA 内看起来连续对应物理页可以离散分布是否一定有内存不一定可能尚未映射或尚未装入表示实际页框或设备地址如何建立关系通过页表、PTE 和 MMU 翻译作为页表映射的目标1.2 经典地址空间布局与每个区域的职责下面是经典 32 位 Linux 进程地址空间的教学模型。图中的 3GiB/1GiB 只是常见划分不能当成所有系统的固定比例64 位系统的用户地址空间更大而且受硬件有效地址位数、内核配置和 ASLR 影响实际地址也不会像图中一样整齐固定。从低地址到高地址各区域的典型用途如下。代码段text保存机器指令通常是文件后备映射常见权限为r-x。只读常量可能位于单独的rodata区域权限通常为r--。已初始化数据段data保存有明确非零初值的全局变量和静态变量例如int g_value 100;。初始内容来自可执行文件。BSS 段保存未显式初始化或初始化为零的全局变量、静态变量例如static int count;。可执行文件不需要为一大片零逐字节存储内容只需记录区域大小装载时按零初始化即可。堆heap用于动态分配。传统上brk推高堆顶小块malloc常从用户态分配器管理的堆块中取得较大分配或特定实现也会直接使用匿名mmap。堆在示意图中通常向高地址增长。共享库与 mmap 映射区容纳动态链接库、文件映射、匿名映射和线程栈等内容。它不是只能放“共享”数据私有的MAP_PRIVATE映射同样可以出现在这里。栈stack保存函数调用现场、局部变量和返回地址等内容经典示意图中向低地址扩展。每个线程有自己的用户栈但这些栈仍处在同一进程地址空间内。命令行参数与环境变量在程序启动时由内核布置到初始用户栈附近main(int argc, char *argv[], char *envp[])最终从这里取得参数。内核空间在经典 32 位布局中位于高地址。用户态不能随意访问带有内核权限的页进入系统调用或异常处理时CPU 会切换到内核态执行受控代码。堆向上、栈向下只是为了说明它们可以动态扩展两者之间不会因此天然形成一整块已分配内存。中间大片地址可以没有任何 VMA也可以被mmap放入新的映射。ASLR 还会随机化栈、堆、共享库和 PIE 可执行文件的基址提高利用固定地址攻击的难度。1.3 为什么要在物理内存前增加虚拟地址层如果程序直接操作全局物理地址系统会立刻遇到三个难题不同进程容易写坏彼此数据程序必须关心自己被装到内存条的什么位置物理内存碎片还会让大块连续空间越来越难找。虚拟内存把这些问题拆开处理。第一把物理内存变得有序。程序看到连续的虚拟区间内核却可以把它映射到离散物理页。只要每个虚拟页都有正确映射程序无需知道页面在内存条上的真实位置。第二实现访问控制和进程隔离。页表项不仅保存页框号还能标记用户/内核、只读/可写、可执行/不可执行等权限。地址翻译本身就顺便完成了权限检查。第三让进程管理与内存管理适度解耦。创建进程时可以先建立task_struct、mm_struct、VMA 和页表骨架不必立刻把全部代码、数据复制进物理内存需要页面时再按需加载。第四支持灵活共享。不同进程的页表可以故意指向同一物理页用于共享库、共享内存和文件页缓存也可以先共享写入时再拆分这正是写时拷贝。目标没有虚拟内存时的困难虚拟内存的解决方式连续地址必须寻找连续物理块连续虚拟页可映射到离散物理页进程隔离任意程序可能覆盖别人的数据每个进程使用独立页表和权限大程序运行程序必须整体装入内存只加载当前真正访问的页面共享数据需要显式搬运和同步副本多套页表映射同一物理页快速创建子进程必须复制整个地址空间fork配合 COW 延迟复制2. 一次变量访问究竟经历了什么2.1 从虚拟地址到物理页的完整路径假设程序读取全局变量g_value。编译器和链接器先让指令以某种方式引用它的虚拟地址CPU 执行访存指令时再把这个虚拟地址交给 MMU。常见页面大小为 4KiB此时虚拟地址的低 12 位是页内偏移其余高位构成虚拟页号。大页机制会使用不同的划分方式。访问流程可以拆成下面几步CPU 生成虚拟地址MMU 用虚拟页号查询 TLB。TLB 是地址翻译缓存命中时不必再次遍历多级页表。TLB 未命中时硬件或架构相关代码从当前进程的页表根开始查找 PTE。多级页表避免为整个巨大地址空间预先创建一张扁平大表。PTE 若有效且权限允许MMU 取出物理页框号与原虚拟地址中的页内偏移拼接得到最终物理地址。PTE 不存在、Present0或权限不满足时CPU 触发页故障异常控制权进入内核。内核先查 VMA判断该地址是否落在合法区间、读写执行方式是否允许。合法但页面尚未装入时内核分配匿名零页或从可执行文件、共享库、普通文件、交换区读取页面。内核更新页表并刷新相关 TLB 状态然后返回用户态重新执行刚才失败的那条指令。如果地址根本不合法或权限冲突无法修复内核通常向进程发送SIGSEGV。缺页异常不一定是错误。合法映射的页面第一次被访问时发生缺页是按需调页的正常组成部分只有无法由内核修复的非法地址或权限访问才会演变成用户看到的段错误。页表项通常还会保存或参与维护多种状态。具体位布局取决于 CPU 架构但核心语义相近。PTE 信息作用典型结果Present/Valid页面是否已有有效物理映射为 0 时可能触发按需调页Read/Write是否允许写入向只读页写入会触发保护异常User/Supervisor用户态能否访问用户程序不能直接访问内核页Executable/NX是否允许取指执行堆、栈通常不可执行Accessed页面近期是否被访问可辅助页面回收决策Dirty页面是否被写过文件页回写或换出时需要关注在进程切换时内核还需要让 CPU 使用下一个进程的页表根。现代处理器可能借助 PCID/ASID 一类标识减少 TLB 全量失效但概念上仍然是同一个虚拟地址因为当前页表不同可以得到不同翻译结果。2.2 按需调页、文件后备页与匿名页把可执行文件装载进程并不等于把整个文件一次性读进物理内存。Linux 通常先依据 ELF 程序头建立 VMA让代码段和已初始化数据段具有文件后备关系。CPU 第一次取某个代码页的指令时如果 PTE 尚未建立有效映射就发生缺页内核再通过页缓存取得对应文件页更新页表后继续执行。匿名内存没有普通文件作为后备来源典型例子是 BSS、堆和栈。程序申请 1GiB 匿名虚拟空间时系统往往只先保留地址范围和必要元数据。只有真正写到其中某个页面才需要为那个页面准备物理页。这就是为什么虚拟内存大小 VSZ 很大不等于常驻物理内存 RSS 同样大。缺页通常还能按是否需要真实存储 I/O 粗略区分缺页类型是否通常需要磁盘 I/O常见场景Minor fault次缺页不需要页已在页缓存、COW 复制、首次匿名页分配Major fault主缺页需要页面必须从可执行文件、映射文件或交换区读取Protection fault保护异常不一定写只读页、执行 NX 页COW 可被内核合法修复物理内存紧张时内核可以回收干净文件页因为以后还能从文件重新读取匿名脏页若要被回收通常需要交换空间或其他内存管理策略承接内容。于是虚拟地址、物理页与磁盘后备存储形成了分层关系上层程序只持有稳定的虚拟地址页面当前在 RAM、页缓存还是交换区由内核动态调度。3. fork 为什么快相同地址、共享页面与写时拷贝3.1 父子进程为什么能打印相同地址却得到不同值fork创建子进程时子进程会获得父进程当时的地址空间视图。如果真的把父进程每个物理页立刻复制一遍创建大型进程的成本会非常高而且许多子进程马上调用execve替换整个地址空间那些复制工作将完全浪费。Linux 因此采用Copy-on-Write写时拷贝。fork初期父子页表可以同时指向同一批物理页并把本来可写的私有映射暂时改成只读/COW 状态。只读访问继续共享任一进程第一次写入时CPU 触发写保护缺页内核确认这是合法的 COW 映射复制目标物理页再让写入方的 PTE 指向新页并恢复可写权限。下面的程序能直观看到“虚拟地址相同变量内容独立”#includestdio.h#includesys/types.h#includesys/wait.h#includeunistd.hintg_value100;intmain(void){printf(fork 前: pid%d, g_value%p, g_value%d\n,getpid(),(void*)g_value,g_value);fflush(stdout);pid_tidfork();if(id0){perror(fork);return1;}if(id0){g_value;printf(子进程: pid%d, g_value%p, g_value%d\n,getpid(),(void*)g_value,g_value);return0;}waitpid(id,NULL,0);printf(父进程: pid%d, g_value%p, g_value%d\n,getpid(),(void*)g_value,g_value);return0;}编译并运行mossVM-0-4-ubuntu:~/process-space$ gcc-O0-gcow_demo.c-ocow_demo mossVM-0-4-ubuntu:~/process-space$ ./cow_demo fork 前:pid3210,g_value0x55b2aaf3c048,g_value100子进程:pid3211,g_value0x55b2aaf3c048,g_value101父进程:pid3210,g_value0x55b2aaf3c048,g_value100gcc用于编译 C 程序-O0关闭优化便于观察源码行为-g写入调试信息-o cow_demo指定输出文件名。输出中的具体地址会受 PIE 和 ASLR 影响但父子进程打印出的地址数值通常相同。子进程写入后得到 101父进程仍为 100说明二者已经映射到不同物理页。fflush(stdout)也不是多余的如果标准输出缓冲区在fork前尚未刷新父子进程会各自继承一份用户态缓冲区随后可能把同一段缓冲内容输出两次。3.2 独立与共享的边界以及“进程挂起”时内存去哪了写时拷贝并不表示父子进程永远共享所有页面。不同映射的语义如下内存类型fork后初始状态一方写入后的结果私有匿名页通常 COW 共享写入方获得私有副本MAP_PRIVATE文件映射通常共享文件页并采用私有 COW修改只在进程私有视图中生效MAP_SHARED映射继续映射共同对象修改可被其他映射者观察到只读代码页可长期共享正常情况下不允许写同一进程的线程共享同一地址空间写入直接对其他线程可见“进程挂起”在日常表达中可能指几种不同状态。进程因等待事件进入可中断睡眠S、等待 I/O 进入不可中断睡眠D或者收到SIGSTOP进入停止状态T时通常只是暂时不在 CPU 上运行它的mm_struct、VMA 和虚拟地址仍然存在。物理页却不一定全部常驻干净文件页可被回收匿名页可能被换出稍后再次访问再通过缺页恢复。因此“没占用 CPU”与“没有地址空间”是两回事。即使进程看起来卡住VSZ 仍可很大RSS 会随页面回收变化。只有进程退出、被成功终止或者execve用新映像替换旧地址空间时旧的内存映射才会按引用关系释放。D状态进程还可能因为内核正在等待不可中断 I/O暂时无法立刻响应终止信号这也不是页表失效造成的。4. 内核如何组织地址空间task_struct、mm_struct 与 VMA4.1 三层结构各自负责什么如果为地址空间中的每个字节都创建一个管理对象元数据会比真正的数据还夸张。内核采用“先描述整体再按连续区间管理”的方式task_struct表示任务mm_struct表示一整套用户地址空间VMA 则描述其中一段连续且属性一致的区间。三类结构的职责可以这样理解内核对象管理粒度典型内容task_struct一个任务/线程PID、状态、调度信息、打开文件引用、mm指针mm_struct一套用户地址空间页表根、代码/数据/堆/栈边界、参数与环境边界、VMA 索引、统计信息VMA概念上由vm_area_struct表示一段连续虚拟区间vm_start、vm_end、访问权限、映射标志、文件来源、相关操作经典内核资料中常见的mm_struct字段具有以下含义。实际字段、类型和索引结构会随内核版本变化因此不能把某一版结构体当成稳定用户态 ABI。pgd一类字段指向页表根是虚拟地址翻译的起点。start_code/end_code、start_data/end_data记录代码和数据的边界。start_brk/brk记录传统堆区的起点和当前堆顶start_stack记录初始栈相关位置。arg_start/arg_end与env_start/env_end描述命令行参数和环境变量的范围。total_vm、locked_vm、shared_vm、exec_vm、stack_vm等字段用于不同维度的页面统计。VMA 索引让内核能快速查找“某个虚拟地址属于哪个区间”。许多教材和旧内核图会画成mm-mmap加vm_next链表阅读具体内核源码时应以该版本真实的 VMA 索引实现为准。一个用户进程通常有自己的mm_struct同进程线程则共享它。纯内核线程没有普通用户地址空间mm可以为空这进一步说明task_struct与mm_struct不是“永远一一对应”的死规则而是由进程/线程模型决定的关联。4.2 为什么按“区间”管理以及 VMA 如何变化可以把地址空间想成一把 100cm 的刻度尺。桌面 A 占用[0, 50)桌面 B 占用[50, 100)系统只需记录两个起止区间而不必为 100 个刻度分别建对象。半开区间[start, end)还能让相邻边界自然衔接前一段的end正好等于后一段的start不会重复包含边界字节。VMA 也是如此。只要一段虚拟地址连续、权限相同、映射来源和相关属性一致就可以由一个 VMA 描述。如果对其中[20, 30)子区间调用mprotect修改权限原来的[0, 50)可能被拆成[0, 20)、[20, 30)、[30, 50)三段相邻 VMA 后来若重新具备完全相同的属性也可能再次合并。常见操作对 VMA 的影响如下操作主要用途对地址空间的典型影响mmap建立文件或匿名映射创建新 VMA必要时调整周围区间munmap解除一段映射删除、截断或拆分 VMA并清理相关 PTEmprotect修改访问权限更新权限可能把一个 VMA 拆成多段brk调整传统堆顶扩大或缩小堆相关虚拟区间栈自动扩展满足合法的栈增长在限制和安全检查通过后扩展栈 VMAVMA 解决“这个地址区间应该是什么”的问题页表解决“这个虚拟页当前实际映射到哪里”的问题。二者不能互相替代只有 VMA 没有 PTECPU 仍无法直接访问物理页只有 PTE 没有合法 VMA缺页和权限管理也失去了完整的高层语义。权限保护可以用字符串常量说明char*strhelloworld;str[0]H;/* 未定义行为常见结果是 SIGSEGV */字符串字面量通常位于只读映射区。str本身是一个可修改的指针变量但它指向的字符区域没有写权限。执行写指令时MMU 发现 PTE 权限不符并触发保护异常内核再检查 VMA确认这不是可修复的 COW 写入最终通常发送SIGSEGV。正确写法是创建可写数组charstr[]helloworld;str[0]H;这段数组位于可写存储区。这个例子也说明指针数值只是地址能不能读、能不能写、能不能执行还取决于 VMA 和页表权限。5. 从 exec 到退出代码和数据如何真正进入内存5.1 先建立映射再在访问时装入页面Shell 启动外部程序时常见路径是先fork得到子进程再由子进程调用execve。fork创建新的任务关系和 COW 地址空间execve不会创建一个新 PID而是用目标程序映像替换调用线程所在进程的旧用户地址空间。execve的核心流程可以概括为内核读取 ELF 头和程序头检查格式、架构、权限及装载信息。为新程序准备mm_struct依据各个PT_LOAD段建立代码、只读数据、可写数据等 VMA。创建用户栈把argc、argv、envp和辅助向量等启动信息放入合适位置。动态链接程序还会映射解释器和共享库由动态链接器完成重定位等工作。大多数代码页和数据页不必立刻读入。CPU 第一次取指或访问数据时触发缺页文件后备页从页缓存/文件取得BSS、堆和栈等匿名页通常按零初始化。页面映射完成后重试指令程序感觉不到中间曾经暂停处理缺页。这回答了一个常见的先后问题创建进程管理结构与装入全部代码数据不是一个不可分割的动作。内核可以先有task_struct、mm_struct、VMA 和稀疏页表再随着执行逐页补齐物理内存。对于典型fork exec路径fork先以 COW 方式得到地址空间execve随后丢弃旧映像并建立新映像对于内核线程甚至可以没有普通用户地址空间。5.2 malloc、free、退出与物理内存回收用户调用malloc时分配器先在自己的空闲块中查找不足时再通过brk或mmap向内核申请更多虚拟地址范围。申请成功只表示虚拟区间可用不保证所有页面已经有独占物理页。第一次写入页面时匿名缺页通常才会真正分配并零填充物理页。free也不等于每次都立刻把物理页还给内核。为了提高后续分配效率用户态分配器可能保留小块内存较大的mmap分配可能通过munmap直接解除堆顶附近的连续空闲区也可能通过调整brk归还。观察内存问题时必须区分以下指标指标含义常见误区VSZ/VIRT进程虚拟地址空间总量大不代表全在 RAM 中RSS/RES当前常驻物理内存包含共享页时会重复计数PSS共享页按共享者数量折算后的占用比 RSS 更适合估算进程实际分摊匿名内存堆、栈、匿名映射等可被交换不一定始终驻留文件映射代码、共享库、mmap 文件干净页通常可直接回收后重读进程正常退出或被终止后内核撤销其用户地址空间释放页表和 VMA对物理页、文件页和共享对象递减引用计数。只有没有其他映射者继续引用的资源才会真正释放。execve则在保留 PID 等进程身份的同时用新mm替换旧映像旧地址空间也按相同引用规则清理。6. 实战观察用 Linux 工具验证地址空间6.1 编写一个可观察的地址布局程序先准备一个小程序同时创建代码、已初始化数据、BSS、堆和栈对象#includestdio.h#includestdlib.h#includeunistd.hintg_init100;intg_uninit;staticconstcharg_readonly[]hello;intmain(void){intstack_value1;int*heap_valuemalloc(sizeof(*heap_value));if(heap_valueNULL){perror(malloc);return1;}*heap_value2;printf(pid %d\n,getpid());printf(code %p\n,(void*)main);printf(rodata %p\n,(void*)g_readonly);printf(data %p\n,(void*)g_init);printf(bss %p\n,(void*)g_uninit);printf(heap %p\n,(void*)heap_value);printf(stack %p\n,(void*)stack_value);fflush(stdout);sleep(300);free(heap_value);return0;}编译并在后台运行mossVM-0-4-ubuntu:~/process-space$ gcc-O0-gaddress_demo.c-oaddress_demo mossVM-0-4-ubuntu:~/process-space$ ./address_demo[1]4208mossVM-0-4-ubuntu:~/process-space$PID$!./address_demo 中的让程序在后台运行Shell 变量$!保存最近一个后台任务的 PID因此PID$!便于后续查询。程序用sleep(300)暂停 300 秒让我们有时间查看它的映射。具体地址每次运行可能改变这是 ASLR 的正常表现。6.2 查看 maps、ELF 段与物理占用先查看进程的 VMAmossVM-0-4-ubuntu:~/process-space$cat/proc/$PID/maps 55c7c2f74000-55c7c2f75000 r--p 00000000 08:01123456/home/moss/process-space/address_demo 55c7c2f75000-55c7c2f76000 r-xp 00001000 08:01123456/home/moss/process-space/address_demo 55c7d71ec000-55c7d720d000 rw-p 00000000 00:000[heap]7ffd6a8b5000-7ffd6a8d6000 rw-p 00000000 00:000[stack]cat输出文件内容/proc/$PID/maps是目标进程的映射表。每行依次包含地址范围、权限、文件偏移、设备号、inode 和映射来源。权限中的r、w、x分别表示可读、可写、可执行p表示私有映射s表示共享映射。输出已做精简真实进程还会包含动态链接器、libc、vDSO 等区域。再查看 ELF 程序头mossVM-0-4-ubuntu:~/process-space$ readelf-l./address_demoreadelf用来读取 ELF 元数据-l表示显示 program headers。重点观察LOAD段及其R、W、E标志它们解释了为什么/proc/$PID/maps中会形成只读、可执行和可写的不同 VMA。ELF 的 section 面向链接组织program segment 才是装载器建立运行时映射的主要依据二者不要混为一谈。查看进程内存统计mossVM-0-4-ubuntu:~/process-space$ pmap-x$PIDmossVM-0-4-ubuntu:~/process-space$cat/proc/$PID/smaps_rollup mossVM-0-4-ubuntu:~/process-space$ps-opid,stat,vsz,rss,cmd-p$PID命令与参数含义如下命令参数含义重点观察pmap -x $PID-x显示扩展映射统计每段虚拟大小、RSS、脏页cat /proc/$PID/smaps_rollup汇总smaps的内存指标RSS、PSS、匿名页、交换量ps -o ... -p $PID-o自定义列-p指定 PIDSTAT、VSZ、RSS 的差别查询页面大小和栈限制mossVM-0-4-ubuntu:~/process-space$ getconf PAGE_SIZE4096mossVM-0-4-ubuntu:~/process-space$ulimit-s8192getconf PAGE_SIZE查询系统基础页大小示例输出 4096 字节系统也可能支持透明大页或显式 HugeTLB不能因此断言所有映射都只用 4KiB 页。ulimit -s查询当前 Shell 为新进程设置的栈大小软限制单位通常为 KiB示例 8192 表示 8MiB。它是上限策略不代表进程启动时已经常驻 8MiB 栈内存。实验结束后终止后台程序并回收任务mossVM-0-4-ubuntu:~/process-space$kill$PIDmossVM-0-4-ubuntu:~/process-space$wait$PIDkill $PID默认发送SIGTERM请求进程正常终止wait $PID让当前 Shell 回收该子进程的退出状态避免留下僵尸任务。如果进程仍在执行清理逻辑SIGTERM不保证瞬间退出不应一上来就使用强制信号。总结Linux 进程地址空间的关键不是背诵一张“代码、数据、堆、栈”的固定布局图而是理解三层关系程序使用虚拟地址内核用mm_struct和 VMA 描述哪些区间合法、具有什么属性硬件再依靠 MMU、TLB 与页表把虚拟页翻译到物理页。VMA 管区间语义PTE 管页面当前落点二者共同完成隔离、权限和按需调页。同一虚拟地址可以在不同进程中映射到不同物理页也可以被有意映射到同一物理页。fork正是先共享私有页面再在首次写入时通过 COW 拆分从而减少创建时间和内存消耗。execve则先依据 ELF 建立新地址空间代码和数据页在真正访问时按需装入。理解这条主线后段错误、VSZ 与 RSS 差异、共享库、malloc、进程睡眠和内存回收等现象就都能放进同一套模型中解释。