初始虚拟地址
一、代码看见虚拟地址空谈理论晦涩难懂我们从一段简单的父子进程代码入手通过直观的运行现象发现内存地址的隐藏秘密这也是理解虚拟地址空间的最佳切入点。1.1 测试代码1.2 运行结果与现象运行现象父子进程打印的全局变量地址完全一致但是变量数值不同子进程修改为100父进程仍保留初始值0。1.3 从现象得出核心结论单单这一个反常现象就能推翻固有认知我们可以得出3个关键结论变量内容不同说明父子进程操作的不是同一块物理内存数据相互独立变量地址相同说明该地址绝对不是物理地址物理内存不可能出现同地址存不同数据Linux下该地址为虚拟地址我们C/C代码中打印、使用的所有地址全部都是虚拟地址物理地址对用户完全透明全程由操作系统底层管控用户无法直接操作操作系统必须完成核心工作虚拟地址 → 物理地址的翻译映射。1.4 现象底层原理写时拷贝为什么地址相同、数据却不同核心原理是Linux的写时拷贝机制。fork创建子进程时操作系统不会直接拷贝父进程的物理内存为了节省资源会让父子进程共享同一块物理内存此时二者虚拟地址完全一致。当父子进程任意一方尝试修改数据时操作系统会触发写时拷贝单独开辟一块新的物理内存拷贝原有数据并完成修改。修改后二者虚拟地址保持不变但映射到了不同的物理内存。这也完美解释了本次代码的反常现象地址相同、数值不同。图片注解从上图可以清晰看出修改前父子进程虚拟地址、物理地址完全共享修改后子进程物理内存发生拷贝分离虚拟地址维持不变严格遵循写时拷贝机制。二、内存五大分区虚拟地址空间划分通过上面的代码实验我们确定了核心前提日常编码使用的所有地址都是虚拟地址。而我们老生常谈的代码段、栈、堆等内存分区全部都是操作系统在虚拟地址空间上划分出来的逻辑区域并非物理内存固有分区。图片注解这是标准的32位进程虚拟地址空间分布图地址从下往上低地址→高地址依次排布分区界限清晰也是下文讲解的核心依据。2.1 内存分区排布顺序高地址 → 低地址结合上图视觉排布图片上方为高地址、下方为低地址我们按照从高地址到低地址的顺序自上而下解析每一块分区的作用与特性栈区Stack靠近高地址位置存放局部变量、函数参数、返回地址系统自动申请释放内存向下生长有固定容量上限堆区Heap位于栈区下方由程序员手动malloc申请、free释放内存向上动态生长无固定大小上限全局数据区.data .bss存放全局变量、static静态变量编译阶段确定大小运行期间不会主动释放常量区.rodata存放字符串常量、const修饰的全局常量硬件层面标记只读禁止修改代码段.text处于整片用户空间最低地址存放编译后的二进制机器指令权限为只读运行期间大小固定不可扩容。2.2 栈区之上的区域结合上面的空间分布图可以发现栈区并非最高地址边界。在栈区往上的高地址位置还有两块特殊区域内核空间位于整片虚拟空间最顶部属于操作系统内核专属内存用户进程没有权限修改、直接访问。环境变量命令行参数区专门存放程序运行所需的环境变量、命令行参数argv在32位系统下操作系统会给每一个进程独立分配0~4GB的虚拟地址空间进程之间空间相互隔离、互不干扰。2.3 全局区与常量区核心特性结合分区特性解答两个疑问1. 为什么全局变量、static变量生命周期随进程全局数据区data/bss属于编译期固定大小区域进程创建之初操作系统就会为该区域映射虚拟内存运行期间不会主动释放、收缩。这片内存永久绑定当前进程只有进程终止销毁时操作系统才会统一回收因此全局、静态变量不会提前销毁生命周期贯穿整个进程。2. 为什么常量区数据不能修改这并非单纯的C语言语法限制本质是硬件权限拦截。常量区对应的内存页操作系统会在页表中标记为只读权限。当程序尝试修改常量数据时硬件MMU会校验页表权限权限不匹配直接触发内存异常程序崩溃报错。1、语言层面原因C 语言标准定义双引号字符串字面量本质是const char[]只读常量语法层面就规定它是固定不可变数据。语法隐式转换漏洞char*可以接收const char*地址只是语法兼容妥协不代表赋予写入权限。行为定性修改字符串字面量属于未定义行为语言标准不保障运行结果禁止人为修改。设计初衷常量字符串全局共享程序中多处使用同个字面量只会存一份一旦修改会全局错乱语言直接从规则上杜绝这种错误。2、底层页表 操作系统权限层面程序编译后字符串常量统一存放至.rodata 只读数据段。操作系统通过虚拟内存 页表管理进程内存会给 .rodata 所在的内存页打上只读 (R) 权限取消写入 (W) 权限。CPU 内置MMU 内存管理单元每次读写内存都会查询页表权限读数据权限合法正常执行写入数据检测到页表无写权限直接触发硬件异常。操作系统捕获异常后直接判定为非法内存访问抛出段错误终止进程从硬件底层锁死写入操作。3、指针指向字符串 与 字符数组初始化 区别1. char *p abc;执行逻辑仅让指针变量p直接指向常量区原始字符串本体无任何数据拷贝。内存位置指针指向 .rodata 只读内存页页表禁止写入。结果试图修改p[]就是往无写权限的常量页写入直接报错无法修改。2. char arr[] abc;执行逻辑先读取常量区的字符串内容完整拷贝一份副本。内存位置副本存放至栈内存栈内存对应的内存页表默认开启读写双权限。结果修改arr[]只是修改栈上独立副本不触碰常量区权限合法可以随意修改。指针方式能否改为可写如果想让指针指向可写内存你需要显式分配内存char *p malloc(6); strcpy(p, hello); // 现在 p 指向堆上的副本可写 // 或者 char buf[6]; p buf;语言定规则禁止修改常量字面量操作系统页表设只读权限硬件拦截写入指针直连常量本体不能改数组拷贝到可写栈内存就能改。三、虚拟地址如何访问物理内存我们已经清楚进程使用虚拟地址、划分虚拟内存分区那么虚拟地址如何落地到真实的物理内存条这就需要依靠寻址机制和页表完成映射也是虚拟内存的核心底层逻辑。3.1 完整寻址链路CPU无法直接识别物理地址执行代码时只会发出虚拟地址完整寻址流程层层递进为了清晰拆解底层流转我把程序从磁盘运行、到最终访问物理内存的完整流程按真实执行顺序拆解1.磁盘存储阶段程序未运行时所有二进制代码、常量、全局数据全部静态存放在磁盘中无任何内存占用2.创建进程PCB双击运行程序操作系统首先创建task_structPCB进程控制块给进程分配唯一标识记录进程基础信息3.开辟虚拟地址空间操作系统为该进程创建mm_struct内存描述符直接划分出一整块完整的虚拟地址空间32位为0~4GB提前规划好代码段、堆、栈等分区布局4.创建进程专属页表操作系统为当前进程单独生成一张页表页表存放在物理内存中PCB记录页表起始地址同时给常量区、代码段配置只读权限此时仅完成虚拟地址规划没有加载任何数据至物理内存5.CPU发出虚拟地址程序运行CPU执行指令只会生成并使用虚拟地址永远不会直接生成物理地址6.硬件MMU介入翻译虚拟地址送入硬件MMU内存管理单元MMU自动拆分地址、查询当前进程页表7.判断内存状态缺页中断详解页表内部存在一位标记位存在位用于判定当前虚拟地址是否映射物理内存。若为首次访问该虚拟地址无物理内存绑定、存在位为0硬件触发缺页中断操作系统接管中断暂停当前进程从磁盘可执行文件中拷贝对应代码与数据写入空闲物理内存页随后修改页表补全虚拟地址与物理地址的映射关系、修改存在位为1若虚拟地址已完成映射直接跳过中断逻辑执行下一步8.权限校验地址翻译MMU校验页表权限位可读/可写/执行拦截非法操作合法则将虚拟地址翻译为真实物理地址9.访问物理内存最终通过物理地址读写内存条中的真实数据。总结极简链路磁盘程序 → 创建PCB → 分配虚拟空间 → 建立专属页表 → CPU下发虚拟地址 → MMU查表翻译 → 合法访问物理内存3.2 页表是什么页表就是操作系统给每个进程单独配备的地址翻译字典。为了方便硬件统一管理内存、减少内存碎片、提高映射效率操作系统会将虚拟地址空间、物理内存空间统一切割为固定大小的内存页Linux默认一页大小为4KB。内存分页后操作系统不再以字节为单位管理内存而是以页为单位进行分配、映射、回收。页表本质是一条映射记录表每一条表项都会保存四个核心字段这里结合真实业务场景详细解释场景程序读取字符串常量char* str hello linux;1.虚拟页号当前访问的虚拟地址属于哪一块虚拟内存页用于在虚拟空间定位数据2.物理页框号该虚拟页真实挂载的物理内存页编号用于MMU翻译成物理地址3.权限位管控当前内存页的访问属性。该字符串处于常量区权限位标记为只读若代码尝试修改 str[0]硬件检测权限不匹配直接触发段错误4.存在位判定数据是否在物理内存。若为0代表当前页面未加载进物理内存触发缺页中断若为1代表映射正常可以直接访问。 同时页表本身也占用内存存储在物理内存当中不会存放在虚拟空间。每一个进程的PCBtask_struct中都会记录该进程专属页表的物理起始地址保证MMU查表时不会错乱做到进程之间页表相互隔离。3.3 谁来查表完成地址翻译很多人误以为是操作系统查表实则不然。完成地址翻译、权限校验的是硬件MMU内存管理单元硬件直接执行查表操作无需软件干预翻译速度极快不会影响程序运行效率。3.4 代码数据存磁盘还是内存结合虚拟内存映射机制我们彻底分清磁盘与内存的存储关系解答常见内存疑问程序未运行所有代码、常量、全局数据全部存放在磁盘无内存占用程序运行进程状态操作系统采用按需加载机制不会一次性加载全部数据正在使用的代码数据加载进物理内存映射对应虚拟地址暂时未使用的数据保留在原磁盘文件中不占用物理内存曾经使用、目前闲置的数据操作系统自动换入磁盘Swap交换分区腾出物理内存补充知识点栈变量出作用域销毁、堆内存free释放不会写入磁盘仅回收虚拟内存空间数据直接丢弃无持久化存储逻辑。四、Linux进程内存管理结构体了解完用户层面的虚拟空间、映射原理我们进一步深入Linux内核。操作系统依靠两个核心结构体精准管理每一个进程的虚拟地址空间。4.1 两大核心结构体task_structPCB进程控制块进程的专属身份证存放进程ID、运行状态、优先级、寄存器、内存指针等全部进程信息mm_struct内存描述符专门用来描述进程虚拟地址空间task_struct内部包含一个指针指向当前进程专属的mm_struct。4.2 mm_struct核心作用mm_struct是虚拟内存的管控核心核心功能有三点独立性每一个进程独有一份mm_struct实现进程内存相互隔离、互不干扰边界记录精准记录虚拟地址空间的分区边界包含代码段、堆、栈的起始与结束地址映射管控维护该进程的所有页表信息管控虚拟地址与物理地址的映射关系。4.3 虚拟内存空间划分规则结合前文配图以32位Linux系统为例4GB虚拟地址空间严格划分为两大区域用户空间0~3GB面向用户开发包含代码段、堆、栈、全局区等所有编码可操作的分区权限开放用户可自主读写内核空间3GB~4GB存放操作系统内核代码、内核数据权限等级最高普通用户进程无法直接修改、访问。struct mm_struct { /*...*/ struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/ /*...*/ // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end /*...*/ }那既然每⼀个进程都会有⾃⼰独⽴的mm_struct操作系统肯定是要将这么多进程的mm_struct组织起来的虚拟空间的组织⽅式有两种1.当虚拟区较少时采取单链表由mmap指针指向这个链表2.当虚拟区间多时采取红⿊树进⾏管理由mm_rb指向这棵树。linux内核使⽤vm_area_struct结构来表⽰⼀个独⽴的虚拟内存区域(VMA)由于每个不同质的虚拟内存区域功能和内部机制都不同因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA⽅便进 程快速访问。struct vm_area_struct { unsigned long vm_start; //虚存区起始 unsigned long vm_end; //虚存区结束 struct vm_area_struct *vm_next, *vm_prev; //前后指针 struct rb_node vm_rb; //红⿊树中的位置 unsigned long rb_subtree_gap; struct mm_struct *vm_mm; //所属的 mm_struct pgprot_t vm_page_prot; unsigned long vm_flags; //标志位 struct { struct rb_node rb; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain; struct anon_vma *anon_vma; const struct vm_operations_struct *vm_ops; //vma对应的实际操作 unsigned long vm_pgoff; //⽂件映射偏移量 struct file * vm_file; //映射的⽂件 void * vm_private_data; //私有数据 atomic_long_t swap_readahead_info; #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif struct vm_userfaultfd_ctx vm_userfaultfd_ctx; } __randomize_layout;五、操作系统为什么需要虚拟内存从代码现象到空间分区再到内核结构体我们已经吃透虚拟内存的底层逻辑。本节总结虚拟内存的三大核心价值理解操作系统设计的底层思想。5.1 权限管控保护物理内存拦截非法操作操作系统通过页表权限位给不同内存页设置访问权限只读、可读写、可执行。例如常量区标记只读、内核空间标记特权访问。一旦进程越界修改、非法访问内存MMU硬件会直接拦截违规操作触发段错误终止程序。既避免单个进程破坏系统内存又防止进程之间相互干扰从硬件层面保障物理内存安全。5.2 规整地址让无序物理内存变为有序虚拟空间物理内存本身是杂乱无序的内存条长期分配释放内存会产生大量内存碎片空闲内存页零散分布、毫无规律。而虚拟内存人为规整有序操作系统给每个进程划分一整块连续、平整的虚拟地址空间固定分区排布。开发者只需操作整齐易懂的虚拟地址底层杂乱的物理内存由页表自动映射屏蔽大幅降低内存开发与管理难度。5.3 解耦合进程与物理内存彻底分离虚拟内存的核心设计思想之一便是完成进程与物理内存的双向解耦。解耦本质是在进程逻辑与物理硬件之间新增一层地址抽象映射层让二者互不绑定、独立迭代运行。我们通过有无虚拟内存的对比结合内存运行机制深度解析5.3.1 无虚拟内存进程直接绑定物理内存原生致命缺陷在无虚拟内存的裸机环境下进程必须直接使用物理地址物理内存以连续内存块为单位分配存在不可规避的硬性缺陷内存碎片无法规避系统长期频繁申请、释放内存物理内存会被切割为大量零散空闲页产生外部碎片。即便物理内存总空闲容量充足若无连续大块空闲空间大型进程依旧无法加载运行软硬件高度耦合程序编码阶段会固化物理内存地址物理内存容量、硬件布局一旦改动所有程序必须重新编译适配硬件兼容性极差并发能力受限进程必须一次性完整载入物理内存无法拆分加载物理内存硬件容量直接限制系统最大并发进程数。5.3.2 有虚拟内存进程与物理内存彻底隔离依托页表映射机制进程仅识别规整连续的虚拟地址完全不感知物理内存的真实排布、容量与分配状态。物理内存允许碎片化、离散化存储操作系统通过页表将零散的物理页框映射拼接为进程视角下连续完整的虚拟地址空间从逻辑上屏蔽底层硬件差异。5.3.3 真实运行场景详解以一台16G物理内存的Linux主机为例系统同时运行浏览器、代码编译器、终端、后台守护进程等数十个进程。主机长期运行后物理内存会产生大量内存碎片空闲物理页零散分布在内存条各处物理内存排布杂乱无序。但对每一个进程而言依旧持有独立、完整、分区规整的虚拟地址空间进程完全感知不到底层物理内存的碎片化状态。当物理内存资源占用过高时操作系统会启动内存置换机制将长期未访问的闲置物理内存页写入磁盘Swap交换分区回收空闲物理页供给活跃进程使用。5.3.4 解耦合最终价值该抽象映射层彻底切断了进程与物理内存的硬性绑定实现两层解耦一方面进程业务逻辑与物理内存布局解耦进程无需关心内存分配规则另一方面软件程序与硬件内存规格解耦程序可跨内存硬件正常运行。该机制大幅提升内存利用率、程序通用性与系统并发调度能力。有了虚拟内存后进程只依赖虚拟地址不依赖物理内存硬件举个通俗场景一台16G物理内存的电脑同时运行数十个进程。物理内存碎片化严重但每个进程依旧拥有完整规整的虚拟空间。操作系统自由调度物理内存、置换Swap分区进程完全无感知实现进程管理与物理内存管理解耦合大幅提升内存利用率与程序兼容性。六、全文终极总结通读全文我们结合代码、配图、内核原理完整吃透虚拟地址空间浓缩5条核心要点通过fork代码现象可证日常代码打印、使用的地址全部是虚拟地址C语言五大内存分区均属于虚拟地址空间代码、常量、全局区大小固定栈、堆支持动态伸缩标准寻址链路虚拟地址 → MMU → 页表 → 物理地址页表同时管控内存访问权限Linux依靠task_structmm_struct管理进程保障每个进程拥有独立虚拟空间虚拟内存三大核心价值权限保护、地址规整、软硬件解耦合。