感性认识一下Linux的进程地址空间和写时拷贝技术

news2025/8/14 4:24:13

虽然本篇文章对操作系统的理解不怎么深入,或者说仅仅是一些皮毛知识(也可能皮毛也算不上),但还是需要读者有一些Linux的基础理解,如何确定是否有这些基础呢?可以参考我的这一篇博客:Linux —— 进程概念超详解!

1.“奇怪”的事

上面给的博客链接提到了环境变量这个概念,环境变量是具有全局属性的一个变量,我们似乎可以推导出子进程可以继承父进程的全局变量(不管这个结论是否正确,我们先暂时这么认为)。那么继承又是通过传参而得来的,传参的方式是传值,意味着子进程拿到的数据是父进程的一份拷贝。在我们C/C++编程中,当两个指针指向同一块空间时,通过任何一个指针对空间内容数据进行了修改,另一个指针看到的空间内容数据也是修改之后的数据。

那么在父子进程上便会发生一件“奇怪”的事,我们可以使用下面的代码来看一看这件事:

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 
  5 int num = 100;
  6 int main()
  7 {
  8     pid_t id = fork();
  9     if(id < 0)
 10     {
 11         printf("创建进程失败!\n");
 12         return 1;
 13     }
 14     else if(id == 0)
 15     {
 16         int cnt=0;
 17         while(1)
 18         {
 19             printf("我是子进程,num=%d,&num=%p\n",num,&num);
 20             sleep(1);
 21             cnt++;
 22             if(cnt == 5)
 23             {
 24                 num = 500;
 25                 printf("子进程把num修改为%d啦!\n",num);                                                                                                                                
 26                 sleep(1);
 27             }
 28         }
 29     }
 30     else
 31     {
 32         while(1)
 33         {
 34             printf("我是父进程,num=%d,&num=%p\n",num,&num);
 35             sleep(2);
 36         }
 37     }
 38     return 0;
 39 }

 编译运行便会看到这样的结果:

在C/C++编程中,我们认为地址是内存单元的唯一标识符, 而观察上图,父子进程的num变量都在同一块空间(因为使用了同一个地址),也就意味着子进程对这个变量值进行了修改,理论上来说父进程的num值也应该随之发生变化,但事实上并非如此。这是什么原因?

事实上对于每一个进程来说,它们所使用的地址都是虚拟地址,这是Linux内核允许使用这种地址的。接下来就该介绍进程地址空间了。

2.进程地址空间

进程地址空间由进程可寻址的虚拟内存组成,每个进程都有一个32位或64位的连续地址空间(空间大小取决于体系结构),例如32位地址空间的地址就可以从0扩展到4294967295。如果这个区间内的每个地址都能表示内存当中一个字节,那么32位的地址空间就有4GB。但是想想不对啊!一个进程的地址空间是4GB,100个进程就是400GB,这怎么可能嘛!确实,这是不可能事,所以进程看到的所谓4GB的内存空间仅仅是操作系统给进程画的饼。那么这些数字在进程的角度来说确实是一个个地址,但是在用户的角度来说,这些数字就是数字,它并没有实际意义(个人拙见,希望大佬能在评论区指正)。这些数字,操作系统把它称为虚拟地址,而这些虚拟地址的集合,又是组成虚拟内存的一部分。

如果操作系统把这些数字(整型)一个个存下来的话,也是不可能的事。所以操作系统有了地址区间这一说法,例如操作系统把地址为2000000~5000000的这个地址区间划分为一个内存区域,而这个内存区域对应一个栈或者是堆或者是静态区,那么操作系统就很好管理它了,操作系统只需要每个内存区域的起始和结束位置,就能找到这块区域的所有地址。

有的读者读到这里不免会产生疑惑,单纯的一个数字区间就能代表一个栈、一个堆了?确实是无法代表,但是操作系统聪明啊!操作系统可以拿这些数字,通过一种特殊的手段,映射到物理内存呀!在这里似乎就能理解了吧?例如我们使用C语言定一个变量:int a = 10;,我们 &a 取到的地址便是一个虚拟地址,当我们想要对这块 int 类型的空间做一些修改的时候,操作系统通过一些手段拿着虚拟地址找到物理地址,有了物理地址不就可以对实实在在的空间进行修改了嘛?这不就顺理成章了嘛?也就是进程使用的地址是操纵系统给画的饼,这些地址在进程看来是指向某一块空间的,但实际上不是,当进程要用这个地址指向的空间时候,操作系统就拿着这个虚拟地址找物理地址,这样就可以骗过进程了。

3.内存描述符结构体

与管理进程如出一辙,管理进程地址空间也需要一个内核结构体,这个结构体称为内存描述符结构体,该结构体包含了和进程地址空间有关的全部信息。内存描述由 mm_struct 结构体表示,根据上面的分析结果,我们看一看内核是如何描述进程地址空间的:

 但是呢,mm_struct 结构体只记录了内存区域的首地址、尾地址而已,并没有详细描述这一块内存区域。那么这个工作是交给谁的呢?交给另一个内核结构体,叫做 vm_area_struct 结构体。

 4.内存描述符放在哪

我们可以自己推理一下, 进程需要的内存是从进程地址空间来的,进程地址空间是用 mm_struct 结构体表示的,那么进程是不是只需要找到 mm_struct 结构体就能找到想要的虚拟地址了?

所以,在进程的进程描述符(pcb)中了存放了内存描述符(mm_struct)的结构体指针。进程只需要有这个指针就能找到进程地址空间。

 5.通过页表查询物理内存

刚才提到了操作系统拿着虚拟地址通过一些特殊手段找到物理地址。这个特殊手段就是页表,但实际上并不全是页表。我们分析一下哈,进程使用的地址是虚拟地址吧?那么cpu能用虚拟地址吗?显然不可以,所以操作系统要想办法通过虚拟地址找到物理地址,这时候MMU(内存单元管理器)就站出来了,它能够完成地址的转换工作,但是就它一个还不太行,它需要一个工具叫做页表,MMU通过页表查询到物理内存,然后MMU就可以完成地址转换的工作了。完成转换之后cpu就拿到物理地址了,就可正常的处理进程的指令。

还记得开篇提到的“奇怪”的事吗?父子进程的拿到的 num 变量的地址都是一样的,那是因为它们拿到的地址都是虚拟地址,子进程修改了值后,父进程没有受到影响,可以说明子进程的页表和父进程的页表映射不一样。 那么为什么子进程的虚拟地址和父进程的虚拟地址一样呢?那是因为操作系统是一个非常懒的家伙!它懒得再为子进程单独创造一个进程地址空间,而是让它使用和父进程地址空间一样的拷贝,甚至页表也懒得修改。那么这里就会有读者觉得不对了,地址空间使用的是相同的就算了,页表还一样,那找到的物理地址不也一样了吗?这就是我们后面要说的写时拷贝技术,不要着急。

6.进程地址空间存在的目的

进程地址空间的存在并不是因为那些大佬想把操作系统搞得复杂,而是因为必须要这么这么设计。如果进程直接访问物理内存,那就会非常的不安全,所以进程在访问内存(虚拟内存)的时候,会通过页表映射物理内存,这个过程页表就会控制进程的行为,把一切不安全的行为在访问物理内存之前杀掉。

这么设计也吻合了当时我们说的进程的独立性,即使父子进程使用的地址空间是一样的,但是只要页表的映射关系不同,它就能映射到不同的物理内存,这样就保证了进程的独立性。

这么设计最大的好处就是统一。让所有的进程通过统一的视角看待内存,这样就方便操作系统的管理了。

总结一下,进程地址空间存在的目的有三个:保证安全、确保独立性、统一视角方便管理

7.“奇怪”变为“不奇怪”

我们现在就可以解释开篇提到的“奇怪”的事了。父进程fork一个子进程,这个子进程使用的地址空间、页表都是直接拷贝父进程的,所以父子进程 &num 得到的地址都是一样的。当子进程没有对 num 的值修改时,因为操作系统很懒,没有改变子进程的页表映射关系,所以父子进程在共用一块物理内存,所以父子进程看到的 num 值都是一样的。当子进程对 num 的值进行修改时,就发生了写入操作,操作系统一看子进程要修改 num 值了,如果不开辟一个新的、独立的物理内存供子进程使用的话,子进程的修改一定会影响父进程,所以这时候操作系统马上更改子进程页表的映射关系,页表就通过新的映射关系就重新在物理内存上发现了一块空间,然后操作系统把共享的物理空间拷贝一份,让子进程在自己拥有的物理内存上做修改。这就是写时拷贝技术

也就是说,当父子进程共享一块物理内存时,如果这时候都只执行只读的操作,那么操作系统是不会另外开辟物理内存的。但是当其中某一个要进行写入时,就会通过之前内核源码提到的主引用计数器,再判断页表的映射关系,来确当前进程是否需要执行写时拷贝——如果主引用计数器的值为3,就说明有三个进程正在使用同一块进程地址空间(都是拷贝的),然后其中某一个进程要进行写入操作时,操作系统就会观察这三个进程的页表,如果此进程的页表与其他进程的页表不同,就不会发生写时拷贝,反之则发生写时拷贝(个人臆想,求大佬在评论区骂醒我)。

8.fork出来的子进程PID在哪

在我刚接触这个知识点的时候我也是一脸懵逼。懵逼在哪呢?父进程fork出来一个子进程,此时父子进程共享同一块物理空间,那子进程的pcb放在哪?一开始我天真的以为它俩的pcb都一样。后面三天不吃饭仔细想了想,写时拷贝技术并没有提到进程的pcb,而进程的定义是程序加载到内存中,然后生成一个pcb被操作系统组织起来。我仔细一想,fork出来的子进程没有被加载到内存吗?加载了!它跟父进程在一块呢!因为加载到了内存,所以生成了pcb,然后就被操作系统组织起来了,因为生成了pcb,所以操作系统赋予了子进程一个PID,然后把PID放在了pcb中。

9.结尾

本篇博客的篇幅不大,所以有些定义、描述可能比较敷衍,但是我确实是认认真真的写了,水平有限嘛!所以呢,这篇博客里面的某些定义啊、理解啊、概念啊可能跟标准定义差了十万八千里,甚至牛头不对马嘴。在这里诚恳的向各位大佬学习。当然了如果是跟我一样的初学者看到了这篇文章,对你有帮助的话,就支持支持一下吧。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/14927.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode 318 周赛

2460. 对数组执行操作 给你一个下标从 0 开始的数组 nums &#xff0c;数组大小为 n &#xff0c;且由 非负 整数组成。 你需要对数组执行 n - 1 步操作&#xff0c;其中第 i 步操作&#xff08;从 0 开始计数&#xff09;要求对 nums 中第 i 个元素执行下述指令&#xff1a;…

阿里 P8 架构师力荐 java 程序员人手一套 116 页 JVM 吊打面试官专属秘籍

只要是 java 程序员&#xff0c;肯定对于 JVM 来说并不陌生&#xff0c;甚至是从熟悉到陌生&#xff0c;为什么这样说呢&#xff1f;因为你看似熟悉的东西&#xff0c;其实对于源码层级了解得少之又少&#xff0c;到头来只有一种陌生的感觉&#xff0c;使用了吗&#xff1f;使用…

技术分享 | 多测试环境的动态伸缩实践

本文将从敏捷研发团队的环境需求与痛点出发&#xff0c;分享如何基于云构建可弹性伸缩的自动化生成式多测试环境&#xff1b;更在经济效益层面&#xff0c;提供了多种成本优化方案&#xff0c;以满足研发团队低成本、高效益的多测试环境运行目标。 一、当前遇到的环境问题 初…

论文阅读笔记 | 三维目标检测——AVOD算法

如有错误&#xff0c;恳请指出。 文章目录1. 背景2. 网络结构3. 实验结果paper&#xff1a;《Joint 3D Proposal Generation and Object Detection from View Aggregation》 1. 背景 AVOD同样是一个two-stage(使用了RPN提取候选框)、anchor-based网络结构。获得较高的召回率对…

【WPF】DiffPlex 文本比对工具

【WPF】DiffPlex 文本比对工具背景关于 DiffPlex准备代码实现效果图源码下载地址背景 现行的文本编辑器大多都具备文本查询的能力&#xff0c;但是并不能直观的告诉用户两段文字的细微差异&#xff0c;所以对比工具在某种情况下&#xff0c;就起到了很便捷的效率。 关于 DiffPl…

D. Extreme Subtraction(差分)

Problem - 1443D - Codeforces 给你一个由n个正整数组成的数组a。 你可以随意使用下面的操作&#xff1a;选择任何一个1≤k≤n的整数&#xff0c;做两件事中的一件。 将数组中的前k个元素递减1。 将数组的最后k个元素递减1。 例如&#xff0c;如果n5&#xff0c;a[3,2,2,1,4]…

【Pytorch with fastai】第 16 章 :训练过程

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

利用jemalloc优化mysql

此方法管理mysql内存也存在一定的弊端&#xff0c;根据自身情况进行选择 优点&#xff1a; jemalloc的确能对内存做一定优化&#xff0c;但是发现并不能解决所有内存碎片问题&#xff0c;只能说有一定缓解作用。 缺点&#xff1a; 使用jemalloc会带来内存增加问题&#xff0…

[附源码]java毕业设计零食销售系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

MySQL8.0 MySQL事务日志、REDO日志、UNDO日志

文章目录学习资料MySQL事务日志REDO日志REDO日志的好处、特点好处特点REDO的组成REDO的整体流程REDO LOG的刷盘策略流程图UNDO日志如何理解UNDO日志UNDO日志的作用作用1&#xff1a;回滚数据作用2&#xff1a;MVCC小结学习资料 【MySQL数据库教程天花板&#xff0c;mysql安装到…

i2c协议的特点是什么,老司机带你深入了解

I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线(一根数据线SDA,一根时钟线SCL)即可在连接于总线上的器件之间传送信息。 在I2C总线中, 各部分器件如下: 主机初始化发送&#xff0c;产生时钟信号和终止发送的器件 从器件被主机寻址的器件 发送器…

浅谈选择示波器时的“5倍法则”

众所周知&#xff0c;选择示波器时经常会用到5倍法则&#xff0c;其实不仅仅是针对带宽&#xff0c;当涉及到快沿信号上升时间测试时&#xff0c;根据上升时间选择示波器也会用到5倍法则。本文将分别对这两种情况下的5倍法则展开讨论&#xff0c;并介绍当考虑示波器和探头构成的…

从结构上浅谈FPGA实现逻辑的原理

FPGA是啥&#xff1f;你要是在百度上一查&#xff0c;多数会搜到什么 Field Programmable Gate Array&#xff0c;现场可编程门整列嘛&#xff0c;但是这句话对咱们新手理解FPGA起到的作用十分有限&#xff0c;其实不单是新手朋友&#xff0c;就连我这个玩了几个月的FPGAer也不…

(三)Logistic回归的梯度下降

一、单个样本的Logistic回归的梯度下降法 在本节中&#xff0c;我们学习如何计算偏导数来实现Logistic回归的梯度下降法。 我们将使用导数流程图来计算梯度。 首先回顾一下Logistic回归的公式 zwTxbz w^TxbzwTxb y^aσ(z)11e−z\widehat{y}a \sigma(z) \frac 1 {1e^{-z}}y​…

会员中心通过AJAX、JSON、PHP、MySql等技术实现注册和登录功能(1+X Web前端开发中级 例题)——初稿

&#x1f4c4;题目要求 阅读下列说明、效果图和代码&#xff0c;进行动态网页开发&#xff0c;补充代码&#xff08;1&#xff09;-&#xff08;30&#xff09;。会员中心&#xff0c;需要先注册后登录&#xff0c;先要求应用HTML、CSS、AJAX、JSON、PHP、MySql等技术实现注册…

反射机制(复习)

反射机制 反射机制定义反射机制的功能反射机制主要的API反射机制演示对 Class 的理解Class实例获取的四种方式Class 对应内存结构说明加载Properties文件的俩种方式调用运行时类的结构调用运行时类的指定属性调用运行时类指定的方法调用运行时类指定的构造器反射的应用&#xf…

技术公开课|深度剖析 Java 的依赖管理,快速生成项目 SBOM清单

背景 近年来软件供应链安全风险涌现&#xff0c;无论是 Fastjson、Log4j 等基础组件的 0day&#xff0c;来源于开源的风险事件不断上升&#xff0c;对于研发以及安全同学来说&#xff0c;都是在不断的摸索建立有效的预防及解决机制&#xff0c;公开课将以风险治理为最终目标、…

Java -- 每日一问:谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

典型回答 所谓隔离级别&#xff08;Isolation Level&#xff09;&#xff0c;就是在数据库事务中&#xff0c;为保证并发数据读写的正确性而提出的定义&#xff0c;它并不是 MySQL 专有的概念&#xff0c;而是源于ANSI/ISO制定的SQL-92标准。 每种关系型数据库都提供了各自特…

(STM32)从零开始的RT-Thread之旅--PWM驱动ST7735调光

上一章&#xff1a; (STM32)从零开始的RT-Thread之旅--SPI驱动ST7735(1) 上一章我们先用SPI读取到了LCD的ID&#xff0c;这一章则是使用PWM调光点亮屏幕&#xff0c;因为测试这块屏幕时&#xff0c;发现直接设置背光引脚为高好像无法点亮&#xff0c;好像必须使用PWM调光&…

信而泰自动化OSPFv2测试小技巧

OSPFv2协议简介 OSPFv2&#xff08;开放式最短路径优先版本2&#xff09;是互联网协议&#xff08;IP&#xff09;网络的路由协议。它使用链路状态路由&#xff08;LSR&#xff09;算法&#xff0c;并且属于在单个自治系统&#xff08;AS&#xff09;内运行的内部网关协议&…