news 2026/5/25 11:12:16

初始虚拟地址

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
初始虚拟地址

一、代码:看见虚拟地址

空谈理论晦涩难懂,我们从一段简单的父子进程代码入手,通过直观的运行现象,发现内存地址的隐藏秘密,这也是理解虚拟地址空间的最佳切入点。

1.1 测试代码

1.2 运行结果与现象

运行现象:父子进程打印的全局变量地址完全一致,但是变量数值不同(子进程修改为100,父进程仍保留初始值0)。

1.3 从现象得出核心结论

单单这一个反常现象,就能推翻固有认知,我们可以得出3个关键结论:

  1. 变量内容不同,说明父子进程操作的不是同一块物理内存,数据相互独立;

  2. 变量地址相同,说明该地址绝对不是物理地址,物理内存不可能出现同地址存不同数据;

  3. Linux下该地址为虚拟地址,我们C/C++代码中打印、使用的所有地址,全部都是虚拟地址;

  4. 物理地址对用户完全透明,全程由操作系统底层管控,用户无法直接操作;

  5. 操作系统必须完成核心工作:虚拟地址 → 物理地址的翻译映射。

1.4 现象底层原理:写时拷贝

为什么地址相同、数据却不同?核心原理是Linux的写时拷贝机制。

fork创建子进程时,操作系统不会直接拷贝父进程的物理内存,为了节省资源,会让父子进程共享同一块物理内存,此时二者虚拟地址完全一致。

当父子进程任意一方尝试修改数据时,操作系统会触发写时拷贝:单独开辟一块新的物理内存,拷贝原有数据并完成修改。修改后,二者虚拟地址保持不变,但映射到了不同的物理内存。这也完美解释了本次代码的反常现象:地址相同、数值不同

图片注解:从上图可以清晰看出,修改前父子进程虚拟地址、物理地址完全共享;修改后,子进程物理内存发生拷贝分离,虚拟地址维持不变,严格遵循写时拷贝机制。

二、内存五大分区(虚拟地址空间划分)

通过上面的代码实验,我们确定了核心前提:日常编码使用的所有地址都是虚拟地址。而我们老生常谈的代码段、栈、堆等内存分区,全部都是操作系统在虚拟地址空间上划分出来的逻辑区域,并非物理内存固有分区。

图片注解:这是标准的32位进程虚拟地址空间分布图,地址从下往上(低地址→高地址)依次排布,分区界限清晰,也是下文讲解的核心依据。

2.1 内存分区排布顺序(高地址 → 低地址)

结合上图视觉排布(图片上方为高地址、下方为低地址),我们按照从高地址到低地址的顺序,自上而下解析每一块分区的作用与特性:

  1. 栈区(Stack):靠近高地址位置,存放局部变量、函数参数、返回地址,系统自动申请释放,内存向下生长,有固定容量上限;

  2. 堆区(Heap):位于栈区下方,由程序员手动malloc申请、free释放,内存向上动态生长,无固定大小上限;

  3. 全局数据区(.data + .bss):存放全局变量、static静态变量,编译阶段确定大小,运行期间不会主动释放;

  4. 常量区(.rodata):存放字符串常量、const修饰的全局常量,硬件层面标记只读,禁止修改;

  5. 代码段(.text):处于整片用户空间最低地址,存放编译后的二进制机器指令,权限为只读,运行期间大小固定,不可扩容。

2.2 栈区之上的区域

结合上面的空间分布图可以发现,栈区并非最高地址边界。在栈区往上的高地址位置,还有两块特殊区域:

  • 内核空间:位于整片虚拟空间最顶部,属于操作系统内核专属内存,用户进程没有权限修改、直接访问。

  • 环境变量&命令行参数区:专门存放程序运行所需的环境变量、命令行参数argv;

在32位系统下,操作系统会给每一个进程,独立分配0~4GB的虚拟地址空间,进程之间空间相互隔离、互不干扰。

2.3 全局区与常量区核心特性

结合分区特性,解答两个疑问:

1. 为什么全局变量、static变量生命周期随进程?

全局数据区(data/bss)属于编译期固定大小区域,进程创建之初,操作系统就会为该区域映射虚拟内存,运行期间不会主动释放、收缩。这片内存永久绑定当前进程,只有进程终止销毁时,操作系统才会统一回收,因此全局、静态变量不会提前销毁,生命周期贯穿整个进程。

2. 为什么常量区数据不能修改?

这并非单纯的C语言语法限制,本质是硬件权限拦截。常量区对应的内存页,操作系统会在页表中标记为只读权限。当程序尝试修改常量数据时,硬件MMU会校验页表权限,权限不匹配直接触发内存异常,程序崩溃报错。

1、语言层面原因

  1. C 语言标准定义:双引号字符串字面量本质是const char[]只读常量,语法层面就规定它是固定不可变数据。
  2. 语法隐式转换漏洞:char*可以接收const char*地址,只是语法兼容妥协,不代表赋予写入权限
  3. 行为定性:修改字符串字面量属于未定义行为,语言标准不保障运行结果,禁止人为修改。
  4. 设计初衷:常量字符串全局共享,程序中多处使用同个字面量只会存一份,一旦修改会全局错乱,语言直接从规则上杜绝这种错误。

2、底层页表 + 操作系统权限层面

  1. 程序编译后,字符串常量统一存放至.rodata 只读数据段
  2. 操作系统通过虚拟内存 + 页表管理进程内存,会给 .rodata 所在的内存页打上只读 (R) 权限,取消写入 (W) 权限。
  3. CPU 内置MMU 内存管理单元,每次读写内存都会查询页表权限:
    • 读数据:权限合法,正常执行;
    • 写入数据:检测到页表无写权限,直接触发硬件异常
  4. 操作系统捕获异常后,直接判定为非法内存访问,抛出段错误,终止进程,从硬件底层锁死写入操作。

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_struct(PCB进程控制块),给进程分配唯一标识,记录进程基础信息;

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,代表映射正常,可以直接访问。 同时页表本身也占用内存,存储在物理内存当中,不会存放在虚拟空间。每一个进程的PCB(task_struct)中,都会记录该进程专属页表的物理起始地址,保证MMU查表时不会错乱,做到进程之间页表相互隔离。

3.3 谁来查表完成地址翻译?

很多人误以为是操作系统查表,实则不然。完成地址翻译、权限校验的是硬件MMU(内存管理单元),硬件直接执行查表操作,无需软件干预,翻译速度极快,不会影响程序运行效率。

3.4 代码数据存磁盘还是内存?

结合虚拟内存映射机制,我们彻底分清磁盘与内存的存储关系,解答常见内存疑问:

  1. 程序未运行:所有代码、常量、全局数据全部存放在磁盘,无内存占用;

  2. 程序运行(进程状态):操作系统采用按需加载机制,不会一次性加载全部数据;

  3. 正在使用的代码数据:加载进物理内存,映射对应虚拟地址;

  4. 暂时未使用的数据:保留在原磁盘文件中,不占用物理内存;

  5. 曾经使用、目前闲置的数据:操作系统自动换入磁盘Swap交换分区,腾出物理内存;

补充知识点:栈变量出作用域销毁、堆内存free释放,不会写入磁盘,仅回收虚拟内存空间,数据直接丢弃,无持久化存储逻辑。

四、Linux进程内存管理结构体

了解完用户层面的虚拟空间、映射原理,我们进一步深入Linux内核。操作系统依靠两个核心结构体,精准管理每一个进程的虚拟地址空间。

4.1 两大核心结构体

  1. task_struct(PCB进程控制块):进程的专属身份证,存放进程ID、运行状态、优先级、寄存器、内存指针等全部进程信息;

  2. mm_struct(内存描述符):专门用来描述进程虚拟地址空间,task_struct内部包含一个指针,指向当前进程专属的mm_struct。

4.2 mm_struct核心作用

mm_struct是虚拟内存的管控核心,核心功能有三点:

  • 独立性:每一个进程独有一份mm_struct,实现进程内存相互隔离、互不干扰;

  • 边界记录:精准记录虚拟地址空间的分区边界,包含代码段、堆、栈的起始与结束地址;

  • 映射管控:维护该进程的所有页表信息,管控虚拟地址与物理地址的映射关系。

4.3 虚拟内存空间划分规则

结合前文配图,以32位Linux系统为例,4GB虚拟地址空间严格划分为两大区域:

  1. 用户空间(0~3GB):面向用户开发,包含代码段、堆、栈、全局区等所有编码可操作的分区,权限开放,用户可自主读写;

  2. 内核空间(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条核心要点:

  1. 通过fork代码现象可证:日常代码打印、使用的地址全部是虚拟地址

  2. C语言五大内存分区均属于虚拟地址空间,代码、常量、全局区大小固定,栈、堆支持动态伸缩;

  3. 标准寻址链路:虚拟地址 → MMU → 页表 → 物理地址,页表同时管控内存访问权限;

  4. Linux依靠task_struct+mm_struct管理进程,保障每个进程拥有独立虚拟空间;

  5. 虚拟内存三大核心价值:权限保护、地址规整、软硬件解耦合。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 16:14:07

3步免费获取百度文库纯净文档:告别广告干扰与付费限制

3步免费获取百度文库纯净文档:告别广告干扰与付费限制 【免费下载链接】baidu-wenku fetch the document for free 项目地址: https://gitcode.com/gh_mirrors/ba/baidu-wenku 百度文库作为国内最大的文档分享平台,为无数用户提供了宝贵的知识资源…

作者头像 李华
网站建设 2026/5/22 16:14:00

炉石传说佣兵战记自动化脚本终极教程:3步解放你的双手

炉石传说佣兵战记自动化脚本终极教程:3步解放你的双手 【免费下载链接】lushi_script This script is to save your time from Mercenaries mode of Hearthstone 项目地址: https://gitcode.com/gh_mirrors/lu/lushi_script 还在为炉石传说佣兵战记的重复操作…

作者头像 李华
网站建设 2026/5/22 16:13:59

YOLOv8 ROS:构建下一代机器人视觉感知的技术栈演进

YOLOv8 ROS:构建下一代机器人视觉感知的技术栈演进 【免费下载链接】yolov8_ros Ultralytics YOLOv8, YOLOv9, YOLOv10, YOLOv11, YOLOv12 for ROS 2 项目地址: https://gitcode.com/gh_mirrors/yo/yolov8_ros 在机器人技术快速发展的2025年,视觉…

作者头像 李华
网站建设 2026/5/22 16:13:58

C语言实战第一篇:简易猜数字游戏实现与拓展

一.项目需求 实现一个控制台版猜数字游戏,具备以下功能: 1.电脑随机生成一个1 ~ 100之间的整数 2.提供主菜单,支持选择“开始游戏”或“退出游戏” 3.处理玩家非法输入(如字母、符号、非法数字),防止程序卡…

作者头像 李华
网站建设 2026/5/22 16:10:55

Blockchain Solutions

Blockchain Solutions Blockchain技术以其独特的去中心化特性和数据不可篡改性,正在成为解决各种行业问题的关键技术。从金融到供应链管理,从医疗保健到版权保护,区块链解决方案正在改变我们处理数据和交易的方式。 在金融领域,区…

作者头像 李华
网站建设 2026/5/22 16:09:43

在ubuntu服务器上快速接入taotoken实现多模型api调用

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 在Ubuntu服务器上快速接入Taotoken实现多模型API调用 对于在Ubuntu服务器上部署应用的开发者而言,快速集成大模型能力是…

作者头像 李华