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

news2025/3/13 19:16:58

序言

 在上一篇文章 进程概念以及进程状态,我们提到了 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.coloradmin.cn/o/1948293.html

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

相关文章

unity美术资源优化(资源冗余,主界面图集过多)

图片资源冗余&#xff1a; UPR unity的性能优化工具检查资源 1.检查纹理读/写标记 开启纹理资源的读/写标志会导致双倍的内存占用 检查Inspector -> Advanced -> Read/Write Enabled选项 2.检查纹理资源alpha通道 如果纹理的alpha通道全部为0&#xff0c;或者全部为2…

了解Selenium中的WebElement

Selenium中到处都使用WebElement来执行各种操作。什么是WebElement&#xff1f;这篇文章将详细讨论WebElement。 Selenium中的WebElement是一个表示网站HTML元素的Java接口。HTML元素包含一个开始标记和一个结束标记&#xff0c;内容位于这两个标记之间。 HTML元素的重命名 …

C++图网结构算法

目录 一.迪杰斯特拉算法&#xff08;dijkstra&#xff09; 1.实现原理&#xff1a; 2.代码实现&#xff1a; 3.例题&#xff1a; 二.spfa算法&#xff1a; 1.实现原理&#xff1a; 2.代码实现&#xff1a; 3.例题&#xff1a; 三.贝尔曼福特&#xff08;bellman_ford&…

HTTP模块(二)

HTTP 设置 HTTP 响应报文 HTTP报文常见属性&#xff1a; const http require(http);const server http.createServer((request, response) > {// 设置请求状态码 2xx 4xx 5xxresponse.statusCode 200;// 设置请求描述 了解即可response.statusMessage hello// 指定响…

Nodepad++运行Python文件的方法

windows中使用nodepad运行python文件 首先&#xff0c;需要在windows中安装python解释器。 然后打开nodepad&#xff0c;新建一个文件&#xff0c;输入一段测试的python代码 import socketdef get_hostname():try:# 获取主机名hostname socket.gethostname()return hostna…

uniapp集成安卓原生录屏插件以及使用

概述 我们知道UniApp的出现简化了开发者的工作流程&#xff0c;并减少了代码的重复编写。开发者可以使用一套代码编译到iOS、Android、以及各种小程序的应用&#xff0c;节省了人力和时间成本&#xff0c;但是涉及到与系统交互的时候&#xff0c;比如录屏、录音、录像、文件操…

Go语言常见序列化协议全面对比

先说结论 从易用性、性能、内存占用、编码后大小等几个方面综合考虑 ProtoBuf 胜出。 Gob 从性能和 I/O 带宽占用上都和 ProtoBuf 差不多&#xff0c;唯一劣势是编解码时内存占用较多。考虑到不用再写 IDL 带来的易用性&#xff0c;如果整个系统内不存在使用除 Go 以外其他语言…

『Django』搭建你的博客网站

theme: smartblue 点赞 关注 收藏 学会了 本文简介 如果你学了我前面写的 Django 文章&#xff0c;现在已经有能力去试试自己搭建博客网站了。 虽然用的不是现在流行的前后端分离的方式(前后端分离的方式会在之后的文章讲解)。 但在搭建网站之前我们还要做一些额外的功能让你…

【机器学习】梯度下降的基本概念和如何使用梯度下降自动化优化w和b

引言 梯度下降是一种用于寻找函数最小值的优化算法&#xff0c;它在机器学习中广泛用于训练模型&#xff0c;如线性回归、神经网络等 一、梯度下降的基本概念 1.1 目标函数 在机器学习中&#xff0c;这通常是损失函数&#xff08;如均方误差、交叉熵等&#xff09;&#xff0…

[渗透测试] 主动信息收集

主动信息收集 在红蓝对抗过程中&#xff0c;资产属于核心地位&#xff0c;攻击方&#xff08;红方&#xff09;要尽可能的去获取对方资产&#xff0c;暴露目标资产&#xff0c;包括IP地址、网络设备、安全设备、服务器、存储在服务器中的数据等。防守方也要清楚自己有多少有价…

了解网络是如何运作

“Web 的工作原理”提供了一个简化的视图,用于了解在计算机或手机上的 Web 浏览器中查看网页时发生的情况。 这个理论对于短期内编写 Web 代码来说并不是必需的,但不久之后,你就会真正开始从理解后台发生的事情中受益。 客户端和服务器 连接到 Internet 的计算机称为客户端和…

dns逆向解析,主从服务,多域名访问(穿插ntp服务器)

复习 域名解析&#xff1a; 正向解析&#xff1a;将域名解析为ip 反向解析&#xff1a;将ip解析为域名 逆向解析 关闭防火墙和selinux&#xff0c;配置静态ip [rootdns ~]# vim /etc/named.rfc1912.zones [rootdns ~]# vim /etc/named.conf [rootdns ~]# cd /var/named/ [rootd…

刚购买的阿里云服务器该如何配置环境(CentOS)

文章目录 购买开始初始设置登录云服务器安装 Apache 服务安装 MySQL安装 PHP快照 第三方 SSH 登录笔者的话 购买 按照需求购买就行。学生有免费试用一个月的活动&#xff0c;可以试着玩玩。 开始初始设置 登录云服务器 购买完后&#xff0c;点击实例&#xff0c;点击实例名…

Linux下RDMA驱动程序探索系列-2

本系列文章将带领读者逐步了解Linux操作系统下的RDMA子系统。本篇文章作为系列的第二篇&#xff0c;将深入内核态驱动程序的代码&#xff0c;主要介绍如下内容&#xff1a; Driver的初始化流程几个重要verbs回调函数的简介 01、Kernel Driver的初始化流程 由于不同厂商的驱动…

进销存系统开发,含税小计和含税单价计算,含税和不含税,1000元电脑为案例

if (data ! null) {console.log("中断调试&#xff0c;2024-7-25 最终计算税务");//删除不需要会报错var 未来之窗_人工智能_计算_税额 parseFloat((data.price * data.num * data.tax_rate / 100 ).toFixed(2));var 未来之窗_人工智能_计算_含税小计 parseFloat((…

js轮播图制作

实现一个简单的JavaScript轮播图可以通过以下步骤完成&#xff1a; 创建HTML结构&#xff0c;包括轮播图容器和图片列表。 使用CSS进行样式设置&#xff0c;包括隐藏多余的图片。 使用JavaScript编写函数来控制图片的切换。

07-15 周一 lmdeploy导出迁移因子到量化模型中

07-15 周一 lmdeploy导出迁移因子到量化模型中 时间版本修改人描述2024年7月15日14:57:02V0.1宋全恒新建文档 简介 方案设计 由于norm层的前后导致smoothquant执行量化不好融合&#xff0c;为了降低我事先的难度&#xff0c;所以就不再融合normalization的算子了&#xff0c…

vue3编程-import.meta.glob实现动态路由(菜单)

import.meta.glob 是vite提供的批量懒加载组件的方法 本地开发环境&#xff1a; const modules import.meta.glob(../views/**/*.vue)这段代码返回的modules是一个Map&#xff1a; key是vue文件的相对路径&#xff0c;值是一个函数&#xff0c;将函数打印出来&#xff0c;如…

【微信小程序实战教程】之微信小程序原生开发详解

微信小程序原生开发详解 微信小程序的更新迭代非常频繁&#xff0c;几乎每个月都会有新版本发布&#xff0c;这就会让初学者感觉到学习的压力和难度。其实&#xff0c;我们小程序的每次版本迭代都是在现有小程序架构基础之上进行更新的&#xff0c;如果想要学好小程序开发技术&…

配置mysql8.0.21版本docker-compose启动容器

1. 总览 2 docker-compose.xml配置 version: 3 services:mysql:image: 192.168.188.131:8000/mysqlrestart: alwaysvolumes:- ./data:/var/lib/mysql- ./my.cnf:/etc/mysql/my.cnf- ./mysql-files:/var/lib/mysql-files- ./log/mysql:/var/log/mysqlenvironment:MYSQL_ROOT_PA…