尝试带你理解 - 进程地址空间,写时拷贝

序言

 在上一篇文章 进程概念以及进程状态,我们提到了 fork 函数,该函数可以帮我们创建一个子进程。在使用 fork 函数时,我们会发现一些奇怪的现象,举个栗子:

  1 #include <stdio.h>2 #include <unistd.h>3 4 int main(){5     int val = 1;6     pid_t pid = fork();7 8     // 创建子进程失败9     if(pid < 0){10         printf("Failed to create child process.");11         return 1;12     }13     // 子进程14     else if(pid == 0){15         val += 1;16         printf("I am child process, pid is %d, val = %d, &val = %p.\n", getpid(), val, &val);17     }18     // 父进程19     else{                                                                                                                                                                                                     20         printf("I am parent process, pid is %d, val = %d, &val = %p.\n", getpid(), val, &val);21     }22 23     return 0;24 }

注意:这里我为了简化代码以便大家更为方便的理解,并没有回收子进程,这是不规范的,会导致僵尸进程的产生QAQ。

运行上段代码,我们得到以下的输出结果:

I am parent process, pid is 4092, val = 1, &val = 0x7ffdde232328.
I am child process, pid is 4093, val = 2, &val = 0x7ffdde232328.

我们在上篇文章中也提到了,fork 函数会返回两个返回值,这对于我们来说简直就是不符合常识呀!

通过程序的运行结果,我们得出以下疑问:

  • 为什么 fork 函数会返回两个返回值?
  • 为什么程序走了 if else 条件控制结构的两个分支?
  • 为什么同一个存储 val 的地址,存储的数值还不同?

1. 进程地址空间

 首先我们肯定的是同一个物理地址只能存储一个值,不可能存储多个值。但是上面变量的地址确实是相同的呀?这又怎么解释呢?有没有可能我们打印出的地址不是真正的物理地址,那是什么呢?虚拟地址。

1.1 物理内存空间

 物理内存可以根据操作系统和程序的需要,按照存储的数据类型和用途来划分为不同的段。以下是按照存储的数据类型进行的分类:
在这里插入图片描述
在创建一个进程时,操作系统通常会为该进程分配以上几种数据段,这些段共同构成了进程的空间。可以发现这些进程空间在物理上是并不连续的,当系统正常的运行时,每时每刻会产生大量的进程完成各项任务,内存管理复杂性是相当高的,所以说我们肯定不会直接在物理内存上进行操作。

1.2 虚拟地址空间

 大家在一定都看过这幅描述进程地址空间的图像吧:
在这里插入图片描述
大家到这里肯定会有疑问,你刚才才说进程的空间在物理内存上是分散的,不连续的,那你为什么还给出这幅图呢?这确实不是在物理内存上的图像,这是我们的虚拟地址空间,它包含了进程执行所需要的所有代码、数据、堆栈等信息。
一个程序的虚拟地址空间是连续的,这大大地简化了我们的内存管理。 但是我们的数据最终肯定是在物理内存上,那和这个虚拟地址空间有什么关系呢?

1.3 页表 — 连通物理和虚拟的桥梁

定义

页表是一种数据结构,通过页表,操作系统能够将程序中的虚拟地址转换为实际的物理地址,从而实现内存的访问。

作用

  • 地址映射:页表实现了虚拟地址到物理地址的映射,使得程序可以使用连续的虚拟地址空间来访问物理内存中可能不连续的内存块。
  • 内存保护:页表还包含了每页的访问权限信息(如可读、可写、可执行等),从而实现对内存访问的控制,提高系统的安全性。

综上所述,通过页表我们可以将在虚拟内存上的操作映射到物理内存上,并且还会提前预警不合法的访问修改操作,确保进程的正确执行和系统的稳定运行。

1.4 进程地址空间的优点

  • 有序化空间地址:将物理地址空间将无序变为有序,让进程以统一的视角看待物理内存以及自己的各个区域
  • 进程隔离:每个进程都拥有自己独立的地址空间,这确保了不同进程之间的内存是相互隔离的。
  • 读写保护:进程地址空间通过页表等机制实现了对内存访问权限的控制。操作系统可以设定页表的权限字段,如只读、可写等,从而限制进程对内存的访问方式。
  • 提高内存使用效率:当我们使用 malloc 等等库函数动态申请空间,却并没有立即使用时,会在页表先登记申请的虚拟空间地址,不立即在物理内存的堆上申请,等到你要使用时再申请

1.5 进程地址空间和总结

 进程地址空间中的虚拟地址通过操作系统的地址转换机制(如页表)映射到物理地址。当程序试图访问某个虚拟地址时,操作系统会查找页表,将虚拟地址转换为对应的物理地址,然后访问物理内存中的数据。


2. fork 函数背后的逻辑

2.1 进程复制

  • 复制进程fork 函数会复制当前进程的上下文来创建一个新的进程。这个复制过程包括进程的 PCB 内的部分信息、虚拟地址空间(写时复制)、页表、文件描述符、环境变量等。
  • 共享与独立:虽然子进程是父进程的复制品,但两者在操作系统中被视为独立的进程,拥有各自的进程 ID(PID)。它们各自独立地执行程序,且可以通过不同的返回值来区分是父进程还是子进程。

2.2 返回值

  • 父进程中的返回值:在父进程中,fork 函数会返回新创建的子进程的PID(一个正整数)。
  • 子进程中的返回值:在子进程中,fork 函数返回 0 。如果 fork 调用失败,则在父进程中返回 -1,并设置 errno 以指示错误原因。

2.3 执行起点

子进程的执行起点:子进程的执行起点是从 fork 调用之后的那条指令开始的。这意味着 fork 在这里插入代码片调用之前的所有变量赋值、文件操作等都会被子进程继承。


3. 虚拟地址中的写时复制

3.1 使用场景

 上面提到当我们创建一个子进程的时候,子进程会直接复制父进程的虚拟地址空间,页表等信息,很明显直接复制这是一个浅拷贝:
在这里插入图片描述
就比如,在上附图中,父进程一个变量 A,他的页表信息也被子进程所直接复制。

当我们的子进程想要修改该变量 A 的值时,如果不进行额外处理,那么父进程的值也会改变,这违背了 父子进程相互独立 规则。所以,会为子进程重新申请一个新的空间,放置修改后 A 的值,这也会使子进程页表中对应的物理地址发生改变。

3.2 写时拷贝的优点

  1. 减少不必要的资源分配,不需要为不用修改的变量申请空间
  2. 提高复制效率,在写时拷贝机制下,资源的复制操作被延迟到实际需要修改资源内容时进行。这种懒惰复制的方式减少了在资源初始分配时的开销,提高了系统的整体效率。

4. 解释开头的疑问

为什么 fork 函数会返回两个返回值?

 使用 fork 函数后,会变为两个进程,一个父进程,一个子进程,两个进程中接收的返回值是不同的,父函数接收的为子进程的 pid,子进程接收的为 0

为什么程序走了 if else 条件控制结构的两个分支?

 不是同一个进程同时走两个分支,是两个进程走各自的分支。


为什么同一个存储 val 的地址,存储的数值还不同?

 子进程直接拷贝了父进程的进程地址空间和页表内的信息,并且打印出来的是页表内的虚拟地址,而非真是物理地址,所以地址相同。但是子进程修改变量时,发生写时拷贝,给该变量一个新的物理空间。总的来说,父子进程的 val 各自存放在不同的物理地址,但是虚拟地址相同。

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

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

相关文章

跟《经济学人》学英文:2024年07月20日这期 The Russell 2000 puts in a historic performance

Why investors have fallen in love with small American firms The Russell 2000 puts in a historic performance 罗素2000指数&#xff1a; 罗素2000指数&#xff08;英语&#xff1a;Russell 2000 Index&#xff09;为罗素3000指数中收录市值最小的2000家&#xff08;排序…

Linux 常用命令详解:从基础操作到进阶应用

Linux 常用命令详解&#xff1a;从基础操作到进阶应用 简介 Linux 是一个强大且灵活的操作系统&#xff0c;它在服务器、开发环境和个人计算机中得到了广泛的应用。Linux 的命令行界面提供了丰富的工具和命令&#xff0c;可以帮助用户高效地管理系统、处理文件、监控性能和进…

WebKit的暗黑魅力:全面拥抱Dark Mode

WebKit的暗黑魅力&#xff1a;全面拥抱Dark Mode 在当今数字时代&#xff0c;用户越来越注重个性化体验和视觉舒适度。暗黑模式&#xff08;Dark Mode&#xff09;作为一种新兴的界面风格&#xff0c;以其减轻视觉疲劳和节省电量的特点&#xff0c;迅速受到用户的青睐。WebKit…

学习笔记 韩顺平 零基础30天学会Java(2024.7.25)

P425 枚举类引出 举了一个例子&#xff0c;季节类创建对象&#xff0c;但是根据Java的规则&#xff0c;可以设置春夏秋冬以外的对象&#xff0c;而且可以修改&#xff0c;这样就会不符合实际&#xff0c;因此引出枚举 P426 自定义枚举类 1.构造器私有化&#xff0c;使外面没有办…

深入解析 GPT-4o mini:强大功能与创新应用

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

【Vue实战教程】之Vue项目中的异步请求

Vue的异步请求 1 axios的安装与使用 Axios是一个基于promise的HTTP库&#xff0c;主要用来向服务端发起请求&#xff0c;可以在请求中做更多可控的操作&#xff0c;例如拦截请求等。 Axios可以使用在浏览器和node.js中&#xff0c;Vue、React等前端框架的广泛普及&#xff0c…

C++ 列式内存布局数据存储格式 Arrow

Apache Arrow 优点 : 高性能数据处理&#xff1a; Arrow 使用列式内存布局&#xff0c;这特别适合于数据分析和查询操作&#xff0c;因为它允许对数据进行高效批量处理&#xff0c;减少CPU缓存未命中&#xff0c;从而提升处理速度。 零拷贝数据共享&#xff1a; Arrow …

【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常

问题现象 客户的java日志中有如下异常信息&#xff1a; 问题的风险及影响 对正常的业务流程无影响&#xff0c;但是影响druid的merge sql功能&#xff08;此功能会将sql语句中的字面量替换为绑定变量&#xff0c;然后将替换以后的sql视为同一个&#xff0c;然后用做执行性能统…

Vue3扁平化Tree组件的前端分页实现

大家好&#xff0c;我是小卷。得益于JuanTree的扁平化设计&#xff0c;在数据量很大的情况下除了懒加载&#xff0c;使用前端分页也是一种解决渲染性能问题的可选方案。 用法 要实现的文档&#xff1a; 分页效果&#xff1a; 实现 新增属性&#xff1a; 组件setup方法中新增…

程序员加班现象:成因、影响与应对策略

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;日常聊聊 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 加班的成因 加班的影响 应对策略 结语 我的其他博客 前言 在现代科技行业中&#xff0c;加班现象已成为一个普遍存在的问题…

proxy是什么,vue3是怎么使用proxy的

Vue 3 使用了 Proxy 作为其响应式系统的基础&#xff0c;这是因为 Proxy 提供了一种更为强大和灵活的方式来实现对象和数组的响应式特性。下面是 Proxy 的一些关键特性以及它们是如何在 Vue 3 中体现并提升响应式性能的&#xff1a; 什么是 Proxy&#xff1f; Proxy 是 ES6 中…

配置sublime的中的C++编译器(.sublime-build),实现C++20

GCC 4.8: 支持 C11 (部分) GCC 4.9: 支持 C11 和 C14 (部分) GCC 5: 完全支持 C14 GCC 6: 支持 C14 和 C17 (部分) GCC 7: 支持 C17 (大部分) GCC 8: 完全支持 C17&#xff0c;部分支持 C20 GCC 9: 支持更多的 C20 特性 GCC 10: 支持大部分 C20 特性 GCC 11: 更全面地支持 C20 …

Android 线程池的面试题 线程线程池面试题

1.为什么要用线程池 降低资源消耗&#xff1a;通过复用线程&#xff0c;降低创建和销毁线程的损耗。 提高响应速度&#xff1a;任务不需要等待线程创建就能立即执行。 提高线程的可管理性&#xff1a;使用线程池可以进行统一的分配、调优和监控。 2. 线程池执行流程&#xff08…

ES中的数据类型学习之ARRAY

Arrays | Elasticsearch Guide [7.17] | Elastic 中文翻译 &#xff1a;Array Elasticsearch 5.4 中文文档 看云 Arrays In Elasticsearch, there is no dedicated array data type. Any field can contain zero or more values by default, however, all values in the a…

SpringBoot 自动配置原理

一、Condition Condition 是在 Spring 4.0 增加的条件判断功能&#xff0c;通过这个可以功能可以实现选择性的创建 Bean 操 作。 思考&#xff1a; SpringBoot 是如何知道要创建哪个 Bean 的&#xff1f;比如 SpringBoot 是如何知道要创建 RedisTemplate 的&#xff1f; …

mysql的B+树索引结构介绍

一、B树 特性&#xff1a; 所有的叶子结点中包含了全部关键字的信息&#xff0c;非叶子节点只存储键值信息&#xff0c;及指向含有这些关键字记录的指针&#xff0c;且叶子结点本身依关键字的大小自小而大的顺序链接&#xff0c;所有的非终端结点可以看成是索引部分&#xff0…

MySQL数据库基本用法

了解数据库基本概念 什么是数据库&#xff1f; • 长期存放在计算机内&#xff0c;有组织、可共享的大量数据的集合&#xff0c;是一个数据“仓库” MySQL数据库的特点 • 开源免费&#xff0c;小巧但功能齐全 • 可在Windows和Linux系统上运行 • 操作方便&#xff0c;…

Hive 的 classpath 简介

Hive的classpath是Hive运行时用于查找所需类和资源文件的路径集合。它包含了Hive运行所需的所有JAR文件和配置文件的位置。理解和管理Hive的classpath对于确保Hive正常运行、添加自定义库或解决类加载问题非常重要。 以下是关于Hive classpath的一些关键点&#xff1a; 默认位…

昇思25天学习打卡营第22天|munger85

LSTMCRF序列标注 我们希望得到这个模型来对词进行标注&#xff0c;B是开始&#xff0c;I是实体词的非开始&#xff0c;O是非实体词。 我们首先需要lstm对序列里token的记忆&#xff0c;和计算每个token发到crf的分数&#xff0c;发完了再退出来&#xff0c;最后形成1模型。那么…

Python主页

文章目录 Python全套内容整理 Python全套内容整理 杂项 代码风格 Python之禅命名规范 基础语法 数值运算赋值判断循环函数 参数(*args、**krags、\、*) 包 import 基础类 字符串 str类方法 列表元组字典集合 集合速览 进阶功能 异常文件类 常见模块 ossys 常见模块&#xf…