【Linux学习】进程地址空间与写时拷贝

news2025/1/12 1:53:19

文章目录

  • Linux进程内存布局图:
      • 内存布局的验证
  • 进程地址空间
    • 写时拷贝


Linux进程内存布局图:

地址空间的范围,在32位机器上是2^32比特位,也就是[0,4G]。
在这里插入图片描述

内存布局的验证

  • 代码验证内存布局: 验证代码:
   #include<stdio.h>
   #include<stdlib.h>
   #include<unistd.h>
   #include<sys/types.h>
   
   
   int init=10;
   int uninit;
   
  int main()
  {
      printf("code addr:%p\n",&main);
      printf("init addr:%p\n",&init);
      printf("uninit addr:%p\n",&uninit);
  
      char* heap = (char* )malloc(20);
      printf("heap addr:%p\n",heap);
      printf("stack addr:%p\n",&heap);
  
      return 0;                                                                                                                             
  }

运行结果及分析:根据下图运行结果分析,验证了上图的内存分布。
在这里插入图片描述

  • 验证堆向上增长与栈向下增长:

验证代码:

    char* heap1 = (char* )malloc(20);
    char* heap2 = (char* )malloc(20);
    char* heap3 = (char* )malloc(20);
    char* heap4 = (char* )malloc(20);
    char* heap5 = (char* )malloc(20);
    printf("heap1 addr:%p\n",heap1);
    printf("heap2 addr:%p\n",heap2);
    printf("heap3 addr:%p\n",heap3);
    printf("heap4 addr:%p\n",heap4);
    printf("heap5 addr:%p\n",heap5);
  
    printf("stack1 addr:%p\n",&heap1);
    printf("stack2 addr:%p\n",&heap2);
    printf("stack3 addr:%p\n",&heap3);
    printf("stack4 addr:%p\n",&heap4);
    printf("stack5 addr:%p\n",&heap5);                                                                                                    
 

运行结果:堆向上增长,栈向下减小,与内存分布图一样。
结论:堆栈相对而生。

在这里插入图片描述

  • 验证命令行参数与环境变量:

验证代码:

   int main(int argc,char* argv[],char* env[])
    {
      for(int i = 0;argv[i];i++)
      {
        printf("&argv[%d]:%p \n",i,argv+i);
      }
      for(int i = 0;env[i];i++)
      {
        printf("&env[%d]:%p \n",i,env+i);
      }
                                                                                                                                          
      return 0;
    }

运行结果及分析:环境变量与命令行参数这两张表(不是表指向的内容),比栈区大,其中,是先有命令行参数这张表,才有环境变量这张表。
在这里插入图片描述

  • 验证表指向的内容的地址存放:

    注意区分下面代码与上面代码的不同!
    验证代码:

   int main(int argc,char* argv[],char* env[])
    {
      	for(int i = 0;argv[i];i++)
      	{
        	printf("argv[%d]:%p \n",i,argv[i]);
      	}
      	for(int i = 0;env[i];i++)
      	{
        	printf("env[%d]:%p \n",i,env[i]);
      	}
                                                                                                                                          
      	return 0;
    }

结果+分析:无论是表还是表指向的项目,都在栈上部的。
在这里插入图片描述

  • 验证静态变量在内存分布中的位置:
    这里就不验证了,直接得出结论:静态变量是存放在初始化数据与未初始化数据之间的。静态变量默认是会被初始化的,哪怕用户定义出来没有赋值,编译器也会初始化。例如int 类型的静态变量,会被编译器初始化为0;

看看一个这样的代码
代码:

   #include<stdio.h>
   #include<stdlib.h>
   #include<unistd.h>
   #include<sys/types.h>
   
   int g_val = 1000;
   int main()
   {
    	pid_t id = fork();
    	if(id==0)
    	{
      	//子进程
      	while(1)
      	{
        	printf("child  pid:%d  ppid:%d  g_val=%d  &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        	sleep(1);
      	}
    	}
    	//父进程                                                                                                                              
    	else{
      		while(1)
      		{
        		printf("father  pid:%d  ppid:%d  g_val=%d  &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        		sleep(1);
      		}
    	}
    	return 0;
  }

运行结果:符合我们预期的,数据本来就是父子进程共享的,除非要写入,进程之间时具有独立性的,写入的时候需要写时拷贝。
在这里插入图片描述

奇怪的现象:

测试代码:

   #include<stdio.h>
   #include<stdlib.h>
   #include<unistd.h>
   #include<sys/types.h>
   
   
   int g_val = 1000;
   int main()
   {
    	pid_t id = fork();
    	if(id==0)
    	{
      		//子进程
      		int cnt = 0;
     		while(1)
     	 	{
        		printf("child  pid:%d  ppid:%d  g_val=%d  &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        		sleep(1);
       			cnt++;                                                                                                                            
        		if(cnt==3)
        		{
          			printf("child change g_val\n");
          			g_val=2000;
        		}
      		}
    		//父进程
    		else{
      			while(1)
      			{                                                                                                                                   
        			printf("father  pid:%d  ppid:%d  g_val=%d  &g_val:%p\n",getpid(),getppid(),g_val,&g_val);
       				sleep(1);
      			}
    		}
    		return 0;
  }

运行结果+分析:奇怪的现象(如下图):同一个变量,子进程尝试对g_val进行写入的时候,会进行写时拷贝,但是为什么地址一样,但是值却不一样呢?
在这里插入图片描述

解释上面的现象:

  1. 地址一样却值不一样,所以这个地址肯定不是物理地址。
  2. 如果是物理地址,绝对不可能在一个地址中存放的内容不一样。

这个地址叫做虚拟地址/线性地址。
结论:我们平时用到的语言的地址全部都不是物理地址,是虚拟地址。所以下面这个图的空间排布的情况不是物理内存,它叫做进程地址空间。
在这里插入图片描述

进程地址空间

每一个进程都有一个task_struct(PCB),PCB里面有该进程的进程地址空间,进程地址空间和内存之间是用一张表(叫做页表:里面存放的是虚拟地址与物理地址)建立关系的,如下图,页表对应一个映射关系,是虚拟地址与物理地址之间的关系。根据虚拟地址可以找到对应的物理地址。下面的结构都是操作系统内部在维护的。

在这里插入图片描述

  • 说明:上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

其中,父进程创建子进程后,子进程也会有一个这样的结构,也会有进程地址空间,页表,并且父进程PCB的大部分属性都会被子进程继承下来,页表也会被继承下来(类似浅拷贝),这时父子进程都指向同一个物理内存。以上面的示例分析:当子进程尝试对g_val进行修改时,操作系统会在内存中重新开一个空间,将修改后的值放在这个空间里,再改变页表中g_val的虚拟地址对应的物理地址,注意:改的是物理地址,虚拟地址没有改变,所以上面示例的结果打印出来的地址(虚拟地址)没有改变。如下图:
在这里插入图片描述
根据上面的解释,也能够很好的解释fork()返回值问题了!

什么是进程地址空间?

进程地址空间是数据结构,具体到进程中,是有特定的数据结构对象。
如下图所示:在进程的PCB中,有一个指针,指向自己的进程地址空间,进程地址空间里面,包含一个结构体,结构体里面有很多start和end,划分区域。

在这里插入图片描述

为什么要有地址空间和页表?

  1. 在进程看来,有了页表,可以将物理内存从无徐变为有序,因为页表是有序的。让进程以统一的视角,看待内存;
  2. 将进程管理和内存管解耦合,进程管理与内存管理互不干扰。
  3. 地址空间+页表是保护内存安全的重要手段(拦截非法:例如:野指针,越界问题)。

内存申请问题(malloc/new)

申请内存,本质是进程的地址空间中申请。
这样:可以充分保证:

  1. 内存使用率,不会空转。
  2. 提升new/malloc的速度。

写时拷贝

  1. 为什么需要写时拷贝?
    答:进程之间要做到独立性。
  2. 创建子进程的时候,为什么不直接将父进程的代码和数据拷贝一份给子进程呢?
    答:因为子进程并不是会对父进程的所有数据都要进行写入操作,如果fork()创建子进程的时候,直接拷贝一份代码和数据,会降低fork()的效率。
  3. 为什么是要拷贝呢,只开空间不拷贝行不行?
    答:因为子进程不一定是对这个数据直接进行覆盖式的写入,可以只是对该数据进行局部修改或则是基于之前的值进行操作。

如何做到写时拷贝的?

前面所说的页表,不只是有虚拟地址与物理地址的转换的,还可以带很多选项的,如下图(介绍其中一个:权限):
下图代码字符串"hello Linux"是具有常属性的,不能被修改,当我们尝试去修改的时候,会报错(运行报错)。
是因为在页表有权限,虚拟地址映射到物理地址的时候,会做权限审核,如下图所示,当只有可读权限,没有修改的权限的时候,尝试去修改,就会报错。
在这里插入图片描述

写时拷贝的细节:

当要进行写时拷贝的时候,会将父子进程页表里大部分内容的映射权限设置为只读权限,当父子进程任何一方要去进行尝试写入的时候,操作系统会进行判断,如果是数据段,对数据进行写入时合理的,就会引发缺页中断,操作系统会将权限改为读写,然后写时拷贝后,再把页表对应的条目改为读写。


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

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

相关文章

搭建第一个SpringBoot+Vue项目

Maven&#xff1a;项目管理工具&#xff0c;对Java项目进行自动化的构建和依赖管理 SpringBoot的特点 只需要很少的配置或者默认配置 能够使用内嵌的Tomcat、Jetty服务器&#xff0c;不需要部署war文件 提供定制化的启动器Starters&#xff0c;简化Maven配置&#xff0c;开…

【Qt】深入探索Qt主窗口与菜单栏:构建高效用户界面的实践指南

文章目录 前言1. 什么是Main Window?2. 详细了解一下其中的 菜单栏&#xff1a;2.1. 创建菜单栏2.2. 添加快捷键2.3. 添加子菜单2.4. 添加分割线2.5. 添加图标 3. 内存泄漏问题&#xff1a;总结 前言 在现代软件开发中&#xff0c;用户界面的设计对于提升用户体验至关重要。Q…

DELPHI通过WebService进行数据交互

WebService是一种可以跨语言和平台的数据交互模式&#xff0c;使用非常广泛&#xff0c;与JSon数据格式结合&#xff0c;更是当前非常流行的一种模式&#xff0c;本章针对DELPHI通过WebService进行数据的交互做一个相对简单例子应用。 本文使用的DELPHI 版本为&#xff1a;DEL…

Linux基础之程序地址空间

目录 一、程序地址空间的基本概念 二、程序地址空间的结构​编辑 三、虚拟地址和物理地址的关系 四、为什么要使用程序地址空间 一、程序地址空间的基本概念 要理解我们的程序地址空间&#xff0c;首先就要认识下面这张图&#xff1a; 这张图上所表示的内容&#xf…

java中写word换行符 poi 换行

省流&#xff1a; 表格外的文本&#xff0c;使用“\r”或者“(char)11”来换行&#xff0c;建议用"\r"。 表格内的文本&#xff0c;使用“(char)11”来换行。 正文&#xff1a; 测试用word文档&#xff1a; t1.doc内容如下&#xff1a; t2.doc内容如下&#xff…

基于xilinx fpga RFSOC系列的Ultrascale+ RF Data Converter ip详解说明

目录 1 概述2 IP功能2.1 ADC性能2.2 DAC性能3 IP端口4 代码框架4.1 ADC功能框图4.2 DAC功能框图5 收发数据时序5.1 ADC数据格式5.2 DAC数据格式6 时钟配置6.1 ADC/DAC参考时钟7 数据格式配置模式7.1 ADC的配置模式7.1.1 Real -> real;7.1.2 Real ->IQ;7.1.3 IQ -> IQ;…

掌握2个手机数据恢复软件,快速恢复丢失的数据!

我们使用手机存储了大量的个人和重要数据&#xff0c;包括照片、视频、联系人、短信等等。然而&#xff0c;数据丢失的风险也随之增加。不小心删除、手机损坏、系统崩溃等问题都可能导致宝贵的数据消失。 幸运的是&#xff0c;现在有许多强大而高效的手机数据恢复软件可以帮助…

CSS transform 三大属性 rotate、scale、translate

transform 浏览器支持定义和用法translate位移函数rotate旋转函数scale缩放函数 浏览器支持 表格中的数字表示支持该属性的第一个浏览器版本号。 紧跟在 -webkit-, -ms- 或 -moz- 前的数字为支持该前缀属性的第一个浏览器版本号。 定义和用法 transform 属性向元素应用 2D…

2024电工杯B题保姆级分析完整思路+代码+数据教学

2024电工杯B题保姆级分析完整思路代码数据教学 B题题目&#xff1a;大学生平衡膳食食谱的优化设计及评价 接下来我们将按照题目总体分析-背景分析-各小问分析的形式来 总体分析&#xff1a; 题目要求对两份一日膳食食谱进行营养分析和调整&#xff0c;然后设计优化的平衡膳…

Python 造数据神器Faker

大家好&#xff0c;在编写代码过程中&#xff0c;我们经常需要一些假数据来进行测试或者演示。手动创建这些数据不仅耗时&#xff0c;而且容易出错。幸运的是&#xff0c;Python有一个非常有用的库叫做Faker&#xff0c;它可以生成各种类型的假数据&#xff0c;从名字、地址到公…

ios 原生项目迁移flutter第一天环境

由于公司已经有第一个吃螃蟹的项目组&#xff0c;我在迁移的时候想着站在巨人的肩膀上&#xff0c;但是搭配环境一定要问清楚对方flutter版本&#xff0c;路径也要安排好&#xff0c;不然就不行。 对着自己的项目照着葫芦画瓢&#xff0c;我刚开始为了配置管理图个方便随便放&…

基于Python实现 HR 分析(逻辑回归和基于树的机器学习)【500010104】

介绍 数据集说明 此数据集包含与员工有关的综合属性集合&#xff0c;从人口统计细节到与工作相关的因素。该分析的主要目的是预测员工流动率并辨别导致员工流失的潜在因素。 在这个数据集中&#xff0c;有14,999行&#xff0c;10列&#xff0c;以及这些变量&#xff1a;满意度…

GPU集合通信库在B站的应用和改进

1. 背景 上篇文章 万字长文解析&#xff1a;大模型需要怎样的硬件算力 深入探讨了大型语言模型&#xff08;LLMs&#xff09;在硬件资源方面的需求和面临的挑战&#xff0c;详尽地阐述了如何进行大模型的硬件选型&#xff0c;以及在实际工作中如何根据模型的特定需求来优化硬件…

安装petalinux工具

petalinux 并不是一个特殊 Linux 内核&#xff0c;而是一套开发环境配置的工具&#xff0c;降低 uboot、内核、 根文件系统的配置的工作量&#xff0c;可以从 Vivado 的导出硬件信息自动完成相关软件的配置。 petalinux 是赛灵思基于 buildroot 工具链为自家处理器方便适配 Li…

每周刷题第三期

个人主页&#xff1a;星纭-CSDN博客 系列文章专栏&#xff1a;Python 踏上取经路&#xff0c;比抵达灵山更重要&#xff01;一起努力一起进步&#xff01; 目录 题目一&#xff1a;环形链表 题目二&#xff1a;删除有序数组中的重复项 题目三&#xff1a;有效的括号 题…

spring-boot-starter-mail 发送带附件的邮件信息

背景 项目使用的事ruoyi低代码开发平台ruoyi中有常见的web端下载excel的方式&#xff0c;但是这种方式是直接把输出流写到一个response中&#xff0c;而不是给一个outputstream&#xff0c;如果是给一个outputstream的话&#xff0c;就可以写入到一个固定的文件中去了 解决思路…

P2. 配置MySQL和用户注册登录模块

P2. 配置MySQL和用户注册登录模块 0 概述Tips1 预备知识1.1 SpringBoot 常用模块1.2 pojo层的实现1.3 mapper层的实现1.4 controller层调试CRUD 2 Spring Security2.1 Spring Security 介绍2.2 Spring Security 对接数据库2.3 密码的加密 3 Jwt验证3.1 传统Session验证方式3.2 …

Langchain-Chatchat的markdownHeaderTextSplitter使用

文章目录 背景排查步骤官方issue排查测试正常对话测试官方默认知识库Debug排查vscode配置launch.json命令行自动启动condadebug知识库搜索测试更换ChineseRecursiveTextSplitter分词器 结论 关于markdownHeaderTextSplitter的探索标准的markdown测试集Langchain区分head1和head…

小白跟做江科大32单片机之学习准备

1.安装好51MDK之后&#xff0c;出现不能正常安装支持包的情况 在线安装支持包——>在keil5软件下点击这个&#xff0c;即可进入更新支持包界面 进去之后找这个 国产的可以找和这个 最后有这个就可以了

【人工智能项目】小车障碍物识别与模型训练(完整工程资料源码)

实物演示效果: 一、绪论: 1.1 设计背景 小车障碍物识别与模型训练的设计背景通常涉及以下几个方面: 随着自动驾驶技术的发展,小车(如无人驾驶汽车、机器人等)需要能够在复杂的环境中自主导航。障碍物识别是实现这一目标的关键技术之一,它允许小车检测并避开路上的障碍物…