『 Linux 』进程地址空间概念

文章目录

    • 🫙 前言
    • 🫙 进程地址空间是什么
    • 🫙 写时拷贝
    • 🫙 可执行程序中的虚拟地址
    • 🫙 物理地址分布方式


🫙 前言

请添加图片描述

在c/C++中存在一种内存的概念;

一般来说一个内存的空间分布包括栈区,堆区,代码段等等;

且内存是自底向上(由0x000000000xFFFFFFFF);
以该图为例:

在这里插入图片描述

该图即为常见的内存分布图;

  • 正文代码段

    正文代码段所存放的数据一般为函数体的二进制代码;

  • 已初始化数据区

    已初始化数据区所存放的数据是在程序中声明的,并且具有初始值的变量,这些变量需要占用存储器的空间;

  • 未初始化数据区

    未初始化数据区所存放的数据是没有进行初始化或者初始值为0的数据,这些数据在存储时不需要额外占用存储器的空间;

  • 堆空间一般为动态空间,即需要成需要手动分配释放;若是分配了堆区空间但使用过后未对堆空间进行手动释放则将会出现内存泄漏的问题;

  • 一般情况下栈所存放的数据基本上都为局部变量;

  • 命令行参数/环境变量

    命令行参数/环境变量,顾名思义该段空间用来存放OS给程序所传递的命令行参数与环境变量;

  • 内核空间

    在Linux操作系统当中,内存的分布一般为其中3G为用户空间,1G为内核空间;

以下操作均在CentOS7_x64环境下进行

存在一个程序 ( mytest ) :

  int init = 10; int uninit; int main(int argc,char *argv[],char *env[])
{char*ch1= new char[10]; char*ch2= new char[10];char*ch3= new char[10];char*ch4= new char[10];char*ch5= new char[10];printf("init : %p\n",&init);//已初始化数据printf("uninit : %p\n",&uninit);//未初始化数据printf("text : %p\n",main);//正文代码段cout<<"--------------"<<endl;//堆区printf("heap1 : %p\n",ch1);printf("heap2 : %p\n",ch2);printf("heap3 : %p\n",ch3);printf("heap4 : %p\n",ch4);printf("heap5 : %p\n",ch5);cout<<"--------------"<<endl;//栈区printf("stack1 : %p\n",&ch1);printf("stack2 : %p\n",&ch2);printf("stack3 : %p\n",&ch3); printf("stack4 : %p\n",&ch4);printf("stack5 : %p\n",&ch5);cout<<"--------------"<<endl;//命令行参数for(int i = 0;i<argc;++i){printf("argv[%d] : %p\n",i,argv[i]);}cout<<"--------------"<<endl;//环境变量for(int i = 0;env[i];++i){printf("env[%d] : %p\n",i,env[i]);}return 0;
}

从这段代码中可以打印出内存中不同数据的内存分布情况;

但实际上在OS层面中,这些所谓的内存并非物理内存;


🫙 进程地址空间是什么

请添加图片描述
在上文中说到,进程所访问的地址并不是物理地址;

存在一个程序(证明):

using namespace std;int tmp = 100;int main()
{pid_t id = fork();if(id == 0){int s = 5;while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);s--;if(!s) tmp = 200;}}else{while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);}}return 0;
}

在该程序中定义了一个全局变量,并使用fork()函数对该进程创建了一个子进程,同时分别在父子进程中打印该全局变量的值与地址;

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

当五秒过后,子进程修改了全局变量的值;

可在父进程当中的这个全局变量并未被更改,且父子进程中所显示的这个全局变量tmp地址相同;

然而实际上,一个程序在运行的过程中所使用的内存地址为虚拟地址(线性地址);

在过去的计算机中,进程对于内存的访问是以直接访问的形式,即运行程序时程序载入至内存当中称为进程,CPU根据进程中的代码数据对内存的各个地址(物理地址)进行操作;

在这里插入图片描述

但是由于访问的是物理内存地址,所以若是程序在内存当中误操作则会导致某些进程的崩溃;

这种操作是十分不安全的操作;

所以为了保证安全性同时也保证进程间的独立性,现在的OS当中,出现了进程地址空间的概念;

在这里插入图片描述

每个进程都存在一个称为进程地址空间的数据结构(mm_struct结构体);

在这个结构体当中以一种类似于区间的方式模拟出地址(在Linux2.6的版本中使用unsigned long类型实现);

/*释放线性区的调用方法*/void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endifunsigned long mmap_base;		/* base of mmap area ,内存映射区的基地址*/unsigned long task_size;		/* size of task vm space */unsigned long cached_hole_size; 	/* if non-zero, the largest hole below free_area_cache */unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */pgd_t * pgd;                            /* 页表目录指针*/atomic_t mm_users;			/* How many users with user space?,共享进程的个数 */atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */int map_count;				/* number of VMAs ,线性区个数*/struct rw_semaphore mmap_sem;spinlock_t page_table_lock;		/* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long hiwater_rss;	/* High-watermark of RSS usage,进程拥有的最大页表数目 */unsigned long hiwater_vm;	/* High-water virtual memory usage ,进程线性区的最大页表数目*/unsigned long total_vm, locked_vm, shared_vm, exec_vm;unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long start_code, end_code, start_data, end_data;     /*维护代码区和数据区的字段*/unsigned long start_brk, brk, start_stack;       /*维护堆区和栈区的字段*/unsigned long arg_start, arg_end, env_start, env_end;  /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

除此之外在进程地址空间这个结构体中有一个指针,这个指针所指向的位置即为页表;

所谓的页表就是一种映射关系,这种映射关系以一种key/value的模型将对应的物理地址与虚拟地址进行一种存储,在查找或访问时将访问至虚拟地址,通过该虚拟地址通过页表的key/value模型找到其对应的物理内存再进行访问;

在CPU中存在一个内存管理单元(MMU),这个内存管理单元是CPU中的一个模块,这个模块具体的作用为负责虚拟地址到物理地址的转换;

在这里插入图片描述

以该图为例,其中task_struct表示PCB结构体,即进程控制块;

mm_struct即为该进程的进程地址空间,mm_struct中的pgd即为页表;


🫙 写时拷贝

请添加图片描述

当多个进程或线程共享同一块内存时,内核会使用写时拷贝来优化内存的复制行为;

当有一个进程尝试修改共享内存页面时,Linux内核会触发写时拷贝机制;

它会为修改的进程创建一个新的私有副本,并将修改的内容写入新的副本中,而不是立即修改原始的共享页面;

以该例子为例:

using namespace std;int tmp = 100;int main()
{pid_t id = fork();if(id == 0){int s = 5;while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);s--;if(!s) tmp = 200;}}else{while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);}}return 0;
}

在该例子中程序运行的结果为:

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

两个进程中的变量的地址相同但其值不同的原因就是在于其所在的虚拟地址相同但页表中虚拟地址所映射的物理地址不同;

在这个程序当中,使用fork()函数创建了子进程,由于子进程是由父进程创建的,所以对应的子进程的PCB结构体继承于父进程,即当父进程创建出一个子进程时,该子进程将会对父进程的PCB结构体进行一次浅拷贝,所以父子进程所对应的代码资源是共享的;

在这里插入图片描述

在只读的情况下两个进程的页表所映射至的物理地址也许相同的,而当一个进程要修改该物理内存中的内容时,OS将会重新在物理内存中申请一块空间,同时修改该进程所对应的页表映射关系;

在这里插入图片描述


🫙 可执行程序中的虚拟地址

请添加图片描述
实际在可执行程序当中也存在着所谓的虚拟地址,在一般的教材当中也被称为"逻辑地址";

存在一个程序:

#include<iostream>
using namespace std;int g_val = 100;int main()
{cout<<&g_val<<endl;return 0;
}

这个程序运行之后可以打印出该程序中全局变量g_val的地址;

在Linux中存在一个命令可以打印出一个可执行程序中的逻辑地址(虚拟地址),即objdump;

语法:

objdump -x <executable_file>

在此处配合| grep打印出该可执行程序中的虚拟地址,即:

objdump -x mytest | grep g_val

使用该命令后运行该程序:

$ objdump -x mytest | grep g_val
00000000004007f7 l     F .text	0000000000000015              _GLOBAL__sub_I_g_val
000000000060105c g     O .data	0000000000000004              g_val
$ ./mytest 
0x60105c

在上面的程序当中,程序运行的结果(打印全局变量地址)与使用objdump所显示出磁盘中的全局变量g_val地址相同,由此可见其进程中的虚拟地址与本在磁盘中的虚拟地址相同;

实际上在计算机当中,本质上无论是磁盘中的虚拟地址(逻辑地址)还是在进程当中的虚拟地址都是相同的;

只不过是在进程与磁盘中的表现形式不同;

当程序编译链接完成时生成的可执行程序当中将会存在代码数据等,在这些代码数据当中存在着静态的虚拟地址,这些地址被称作逻辑地址;

当这个程序被执行后即被加载至内存当中成为进程时,进程将会去初始化自身的PCB结构体;相对应的PCB结构体内的各种数据结构也将要被进行维护与初始化;

磁盘中的虚拟地址(逻辑地址)将会初始化PCB结构体中对应的进程地址空间,使得进程地址空间中的虚拟地址与原本磁盘内的虚拟地址(逻辑地址)保持一致;

在这里插入图片描述


🫙 物理地址分布方式

请添加图片描述
在上面的图中可以发现:

在对进程地址空间进行初始化时,真正将虚拟地址与物理地址进行关联的时候,其物理地址并没有按照原本的虚拟地址原模原样的进行对应的初始化;

在对对应物理地址进行初始化时更像是以一种随机的方式;

为了物理内存的安全性,Linux中采用了一种地址空间随机化(ASLR)的一种内存攻击缓存技术;

当对应的进程地址空间的虚拟地址在初始化时通过页表映射至物理内存时将会采用这种方式;

使得对应进程的物理内存地址无法被预测,也保证了进程在运行时的安全性;

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

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

相关文章

精通C语言函数,轻松入门!通过实例掌握技巧

一、题目&#xff1a; 1~n的倒数相加&#xff0c;即&#xff1a;1 1/2 1/3 …… 1/n 二、上代码&#xff1a; #include <stdio.h> double fun(int n) // 定义一个函数&#xff0c;参数为n&#xff08;形参&#xff09; { double sum 0; // 初始化…

vue3+vite4中使用svg,使用iconfont-svg图标

记录一下vue3中如何使用svg图标&#xff0c;vue2中大家常用iconfont字体图标&#xff0c;现在vue3大家都又推荐svg的方式使用图表&#xff0c;包括elementplus组件库也变成使用svg方式引用图标。 1、创建svg组件 components/IconSvg.vue <template><svg class"…

代理模式:解析对象间的间接访问与控制

目录 引言 理解代理模式 不同类型的代理模式 代理模式的应用场景 代理模式的优缺点 优点 缺点 实际案例&#xff1a;Java中的代理模式应用 结语 引言 代理模式是软件设计模式中的一种结构型模式&#xff0c;旨在为其他对象提供一种代理以控制对这个对象的访问。它允许你…

改造项目用 开口互感器AKH-0.66/KK-∮24 操作方便,节约时间!

1.产品特点 产品外形美观&#xff0c;安装、接线方便&#xff0c;主要用于电力运维及用电改造项目&#xff0c;一般输出为 AC 5A、 1A 或者毫安等信号&#xff0c;具有体积小、精度高、带载能力强、安装方便等优点&#xff0c;为用户改造项 目节省人力、物力、财力&#xff0c…

php之jwt使用

PHP JWT&#xff08;JSON Web Token&#xff09;是一种用于身份验证和授权的开放标准。JWT是一个包含有关用户或实体身份信息的安全令牌&#xff0c;它由三部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&#xff08;Sig…

蓝桥杯周赛 第 1 场 强者挑战赛 6. 小球碰撞【算法赛】(思维题/最长上升子序列LIS)

题目 https://www.lanqiao.cn/problems/9494/learning/?contest_id153 思路来源 Aging代码 题解 二分时间t&#xff0c;第i个小球对应一个起点pi、终点pit*vi的区间&#xff0c;问题转化为&#xff0c; 选最多的区间&#xff0c;使得不存在区间包含&#xff08;即li<l…

Goby 漏洞发布| 亿赛通电子文档安全管理系统 LinkFilterService 接口权限绕过漏洞

漏洞名称&#xff1a;亿赛通电子文档安全管理系统 LinkFilterService 接口权限绕过漏洞 English Name&#xff1a;Esafenet Electronic Document Security Management System LinkFilterService API Permission Bypass Vulnerability CVSS core: 9.3 影响资产数&#xff1a;…

【gRPC实现java端调用go的服务】

实现一个客户端调用go服务端的简单服务 1.项目结构如下 在lib下面的存在一个simple.proto文件&#xff0c;我们使用插件protobuf-maven-plugin对其进行编译。配置如下&#xff1a; <properties><os-maven-plugin.version>1.5.0.Final</os-maven-plugin.version…

生产上线需要注意的安全漏洞

一、关闭swagger 1、关闭swagger v3 # 需同时设置auto-startupfalse&#xff0c;否则/v3/api-docs等接口仍能继续访问 springfox:documentation:enabled: falseauto-startup: falseswagger-ui:enabled: false 2、关闭swagger v2 # 只要不是true就不启用 swagger:enable: fa…

mysql:查看一个表的索引信息

可以使用命令SHOW INDEX FROM table_name;查看一个表的索引信息&#xff0c;例如&#xff1a;

Python并发:多线程与多进程的详解

文章目录 本篇概要关于并发线程与多线程线程的类型什么是多线程让我们看看线程的一些优点&#xff1a;线程还有一些缺点&#xff1a;进程与多进程进程的特性进程有以下优点&#xff1a;同时&#xff0c;还有以下缺点&#xff1a;多进程Python的局限性并发文件下载先看看一个普通…

TestCase与TransactionTestCase的区别

目录 一、概述 二、区别 1、事务管理方式 2、性能影响 3、适用场景 三、示例代码 TestCase示例代码 TransactionTestCase示例代码 四、总结 TestCase与TransactionTestCase是Django框架中两个重要的测试类&#xff0c;用于对数据库操作进行测试。在编写测试用例时&…