『 Linux 』进程地址空间概念

2023-12-12 15:20

本文主要是介绍『 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)的一种内存攻击缓存技术;

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

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

这篇关于『 Linux 』进程地址空间概念的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/485072

相关文章

VScode连接远程Linux服务器环境配置图文教程

《VScode连接远程Linux服务器环境配置图文教程》:本文主要介绍如何安装和配置VSCode,包括安装步骤、环境配置(如汉化包、远程SSH连接)、语言包安装(如C/C++插件)等,文中给出了详... 目录一、安装vscode二、环境配置1.中文汉化包2.安装remote-ssh,用于远程连接2.1安装2

Linux中shell解析脚本的通配符、元字符、转义符说明

《Linux中shell解析脚本的通配符、元字符、转义符说明》:本文主要介绍shell通配符、元字符、转义符以及shell解析脚本的过程,通配符用于路径扩展,元字符用于多命令分割,转义符用于将特殊... 目录一、linux shell通配符(wildcard)二、shell元字符(特殊字符 Meta)三、s

Linux之软件包管理器yum详解

《Linux之软件包管理器yum详解》文章介绍了现代类Unix操作系统中软件包管理和包存储库的工作原理,以及如何使用包管理器如yum来安装、更新和卸载软件,文章还介绍了如何配置yum源,更新系统软件包... 目录软件包yumyum语法yum常用命令yum源配置文件介绍更新yum源查看已经安装软件的方法总结软

linux报错INFO:task xxxxxx:634 blocked for more than 120 seconds.三种解决方式

《linux报错INFO:taskxxxxxx:634blockedformorethan120seconds.三种解决方式》文章描述了一个Linux最小系统运行时出现的“hung_ta... 目录1.问题描述2.解决办法2.1 缩小文件系统缓存大小2.2 修改系统IO调度策略2.3 取消120秒时间限制3

Linux alias的三种使用场景方式

《Linuxalias的三种使用场景方式》文章介绍了Linux中`alias`命令的三种使用场景:临时别名、用户级别别名和系统级别别名,临时别名仅在当前终端有效,用户级别别名在当前用户下所有终端有效... 目录linux alias三种使用场景一次性适用于当前用户全局生效,所有用户都可调用删除总结Linux

Linux:alias如何设置永久生效

《Linux:alias如何设置永久生效》在Linux中设置别名永久生效的步骤包括:在/root/.bashrc文件中配置别名,保存并退出,然后使用source命令(或点命令)使配置立即生效,这样,别... 目录linux:alias设置永久生效步骤保存退出后功能总结Linux:alias设置永久生效步骤

Linux使用fdisk进行磁盘的相关操作

《Linux使用fdisk进行磁盘的相关操作》fdisk命令是Linux中用于管理磁盘分区的强大文本实用程序,这篇文章主要为大家详细介绍了如何使用fdisk进行磁盘的相关操作,需要的可以了解下... 目录简介基本语法示例用法列出所有分区查看指定磁盘的区分管理指定的磁盘进入交互式模式创建一个新的分区删除一个存

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

Linux Mint Xia 22.1重磅发布: 重要更新一览

《LinuxMintXia22.1重磅发布:重要更新一览》Beta版LinuxMint“Xia”22.1发布,新版本基于Ubuntu24.04,内核版本为Linux6.8,这... linux Mint 22.1「Xia」正式发布啦!这次更新带来了诸多优化和改进,进一步巩固了 Mint 在 Linux 桌面