Linux虚拟地址空间

2023-11-14

父子进程地址相同的变量值不同问题

  #include<stdio.h>
#include<unistd.h>
int g_val = 100;


int main()
{
        pid_t id = fork();
        
    
	if (id == 0)
	{
		// 子进程
		int i = 0;
		while (1)
		{
			printf("I am child process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
			i++;
			if (i == 5)
			{
				g_val = 200;
				printf("Child process changed g_val success!!!\n");
			}
			sleep(1);
		}
	}
	else {
		while (1)
		{
			printf("I am parent process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
			sleep(1);
		}
	}
	return 0;
}

运行结果

[yzl@VM-4-5-centos tmp]$ ./proc
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
Child process changed g_val success!!!
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:200, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:200, &g_val:0x601054

上述代码为,创建子进程,若干秒后,子进程改变全局变量值,发现子进程与父进程打印此全局变量值时,值不同,且地址相同

同一个地址处的值在同一时刻不可能不同,于是引出了虚拟地址空间的概念。即这里子进程与父进程打印的地址并非实际的物理地址,而是一种虚拟地址(线性地址)。

Linux下进程虚拟地址空间分布

虚拟地址空间使得每个进程看待内存时都有一个统一的视角,并且在他们看来,内存的分布是井然有序的。具体分布如下图

img

  1. 栈堆相向增长,堆向高地址增长,栈向低地址增长。这两个区域是动态变化的。

  2. 虚拟地址空间分为两个空间:1. 内核空间,在32位下占1G 2. 用户空间,在32位下占3G即[0, 3GB] 用户空间 [3GB, 4GB] 内核空间

  3. static修饰局部变量,本质上是将此变量属性变为全局属性,存储在全局区。而语法的限制使得此static变量仅能在局部可见。

  4. 上图虚拟内存分布仅适用于Linux操作系统,不适于Windows。

  5. 一个有关堆区的知识:当C语言使用malloc函数时,申请10字节空间,实际在内存中会占用大于10字节的空间,多出的空间用于存储一些属性。这也是为什么free时传首地址即可,而不需要传空间大小。

什么是虚拟地址空间?

  1. 虚拟地址空间(进程地址空间)在操作系统内核中是一个数据结构,在Linux内核中,就是一个struct结构体

  2. 在Linux下,进程地址空间是一个名为mm_struct的结构体,主要存储各区域(堆,栈,全局数据区,只读代码区等)的范围,即start和end,用于划分各个区域。

  3. 页表是和虚拟地址空间结构体配套的内核数据结构,页表的作用是:保存对应进程中每一个虚拟地址到物理内存中的物理地址的映射关系。即起一个映射配对的作用。 因为实际上数据,代码,变量等肯定最终要存储在物理内存中。

  4. 页表起映射作用,就类似于C++中的map数据结构,key 是虚拟地址, value是对应的物理地址

  5. 每一个进程都有一份地址空间结构体变量mm_struct和页表实例化对象。在磁盘中的二进制可执行程序加载到内存中时,要创建对应的PCB结构体,同时,也会创建对应的地址空间结构体变量和页表。

进程直接访问物理内存(无虚拟空间)

在早期计算机操作系统内部,进程直接访问物理内存。这样做有很多弊端。在说弊端之前,有一个点需要明确:内存本身是可以随意读写的,物理内存是不存在只读的情况的。

比如,最典型的野指针问题,一个进程的野指针很容易破坏其他进程,甚至影响操作系统内的安全数据。其次,进程直接访问物理内存,使得进程和物理内存耦合度很大,内存管理变得不方便,从而内存碎片等问题也变得更难处理。

基于进程直接访问物理内存的弊端,衍生出虚拟地址空间

再述虚拟地址空间!

img

程PCB,虚拟地址空间,页表,物理内存的关系大致如上图所示。

  1. PCB中有一个struct mm_struct* mm指针数据成员指向这个进程对应的mm_struct

img

  1. 因为内存本身是随意读写的,所以,在地址空间+页表的作用下可以在某些虚拟地址与物理地址的映射关系中,用某些数据(比如页表中存储)表明这个内存是只读的。以此来保护某些数据。这些都是地址空间+页表的作用,而非使用内存的权限控制。

  2. 基于第二点,地址空间+页表可以对某些内存进行权限管理,比如常量代码区设为只读。同时,对于某些内存的非法访问,也可以及时禁止。从而保护物理内存。

  3. 我们知道,进程是具有独立性的,那么,在地址空间+页表的作用下,只要使得各个进程的虚拟地址通过页表映射的物理内存是不同的,则可以保证进程之间互不干扰,即进程独立性。

虚拟地址空间结构体是如何区域划分?

通过定义栈区,堆区,常量代码区,全局数据区等区域的start,end。来对这些区域进行划分。

比如栈区,堆区是动态变化的。那么只需要增大或者减小end,即可对栈区堆区的空间大小进行控制。再比如只读代码区,在源文件编译之后,可执行程序内部已经有了虚拟地址。若此文件加载到内存中变为进程,则mm_struct中的常量代码区的start和end即可通过这些编译生成的虚拟地址来确定start和end

Linux内核源码
img

如图,为Linux内核源码中mm_struct的定义,即虚拟地址空间的定义。可以看到,它确实是通过定义各个区域的start,end来划分各个区域的。类型是unsigned long

解答最初的问题

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int g_val = 100;
  4 
  5 int main()
  6 {
  7     pid_t id = fork();
  8     if(id == 0)
  9     {
 10         // 子进程
 11         int i = 0;
 12         while(1)
 13         {
 14             printf("I am child process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
 15             i++;
 16             if(i == 5)
 17             {
 18                 g_val = 200;
 19                 printf("Child process changed g_val success!!!\n");
 20             }
 21             sleep(1);                                                                                                                                                                                        
 22         }
 23     }
 24     else {
 25         while(1)
 26         {
 27             printf("I am parent process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
 28             sleep(1);
 29         }
 30     }
 31     return 0;
 32 }

img

最初,父进程与子进程打印出同一个变量地址相同,但是值不同。我们现在知道了,这个地址其实是虚拟地址,而非物理地址。

一个事实:父进程创建子进程时,除了一些子进程独有的属性,比如典型的pid。其余大部分属性和数据都是从父进程那里拷贝过来的。包括mm_struct 和 页表。

所以,起初,在子进程执行g_val = 200;之前,也就是修改这个全局变量之前。因为子进程的mm_struct和页表是直接从父进程那里拷贝过来的。故父子进程的g_val的虚拟地址,以及这个虚拟地址映射的物理内存中的数据都是一样的

这样做的原因是:如果有某些数据,父子进程都是只读的,也就是不会修改,那么这份数据在内存中只保存一份即可,没必要给子进程在内存中再创建一份相同的,只读的数据。(写时拷贝)

而当子进程执行g_val = 200;时,这是子进程对这个全局数据执行写操作。因为父子进程访问的g_val不应该互相干扰。故此时,OS在内存中的其他区域,拷贝了一个新的,子进程的g_val,赋值为200,并改变子进程的页表的映射关系即可!(不需要改变g_val的虚拟地址)。

从而当子进程修改g_val后,父子进程打印的这个全局数据的虚拟地址相同但是映射到物理内存不同区域值不相同。才有了最初的现象。

这种子进程写数据时进行拷贝的操作,称为写时拷贝!

延伸问题: 一个pid变量怎么可能保存不同的值?

pid_t pid = fork();

我们知道,fork函数内部的主体逻辑就是创建子进程,而当fork函数return之前,则子进程已经创建好了

所以有了两个进程执行流,两个执行流会执行两次return其次,return了两次给pid赋值,也就是子进程执行流的return给pid本质就是对pid变量进行写操作!会发生写时拷贝,那么各自就有各自的pid了(虽然虚拟地址相同,页表映射到物理内存是不同的,看到的是自己的pid变量)

fork return两次,第二次return发生了写时拷贝,则父子进程各自在物理内存中,都有属于自己的id变量空间!

只不过在用户层用同一个变量pid(虚拟地址)来标识了。

为什么存在虚拟地址空间?(虚拟地址空间的好处)

保护物理内存

  • 凡是非法的访问或者映射,OS都会识别到,并终止你的进程。(页表机制会将虚拟地址空间会划分为多个页,通过设置页表项的权限位答到保护物理敏感内存的目的)

原因就是:比如const char* p = “abcd”; p指针保存的是虚拟地址,且这个虚拟地址所映射的物理内存不可被写,这都是基于虚拟地址空间(代码段不能修改等等)+页表的作用。

除了这种保护只读的数据,当存在野指针或者非法访问时,虚拟地址空间+页表也能以某种方式告诉OS,从而OS可以发信号终止这个进程。

样一来,物理内存的访问都在OS的监管之下。保护了物理内存,物理内存中的数据,其他进程,以及内核的相关有效数据

内存管理和进程管理低耦合

因为有虚拟虚拟地址空间+页表物理内存中的数据可以随意存储。只要保证虚拟地址可以通过映射找到对应的数据即可

物理内存管理进程管理因此可以做到关联性很低

内存管理模块和进程管理模块 完成了解耦合。在操作系统层面,这两个模块关联性很低,维护成本也会降低(各维护各的);

延时分配,提高整机效率

  • 我们在C语言中进行malloc时,申请内存本质是在虚拟地址空间中申请,并不会立即向物理申请内存空间

原因是:如果我malloc时就立刻申请物理内存,且不立刻使用,则这就是一种内存资源浪费

所以,因为有地址空间存在,上层申请内存,其实是在虚拟地址空间中申请。

而当你进行对物理内存的访问时,才执行相关的内存管理算法(缺页中断等),帮你申请内存,构建页表映射关系。然后再让你进行内存访问。 (这些是由操作系统完成的,进程0感知)

虚拟地址存在,但是物理内存中没有对应的空间。称为缺页(映射)中断。

那么,这样延时分配的好处就是:确保物理内存中的有效使用是100%的不会出现物理内存中申请空间但不使用的情况。提高整机效率。

使内存分布有序化,实现进程独立性

  • 地址空间+页表的存在,可以使得内存分布有序化!每个进程看到的是完整的虚拟地址->实现进程独立;

在虚拟地址中,每个进程以完整虚拟地址的角度来看待内存布局,这个布局是有序的(代码段,数据段,堆栈等分区);

因为页表的存在,可以建立虚拟地址和物理地址的映射关系,物理内存存放的数据是随机无序的。

通过让不同进程看到的完整分布的内存虚拟地址 映射 到物理内存的任意区域 -> 实现进程独立性

内存扩展(物理内存不够时,通过内存页和硬盘的交换达到扩展(了解)

内存共享(允许多进程通过虚拟地址+页表等机制,访问同一个物理内存(eg:fork的写时拷贝)(了解)

小结

综上:

可以说虚拟地址空间是OS内核中的一种数据结构主要保存各个数据区的start和end

32位系统下,虚拟地址空间使得每个进程都认为自己独占4GB内存,它们也看不到其他进程的存在。内核通过页面的映射等管理手段,从而让物理内存中的进程和进程之间,进程和内核之间可以互不干扰;

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux虚拟地址空间 的相关文章

随机推荐

  • 分苹果问题

    题目大意 有N个苹果 要把这些苹果粉给2个人 使得这两个人得到的苹果重量差最小 先求得N个苹果的重量总和 分成两堆 差值最小 则有一堆大于或等于SUM 2 有一堆小于等于SUM 2 所以有for j sum 2 j gt w i j 只要d
  • error C1076: compiler limit: internal heap limit reached 【UE4出现C1076错误的解决方法】

    如果编译后出现以下问题 导致这个问题的原因是 预分配 头内存不足 可以通过 Zm114 多分配一些
  • docker下交叉编译环境配置

    为什么在docker中搭建开发环境 Docker 是一个开源的应用容器引擎 让开发者可以打包他们的应用以及依赖包到一个可移植的容器中 然后发布到任何流行的 Linux 机器上 也可以实现虚拟化 容器是完全使用沙箱机制 相互之间不会有任何接口
  • matlab数学实验 课件,MATLAB数学实验课件.PPT

    摘要 第一章 Matlab入门 MATLAB数学实验 第八章 随机模拟和统计分析 第八章 随机模拟和统计分析 8 1 预备知识 概率和统计 8 2 概率和统计的MATLAB指令 8 3 计算实验 随机模拟 Monte Carlo算法 8 4
  • [Koishi] 实现简易QQ机器人

    以前使用的QQ机器人是千寻Bot为基础框架的 配置环境相较于Koishi复杂得多 在此记录一下使用Koishi的踩坑过程 目录 1 软件下载与安装 1 1下载 1 2安装 2 插件 2 1插件安装 2 2插件更新 2 3插件配置 2 3 1
  • python从MySQL数据库中读取数据

    import pymysql 连接数据库 link pymysql connect host 127 0 0 1 连接地址 连接本地默认 127 0 0 1 user root 用户名 passwd 密码 port 3306 端口 默认为3
  • CPU数据预取对软件性能的影响

    一 什么是预取 预取是指将内存中的指令和数据提前存放到cache L1 L2 L3 中 从而加快处理器执行速度 Cache预取可以通过硬件或者软件实现 也就是分为硬件预取和软件预取两类 硬件预取 是通过处理器中专门的硬件来实现的 该硬件监控
  • 除了 :还有哪些空白符实体(转载)

    除了 nbsp 还有哪些空白符实体
  • CentOS7安装OpenLDAP+MySQL+PHPLDAPadmin(本人测试通过)

    转载自 http www cnblogs com bigbrotherer p 7251372 html 安装环境 CentOS 7 1 安装和设置数据库 在CentOS7下 默认安装的数据库为MariaDB 属于MySQL数据库的一个分支
  • jlink烧录,多个jlink同时烧录多个板子

    查看jlink的序列号 S N 如下是59408473 然后使用JFlashSPI exe打开之后 填写进去这个序列号 最后保存为jflash文件
  • 密码学原语如何应用?解析单向哈希的妙用|第9论

    作者 廖飞强 来源 微众银行区块链 隐私数据如何验明真伪 区块链数据何以可信 如何快速检验海量数据是否被篡改 单向哈希在其中起到了什么作用 隐私数据的价值很大程度上源自其真实性 如何防止数据被恶意篡改 是隐私保护方案设计中不可忽视的关键目标
  • springboot整合shiro-登录失败次数限制(八)

    原文地址 转载请注明出处 https blog csdn net qq 34021712 article details 80461177 王赛超 这次讲讲如何限制用户登录尝试次数 防止坏人多次尝试 恶意暴力破解密码的情况出现 要限制用户登
  • sqli-labs靶场15-16关(基于POST时间盲注)

    第十五关 sqlmap方法 python2 sqlmap py u http 127 0 0 1 sqlilabs Less 15 id 1 data uname admin passwd 1 submit Submit current d
  • 快速解决数据库连接失败

    无法连接到数据库 以Microsoft SQL Server Management Studio为例 以下简称 SSMS 显示下图连接失败界面 处理方法 打开SQL Server配置管理器 会出现如图服务器状态关闭 此时只需重新启动服务状态
  • RFID标签技术变成仓储物流物流关键技术

    伴随着各种各样电子商务的全方位普及化和市场需求的愈来愈猛烈 怎样完成节约成本 提升工作效能变成持续提升的总体目标 根据创建物流仓储物流运行的自动化技术 信息化 智能化系统 RFID技术变成与公司信息化管理体系的无缝拼接的关键公路桥梁 保证R
  • SpringMVC拦截器(资源和权限管理)

    原文地址 http blog csdn net tonytfjing article details 39207551 1 DispatcherServlet SpringMVC具有统一的入口DispatcherServlet 所有的请求都
  • 大数据技术Flink详解

    一 有状态的流式处理 Apache Flink 是一个分布式流处理器 具有直观和富有表现力的API 可实现有状态的流处理应用程序 它以容错的方式有效地大规模运行这些应用程序 Flink 于2014 年4 月加入Apache 软件基金会作为孵
  • 基于BSC测试网收益聚合器Beefy协议的编译、测试、部署

    前言 文章主要介绍了收益聚合器Beefy协议在币安智能链测试网网上的编译测试部署流程 以Pancake上的USDC BUSD最新Curve版流动池的农场质押为例 详细介绍了完整的操作流程 准备工作 Node js环境 https nodej
  • 在Struts2里面嵌入Spring

    第一步 在web xml中加入下面的listener
  • Linux虚拟地址空间

    目录 父子进程地址相同的变量值不同问题 运行结果 Linux下进程虚拟地址空间分布 什么是虚拟地址空间 进程直接访问物理内存 无虚拟空间 再述虚拟地址空间 虚拟地址空间结构体是如何区域划分 解答最初的问题 延伸问题 一个pid变量怎么可能保