当前位置: 首页 > news >正文

【Linux】探索Linux虚拟地址空间及其管理机制

在这里插入图片描述

Linux相关知识点可以通过点击以下链接进行学习一起加油!
初识指令指令进阶权限管理yum包管理与vim编辑器GCC/G++编译器
make与Makefile自动化构建GDB调试器与Git版本控制工具Linux下进度条冯诺依曼体系与计算机系统架构进程概念与 fork 函数
进程状态与优先级

Linux操作系统的虚拟地址空间在内存管理中起着核心作用,它通过虚拟内存机制有效地隔离进程,优化资源利用,并提高系统的稳定性与安全性。本文将简要探讨Linux虚拟地址空间的结构与管理机制,帮助读者理解虚拟地址到物理地址的映射以及内存管理的关键技术。
程序地址空间

请添加图片描述

Alt

🌈个人主页:是店小二呀
🌈C/C++专栏:C语言\ C++
🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构
🌈Linux专栏: Linux
🌈算法专栏:算法
🌈Mysql专栏:Mysql

🌈你可知:无人扶我青云志 我自踏雪至山巅 请添加图片描述

文章目录

  • 一、程序地址空间
  • 二、Fork遗留问题
    • 【第一个问题】:同一个变量同时接收多个返回值
    • 【第二个问题】:相同地址存储不同数值
    • 总结结论
  • 三、进程地址空间
    • 3.1 地址空间
    • 3.2 地址空间区域划分
    • 3.3 进程地址空间
    • 3.4 如何理解进程地址空间
    • 3.5 为什么要有地址空间
    • 【第一个理由】:虚拟地址空间与页表的重要性
    • 【第二个理由:进程与内存管理模块解耦】
    • 【第三个理由: 拦截非法请求对物理内存进行保护】
  • 四、页表和写时拷贝
    • 4.1 操作系统如何处理错误
    • 4.2 如何理解虚拟地址
    • 4.3 正式解决遗留问题

一、程序地址空间

内存管理相关文章】:

  1. C语言内存管理基础:C语言内存管理基础
  2. C++语言内存管理基础:C++语言内存管理基础

在这里插入图片描述

在这里插入图片描述

内存区域相关作用:

  • 栈又叫堆栈:非静态局部变量、函数参数、返回值等等,栈是向下增长的
  • 内存映射段时高效的I/O映射方式,用于装载一个共享的动态内存库,用户可以使用系统接口创建共享共享内存,做进程间通信
  • 堆用于程序运行时动态内存分配,堆时可以上增长的
  • 数据段:存储全局数据和静态数据
  • 代码段:可执行的代码、只读常量

二、Fork遗留问题

【第一个问题】:同一个变量同时接收多个返回值

主要是由于进程具有独立性,父子进程最初是独立的拷贝,但是存储的初始值相同。两个进程此时运行在不同的内存空间中,变量之间不再共享

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0){ //childprintf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{ //parentprintf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);
return 0;
}

输出结果:

  • parent[2995]: 0 : 0x80497d8
  • child[2995]: 0 : 0x80497d8

从输出结果上来看:父子进程使用该变量地址空间是一样的,为什么会说两个进程此时运行在不同的内存空间中,变量之间不再共享呢?

【第二个问题】:相同地址存储不同数值

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取g_val=100;printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{ //parentsleep(3);printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);
return 0;
}

在这里插入图片描述

输出结果:

  • parent[3046]: 100 : 0x80497d8
  • child[3045]: 0 : 0x80497d8

从输出结果上来看,问题在于:父子进程输出地址是一致的,但是变量内容不一样

我们知道一个工位只能占一个人,意味着同一个物理地址绝对不能存储两个数值,所有我们得出一个结论,这些打印出来的地址绝对不是物理空间,而是虚拟空间

总结结论

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做 虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理OS必须负责将 虚拟地址 转化成 物理地址 。

三、进程地址空间

在这里插入图片描述

当我们学习内存管理时,理解语言层面上的线性地址至关重要。所谓线性地址,并不是直接对应物理内存,而是操作系统通过虚拟地址机制为进程提供的一种连续、统一的访问视图。每个进程拥有独立的虚拟地址空间,它是进程控制块(PCB)的一部分,系统通过页表将虚拟地址映射到实际的物理内存。

由于 PCB 是操作系统管理进程的核心数据结构,必须始终可访问,因此它被存储在物理内存的特定区域,而不是普通用户进程的虚拟空间中。

全局变量等初始化数据虽然在程序中表现为连续的内存地址,其实也是通过虚拟地址提供的线性访问方式。
当使用 fork 创建子进程时,系统会复制父进程的虚拟地址空间。但在实际物理内存层面,并不会立即复制所有数据,而是采用**写时拷贝(Copy-On-Write)**机制:只有在子进程尝试修改数据时,才为其分配新的物理内存,并更新页表以指向新的地址,从而确保父进程的地址空间不被影响。

通过这一机制,父子进程既能运行在独立的虚拟地址空间中,又能高效共享未修改的物理数据资源,实现性能与隔离性的平衡。

3.1 地址空间

在 32 位和 64 位系统中,地址空间的范围是不同的。为了统一演示,以下内容将以 32 位系统为例进行讲解。

在 32 位系统中,地址空间的范围为 [0, 2^32),即从 0 到 2 的 32 次方减 1,总共可表示 4GB 的地址空间。这意味着 CPU 最多可以寻址 4GB 的内存空间。

在这样的架构下,地址总线和数据总线通常都是 32 位。
其中,地址总线负责向内存传递“我要访问哪个地址”的信息,它通过 高低电平信号(1 表示高电平,0 表示低电平) 来编码地址。内存接收到这些信号后,会识别所需地址,并将该地址对应的数据通过数据总线,以相同的方式传回给 CPU

这一过程是 CPU 与内存之间进行数据交换的基础,也是理解底层内存访问的重要起点。

在这里插入图片描述

3.2 地址空间区域划分

场景】:假设一张桌子长 100cm,小胖和小美坐在两边。由于小胖经常骚扰小美,于是他们在桌子中间画了一条“三八线”,各占 50cm 的空间。

这就像他们将地址空间划分为两部分,各自拥有独立的使用区域。从结构上来看,这种划分方式就像地址空间的物理分段——虽然整体是一块连续的资源,但通过约定,形成了逻辑上的独立区域,互不干扰。

在这里插入图片描述

地址空间的区域划分,实际上是通过结构体中的 startend 成员来实现的。每个区域由这两个边界值定义,其大小和位置的变化,本质上就是修改结构体中 startend 的值

我们在关注地址空间范围的同时,更要理解:在这段连续的空间中,每一个最小单位(通常是一个字节)都有唯一的地址,而且这个地址是可以被直接访问和使用的。

在这里插入图片描述

3.3 进程地址空间

所谓进程地址空间,本质上是对进程在内存中可见区域的抽象与描述。它由操作系统通过一系列区域划分(如代码段、数据段、堆、栈等)进行管理,这些区域通常由 startend 两个边界标识。

在这里插入图片描述

进程地址空间其实就是一个内核级的数据结构,类似于进程控制块(PCB)。它不仅描述了一个进程的内存使用范围,也为系统管理进程资源提供了组织依据——先描述,再管理

每个进程都拥有自己独立的地址空间,而 PCB 中则包含一个指向该地址空间的指针,从而实现操作系统对进程内存的统一管理和调度。

3.4 如何理解进程地址空间

场景】:可以把操作系统比作一个拥有 10 亿资产的大富翁,而它有四个彼此不知情的“私生子”(即四个进程)。每个进程都被告知:“这 10 亿都是你的,将来全部留给你。” 于是每个进程都相信自己是唯一的继承人,拥有完整的资源。

实际上,操作系统只是给每个进程画了一个“看起来拥有 10 亿的饼”(也就是一个独立的进程地址空间),每个进程都认为自己独占了整块内存空间。

但真实的情况是,只有当进程真正“花钱”(即访问或使用某段内存)时,操作系统才会分配对应的物理资源。这就是虚拟内存管理的基本思想 —— 看起来每个进程都拥有整个世界,实际上只按需分配内存资源

3.5 为什么要有地址空间

在没有 虚拟地址空间 的情况下,可执行程序需要直接加载到 物理内存 中。这意味着操作系统需要在特定位置为程序的代码和数据分配内存,并通过 进程控制块(PCB) 来记录这些位置。这种方式较为混乱,尤其是当多个进程加载时,每个进程的代码和数据可能会交错在物理内存中,导致内存管理变得复杂,难以统一调整。

直接使用 物理地址 的方式对于进程来说是高风险的,可能会导致 访问越界、误修改其他进程的数据和代码等问题。实际的物理内存中,代码区数据区堆区共享区命令行参数环境变量 可能会被杂乱地存储在内存的不同位置,增加了管理的难度。

【第一个理由】:虚拟地址空间与页表的重要性

虚拟地址空间页表 的引入可以将这些混乱的物理内存区域变得有序,进程可以以一致的 虚拟地址 视图来访问自己的代码、数据和堆栈区域。操作系统将 虚拟地址 映射到 物理地址,进程不需要关心数据的实际存储位置,而是可以以 线性 方式访问所有内存区域。

【第二个理由:进程与内存管理模块解耦】

当进程申请 堆内存 时,操作系统不需要立即为其分配 物理内存。因为堆内存不一定会马上使用,如果一开始就为其分配物理内存,可能导致内存浪费。通过 虚拟地址空间,操作系统可以先为进程分配 虚拟地址,而在实际使用内存时再进行 物理内存 分配。这种方式提高了内存的利用率,避免了在进程尚未使用内存时就占用实际物理资源。

这样,虚拟内存 不仅优化了内存的使用效率,还使得进程与内存的管理更加灵活和安全。

在这里插入图片描述

从进程的角度来看,当你申请内存时,实际上你只需要在 虚拟地址空间 上进行申请就可以了。什么叫做在虚拟地址空间上申请呢?就是说,只要你在虚拟地址空间中发出申请,并且该虚拟地址有对应的页表条目,就可以继续进行操作。至于物理内存的具体位置,右侧的映射可以暂时不填。当你真正需要使用内存时,操作系统会根据需求为你分配物理内存。

【第三个理由: 拦截非法请求对物理内存进行保护】

当进程试图访问越界的内存时,操作系统会检查访问请求。如果该虚拟地址没有有效的映射关系(即没有相应的物理内存),操作系统会 拦截请求。这样,进程就无法访问未经授权的内存区域,防止了非法的内存写入,从而保护了其他进程的数据不被影响。

四、页表和写时拷贝

在这里插入图片描述

你只需要知道,CPU 内部有一个 内存管理单元(MMU),它能够帮助 CPU 直接找到当前进程的虚拟地址对应的物理地址。进程的 页表 中包含了很多标记位,这些标记位用于表示在物理内存中的 rwx(读、写、执行)权限

如果某个内存页不在物理内存中,这意味着该代码或数据已经被 换出 到外部存储(如硬盘)上。此时,操作系统会使用相关的 换入换出列表,来支持对内存页的挂起与恢复,确保程序可以在需要时访问到正确的数据。

问题】:字符常量区不能被修改

在这里插入图片描述

字符常量区之所以不能被修改,是因为 页表 对内存进行映射,并且在其中进行 权限管理。如果你没有相应的执行(x)权限,尝试对该区域进行写操作时,操作系统会立即 终止进程,并且阻止任何进一步的转换。也就是说,进程在崩溃时,实际上并没有成功地将任何数据写入物理内存,这也是为什么在 Linux 或 Windows 中,程序崩溃不会影响其他进程或操作系统,因为它们没有执行写操作。

4.1 操作系统如何处理错误

  1. 是否是数据不在物理内存?如果是 缺页中断,操作系统会重新开辟空间,通常情况会正常处理。
  2. 是否需要写时拷贝?如果是,则会发生 写时拷贝
  3. 如果以上都不是,操作系统才会进行 异常处理

4.2 如何理解虚拟地址

虚拟地址空间其实就是操作系统内核中的一个 结构体,它包含了不同的内存区域划分。在操作系统加载进程时,会帮助构建该进程的 页表,页表记录了虚拟地址与物理内存的映射关系。

虚拟地址是从哪里来的?程序本身就有一组地址,这些地址对应着程序中的变量、函数等。比如,一个函数并不是单一的代码块,它的地址就是虚拟地址(逻辑地址)。操作系统在加载可执行程序时,会根据程序中的信息为其创建虚拟地址空间,并通过页表来进行地址的映射。

4.3 正式解决遗留问题

这是因为虚拟地址相同,但对应的 物理内存 内容不同。由于操作系统使用虚拟地址来管理内存,不同的进程可能会看到相同的虚拟地址,但实际访问到的物理内存却是不同的,这就是为什么一个虚拟地址在不同的进程中可以表示不同的物理内容。

在这里插入图片描述

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是Linux笔记,希望对你在学习Linux旅途中有所帮助!

http://www.dtcms.com/a/332791.html

相关文章:

  • C# HangFire的使用
  • 概率论基础教程第2章概率论公理(习题和解答)
  • 在 Linux 服务器搭建Coturn即ICE/TURN/STUN实现P2P(点对点)直连
  • HarmonyOS 实战:用 @Observed + @ObjectLink 玩转多组件实时数据更新
  • pyecharts可视化图表-pie:从入门到精通(进阶篇)
  • Python 数据可视化:柱状图/热力图绘制实例解析
  • 概率论基础教程第2章概率论公理
  • 享元模式C++
  • 基于深度学习的零件缺陷识别方法研究(LW+源码+讲解+部署)
  • 力扣hot100 | 普通数组 | 53. 最大子数组和、56. 合并区间、189. 轮转数组、238. 除自身以外数组的乘积、41. 缺失的第一个正数
  • 什么才是真正的白盒测试?
  • 专题三_二分_x 的平方根
  • JavaScript 解析 Modbus 响应数据的实现方法
  • 记录处理:Caused by: java.lang.UnsatisfiedLinkError
  • MARCONet++ 攻克中文文本图像超分难题
  • 疯狂星期四文案网第40天运营日记
  • Web 开发 15
  • Transformer实战(11)——从零开始构建GPT模型
  • required a bean of type ‘com.example.dao.StudentDao‘ that could not be found
  • (Arxiv-2025)Stand-In:一种轻量化、即插即用的身份控制方法用于视频生成
  • All Document Reader:一站式文档阅读解决方案
  • LT6911GXD,HD-DVI2.1/DP1.4a/Type-C 转 Dual-port MIPI/LVDS with Audio 带音频
  • 【C++】缺省参数
  • Vue3中的ref与reactive全面解析:如何正确选择响应式声明方式
  • 采购招标周期从2月缩至3周?8Manage招标系统实战案例分享
  • 社区物业HCommunity本地部署二开与使用
  • 我的学习认知、高效方法与知识积累笔记
  • JAVA 关键字
  • Redis 官方提供免费的 30 MB 云数据库
  • 【机器人】人形机器人“百机大战”:2025年产业革命的烽火与技术前沿