Linux程序地址空间

一 预热

1 进程虚拟地址空间

这是操作系统给每个进程营造的 "独立内存假象",从低地址高地址大致分为以下区域:

  1. 代码段(.text)
    • 内容 :编译后的机器指令(如 main 函数、系统调用)。
    • 属性只读、可执行
    • 特点 :多个进程可共享(如运行多个 ls 命令共用同一份代码),防止程序意外修改自身逻辑。
  2. 常量区(.rodata)
    • 内容 :字符串常量(如 "hello world")、const 全局常量。
    • 属性只读
    • 关键点 :图中明确指出 char *str = "hello world" 中的字符串存于此,不可直接修改,否则会触发段错误。
  3. 数据段(.data)与 BSS 段
    • .data 段 :存放已初始化 的全局变量、静态变量(static)。
    • .bss 段 :存放未初始化的全局变量、静态变量。程序加载时由系统自动清零。
    • 共同点:生命周期与程序共存,位于高地址下方,堆和栈之间。
  4. 堆(Heap)
    • 方向向上增长(地址升高)。
    • 管理 :由程序员手动分配 / 释放(malloc/free),用于动态内存。
  5. 栈(Stack)
    • 方向向下增长(地址降低)。
    • 管理:编译器自动管理,存放局部变量、函数参数、返回地址。
    • 特点:空间有限(通常 8MB),溢出会导致段错误。

二、 关键机制:fork 与 写时复制(COW)

解析Linux 下 fork() 系统调用的底层优化逻辑:

  1. 传统误区
    • 误以为 fork() 会立刻完整复制父进程的所有物理内存(如代码、数据、堆、栈)。
    • 弊端 :耗时、浪费内存(子进程常随后执行 exec 替换镜像,复制的内容无效)。
  2. 真实实现:写时复制(Copy-On-Write, COW)
    • fork 瞬间不复制任何物理内存
      • 内核只创建子进程的 PCB(进程控制块)和页表。
      • 父子进程共享同一份物理内存页面。
      • 内核将这些共享页面的权限标记为只读
    • 读操作:父子进程任意读取共享页面,数据一致,无开销。
    • 写操作(触发 COW)
      • 当任一进程(父 / 子)尝试写入共享页面时,CPU 触发写保护异常(Page Fault)
      • 内核介入:分配新的物理页,复制原页内容,修改页表指向新页,并恢复可写权限。
      • 结果:此时父子进程拥有独立内存,数据不再互通,实现了 "按需复制"。

三、 关键细节

  1. "不是内存,是进程地址空间 / 虚拟地址空间!"
    • 这是核心重点:程序看到的地址是虚拟地址,需经过页表映射才能找到物理内存。每个进程都有独立的 4GB(32 位)或更大虚拟地址空间,互不干扰。
  2. 常量特性
    • 常量区(字符串)具有常性,只要进程地址空间存在,它就一直存在于内存中,且不可修改。
  3. 堆栈相对而生
    • 堆向上涨,栈向下涨,两者之间是空闲空间。栈溢出可能会覆盖堆数据(堆栈碰撞),导致程序崩溃。

四、 总结

阐述了 Linux 内存管理的两大基石:

  1. 分层布局:清晰的虚拟地址空间划分(代码 / 数据 / 堆 / 栈)保证了内存访问的有序性和安全性。
  2. 惰性复制 :COW 机制极大提升了 fork 的效率,是现代操作系统优化进程创建的核心手段。

二 正式进入虚拟地址空间

一、 核心实验现象:进程独立性的铁证

1. 地址与数值的矛盾
  • 现象 :父子进程中,全局变量 g_val地址0x601054)完全相同,但数值不同(父进程 100,子进程 101/103/105)。
  • 核心结论
    • 打印出的地址 不是物理地址 ,而是 虚拟地址
    • 物理地址对用户完全不可见,由操作系统通过页表隐藏。
    • 这证明了 虚拟地址空间是进程独有的,每个进程都有独立的 4GB 虚拟地址蓝图。

二、 内存管理架构:虚拟 vs 物理

1. 虚拟地址空间(用户视角)
  • 32 位布局
    • 高地址 1G:内核空间(系统共享,用户态无权访问)。
    • 低地址 3G :用户空间(进程私有),包含:
      • 代码段:只读,共享。
      • 数据段 / BSS:全局 / 静态变量。
      • :向上增长,malloc 申请区域。
      • 共享区:动态库、内存映射文件。
      • :向下增长,局部变量 / 函数调用。
2. 物理内存(内核视角)
  • 物理内存是稀缺资源,被所有进程共享。
  • 映射机制 :进程看到的虚拟地址必须通过 页表 转换才能找到物理内存。
  • 图中关键点g_val 在虚拟地址中是 0x601054,在物理内存中对应 0x12345678

三、 核心机制:fork 与 写时复制(COW)

这是图中最核心的技术点,涵盖了从创建到隔离的全过程:

1. 进程创建的流程(OS 自动完成)

Linux 创建进程的流程是:先创建内核数据结构(PCB / 页表),再按需加载代码和数据

  • 支持延迟加载 :如果不立即执行,可以先创建好进程控制块(task_struct),等真正要运行时再加载代码和数据。
2. 父子进程的 "共享与拷贝"
  • fork 瞬间
    • 代码共享:父子进程执行同一段代码(只读)。
    • 数据共享 :内核不立即复制 物理内存,而是让父子进程共享 同一份物理页,并将页表权限标记为 只读
  • 写时复制(触发条件)
    • 当父子进程中任意一个尝试修改 共享数据(如子进程改 g_val)时,CPU 触发 缺页中断(Page Fault)
    • 内核介入:分配新的物理页,复制原页内容,修改页表指向新页,恢复可写权限。
    • 结果:父子进程从此拥有独立内存,数据不再互通,实现了 "进程独立性"。
3. 图中的关键异常:缺页中断引起内存二次申请
  • 当进程访问的虚拟地址不在物理内存中(如写时复制触发),会触发缺页中断。
  • 内核会进行 二次申请:分配新的物理页,建立新的映射。这就是 "深拷贝" 的发生时刻。

四、 动态内存分配:malloc 的本质

  • 图中标注malloc(1024); new XXX();
  • 本质malloc 并没有立刻分配物理内存,而是在 进程虚拟地址空间 中申请了一段连续的 虚拟地址
  • 延迟分配:真正的物理内存分配,发生在第一次访问该地址(触发缺页中断)的时候。

五、 内核数据结构与进程控制块(PCB)

展示进程的内核态管理:

  1. struct task_struct
    • 也就是 PCB(进程控制块)
    • 包含进程的所有元数据(PID、状态、优先级、文件描述符表、信号处理函数等)。
  2. struct mm_struct
    • 管理进程的虚拟地址空间。
    • 图中下方的结构体字段:total_vmlocked_vmstart_codeend_codestart_brkstart_stack 等,定义了地址空间的起止范围和属性。
  3. 父子进程的关系
    • 子进程创建时,以父进程为模板。
    • 子进程会拷贝父进程的 task_struct,但会独属于自己的虚拟地址空间和页表。

六、 核心结论与 OS 规定

  1. 进程独立性的保证
    • 虚拟地址空间和页表,每个进程自有一套
    • 代码只读(父子不影响),数据写时复制(修改后不影响)。
  2. OS 规定
    • 父子进程共享只读代码,互不影响。
    • 若对共享数据进行修改,则发生 "写时复制",变为深拷贝,进程彻底独立。

七、 总结

用户态代码内核态内存管理 的完整链路:

  1. 程序视角 :看到的是虚拟地址(如 0x601054)。
  2. 内核视角:通过页表映射到物理内存。
  3. fork 机制:利用写时复制(COW)优化了进程创建开销。
  4. 最终结果:实现了进程间的完美隔离(独立性)。

三 理解虚拟地址空间和区域划分

一、 核心概念:什么是虚拟地址空间?

1. 核心比喻:OS = 大富翁,内存 = 大饼
  • 物理内存的局限性:物理内存大小有限(如 10 亿字节),无法直接给每个进程完整的 "大饼"。
  • 虚拟地址空间的本质
    • OS 给每个进程画了一张独立的 "大饼蓝图"(虚拟地址空间)。
    • 进程认为自己独占了所有内存(如 "私生子" 各自独立),实际上 OS 只按需分配真正的物理内存("饼")。
  • struct mm_struct 的作用
    • 它是进程的内存管理对象,描述了该进程的虚拟地址空间全貌。
    • 管理逻辑:先描述(建立虚拟地址空间蓝图),再组织(通过页表映射物理内存)。
    • 按需分配:OS 不会一次性给足,而是进程要用多少,就批多少虚拟地址,再动态映射物理内存。
2. 进程独立性的比喻
  • "私生子" 比喻:每个进程都认为自己是 OS 的独子,拥有完整的地址空间。
  • 实际上 OS 只是为每个进程提供了独立的 虚拟地址蓝图,物理内存则由 OS 统一调度、共享。

二、 区域划分:如何理解虚拟地址空间中的区域划分?

1. 核心比喻:分桌子
  • 问题:多人共用一张大桌子(物理内存),容易混乱。
  • 解决方案:OS 将虚拟地址空间(桌子)划分为不同区域(代码区、数据区、堆、栈等),每个区域有专属的起止地址。
2. 区域划分的代码实现逻辑
  • 类比 struct deskop(桌面区域结构体)
    • 定义区域的起始和结束地址(如 int xs_start, int xs_end)。
    • 例如 struct deskop d = {1, 50, 50, 100} 表示一个区域。
  • 计算机语言的实际应用
    • struct 描述虚拟地址区域,记录区域的权限 (可读 / 可写 / 可执行)和范围
    • 区域内的地址是连续且有效的,进程可以直接使用,无需关心物理内存的具体位置。
3. 区域划分的目的
  • 隔离与保护:不同区域(如代码区 vs 数据区)权限不同,防止越界访问或修改。
  • 高效管理 :按区域管理内存,方便进行缺页中断权限检查等操作。

三、 关键机制:页表与权限管理

1. 页表的核心作用
  • 它是 虚拟地址 → 物理地址翻译官
  • 图中展示了 页表项 的结构:
    • 虚拟地址0x601054
    • 映射的物理地址0x11223344
    • 标志位:包含权限(读 / 写 / 执行)、是否存在、是否被修改等。
2. 权限管理:为什么代码和常量是只读的?
  • 问题 :为什么全局变量、static 变量生命周期是全局的?为什么字符串和代码是只读的?
  • 答案 :由页表标志位 强制限制。
    • 代码段(.text) :页表标记为 只读(Read-Only) + 可执行(Execute)
    • 常量区(.rodata) :页表标记为 只读
    • 效果 :如果进程尝试修改代码或常量,CPU 会触发 段错误(Segmentation Fault),因为页表禁止了写操作。
3. 生命周期与区域的关系
  • 全局 / 静态变量 :存放在 数据段 /.bss 区,生命周期与程序一致,因此是 "全局的"。
  • 字符串常量 / 代码 :存放在 只读区,生命周期与进程一致,且禁止修改。

四、 硬件支撑:MMU(内存管理单元)

1. MMU 的角色
  • 硬件 :集成在 CPU 内部,负责实时完成虚拟地址到物理地址的转换。
  • 核心功能
    • 检查页表中的权限标志位
    • 拦截非法操作(如向只读区域写数据)。
    • 处理缺页中断(当物理内存缺失时,触发 OS 介入)。
2. 实战案例:修改字符串的崩溃过程
  • 代码char *s = "helloworld"; *s = 'H';
  • 过程
    1. "helloworld" 存放在 只读区 ,页表标记为 只读
    2. 执行 *s = 'H' 时,CPU 通过 MMU 检查权限。
    3. 发现是写操作 ,但目标区域是只读
    4. MMU 触发 权限异常 ,操作系统终止进程,输出 段错误

五、 总结:内存管理的三层架构

这张图串联起了从 用户逻辑硬件实现 的完整链路:

  1. 用户层 :通过 struct mm_struct 描述虚拟地址空间,按区域划分使用。
  2. 内核层:通过页表建立虚拟地址与物理内存的映射关系,管理权限。
  3. 硬件层 :CPU 内的 MMU 负责实时地址转换和权限检查,保障内存安全。

💡 核心启示

这张图的精髓在于 "虚拟化" 与 "隔离"

  • 虚拟化:OS 创造了 "独占内存" 的假象(虚拟地址空间)。
  • 隔离:通过区域划分和页表权限,严格保护进程间、进程与内核间的内存安全。

四 虚拟地址空间设置的意义

一、 开篇核心:为什么需要虚拟地址空间?

图中左上角明确给出了 3 个根本原因,是理解所有后续内容的前提:

  1. **保护物理内存(安全层面)**虚拟地址空间 + 页表机制,会拦截进程的非法访问(如越界、修改只读代码 / 常量),从根源上避免进程直接操作物理内存,防止恶意 / 误操作破坏系统。
  2. 无序变有序(内存布局层面) 物理内存是碎片化的(图中右侧物理内存的零散块),但虚拟地址空间给进程呈现了连续、完整的 4G(32 位)地址蓝图,让程序无需关心物理内存的实际排布,实现了 "逻辑连续、物理离散"。
  3. **解耦进程管理与内存管理(架构层面)**进程管理只需要维护虚拟地址空间的布局,内存管理只需要维护物理内存的分配,两者通过页表解耦,大幅提升了系统的可扩展性和管理效率。

关键细节

  • 虚拟连续 vs 物理不连续:虚拟地址空间的区域是连续的,但对应到物理内存是零散的,通过页表完成映射。
  • 堆与栈的相对生长:堆向上、栈向下,中间是空闲虚拟地址空间,避免了相互覆盖。

二、 内核数据结构:虚拟地址空间的 "管理者"

Linux 内核中管理虚拟地址空间的三层数据结构,是理解内存管理的核心:

1. task_struct(进程控制块 PCB)

  • 是进程的 "身份证",包含进程所有元数据(PID、状态、文件描述符、信号等)。
  • 其中的struct mm_struct *mm指针,指向该进程的虚拟地址空间管理结构体,是进程与内存的连接点。

2. mm_struct(虚拟地址空间总控)

  • 整个进程虚拟地址空间的总描述符
  • 核心作用:
    • 记录虚拟地址空间各区域的起止地址 (如start_code/end_code对应代码段)。
    • 维护vm_area_struct链表 / 红黑树,管理所有虚拟区域。
    • 关联页表,完成虚拟地址到物理地址的映射。

3. vm_area_struct(虚拟区域结构体)

  • 虚拟地址空间中每个独立区域的描述符,对应图中右侧的代码段、数据段、堆、栈等彩色块。
  • 每个vm_area_struct描述一个连续的虚拟地址范围,包含:
    • 区域的起始 / 结束地址、权限(读 / 写 / 执行)。
    • 前后指针vm_prev/vm_next,形成链表,用于遍历所有区域。
    • 关联的mm_struct,归属到对应进程。
  • 核心特性:
    • 虚拟地址空间的宏观划分,由这些结构体串联而成。
    • mmap系统调用会创建新的vm_area_struct,用于文件映射、共享内存等。

三、 虚拟地址 → 物理地址的映射

图中右侧完整展示了映射逻辑:

  1. 虚拟地址空间 :进程看到的连续 4G 地址,由mm_structvm_area_struct管理。
  2. 页表:是虚拟地址到物理地址的 "翻译表",每个进程有独立的页表,存储在内核空间。
  3. 物理内存:实际的硬件内存,是零散的物理页框,由内核统一分配管理。
  4. 映射流程
    • 进程访问虚拟地址 → CPU 通过 MMU(内存管理单元)查页表 → 找到对应物理地址 → 访问物理内存。
    • 若页表中无对应映射,触发缺页中断 ,内核动态分配物理页并更新页表(malloc的延迟分配原理)。

四、 补充关键知识点

  1. malloc的本质 malloc(100)只是在虚拟地址空间的堆区 申请了一段连续的虚拟地址(如0x10000000),不会立即分配物理内存,真正的物理内存分配发生在第一次访问该地址(触发缺页中断)时。

  2. 区域权限的意义 每个vm_area_struct都有独立的权限标志:

    • 代码段:只读 + 可执行,防止程序修改自身代码。
    • 常量区:只读,防止修改字符串常量(如char *s = "hello"; *s = 'H'会触发段错误)。
    • 数据段 / 堆 / 栈:可读写,用于存储变量。
  3. 进程独立性的保障 每个进程有独立的mm_structvm_area_struct链表和页表,因此即使虚拟地址相同,也会映射到完全不同的物理内存,实现了进程间的内存隔离。


五、 总结

这张图完整打通了 Linux 内存管理的全链路:

  1. 设计层面:虚拟地址空间解决了物理内存的安全、布局、管理三大问题。
  2. 布局层面:清晰的区域划分,规范了程序的内存使用。
  3. 内核层面task_structmm_structvm_area_struct的三层结构,实现了虚拟地址空间的精细化管理。
  4. 映射层面:页表 + MMU 完成虚拟地址到物理地址的翻译,保障了进程独立性和内存安全。
相关推荐
大卡片2 小时前
linux和IO常见面试题
linux·运维·服务器
RisunJan2 小时前
Linux命令-newusers(用于批处理的方式一次创建多个命令)
linux·运维·服务器
嵌入式吴彦祖2 小时前
RKNN demo运行
linux
草莓熊Lotso2 小时前
Linux 线程深度剖析:线程 ID 本质、地址空间布局与 pthread 源码全解
android·linux·运维·服务器·数据库·c++
殇者知忧2 小时前
Tmux快速上手
linux·tmux
AcrelGHP2 小时前
安科瑞AIM-T系列工业IT绝缘监测及故障定位解决方案为关键供电场所筑牢安全防线
大数据·运维·数据库
草莓熊Lotso3 小时前
MySQL 从入门到实战:视图特性 + 用户权限管理全解
linux·运维·服务器·数据库·c++·mysql
雾岛听蓝3 小时前
进程信号机制深度解析
linux·开发语言·经验分享·笔记
zmjjdank1ng4 小时前
OSI模型和TCP/IP模型
服务器·网络·tcp/ip