【linux进程】进程地址空间(什么是进程地址空间?为什么要有进程地址空间?)

news2024/11/18 10:37:52

目录

一、前言

二、 程序的地址空间是真实的 --- 物理空间吗? 

三、进程地址空间 

🔥 操作系统是如何建立起进程与物理内存之间的联系的呢?

🔥什么是进程地址空间?

🔥为什么不能直接去访问物理内存? 

🔥为什么有进程地址空间和页表?

🔥malloc和new开辟空间的原理 

🔥什么是写时拷贝?

四、总结 

五 、共勉 


一、前言

        对于 C/C++ 来说,程序中的内存包括这几部分:栈区堆区静态区 等,其中各个部分功能都不相同,比如函数的栈帧位于 栈区动态申请的空间位于 堆区全局变量和常量位于 静态区 ,区域划分的意义是为了更好的使用和管理空间,那么 真实物理空间 也是如此划分吗?多进程运行 时,又是如何区分空间的呢?写时拷贝 机制原理是什么?本文将对这些问题进行解答 

二、 程序的地址空间是真实的 --- 物理空间吗? 

对于下面的图大家一定不陌生,这是在我们学习  C/C++ 内存管理 的时候经常见到的,内存空间部署图


在 C/C++语言 学习阶段,我们可以通过对变量 & 取地址的方式,查看当前变量存储空间的首地址信息 。我们还是来验证一下数据是不是按如图所示进行排列存储的呢?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> 

int g_unval;
int g_val = 100;
 
int main(int argc, char *argv[], char *env[])
{
    printf("code addr:\t%p\n", main);//验证正文代码
    printf("init data addr:\t%p\n", &g_val);//验证初始化数据(全局)
    printf("uninit data addr: %p\n", &g_unval);//验证未初始化数据(全局)
 
    char *heap = (char*)malloc(20);//如图先创建的动态内存应该在堆底
    char *heap1 = (char*)malloc(20);//所以heap的地址应为最小
    char *heap2 = (char*)malloc(20);//heap3的地址应为最大
    char *heap3 = (char*)malloc(20);//一会观察是否是这样
 
    printf("heap addr: %p\n", heap);//验证堆区(动态内存)
    printf("heap1 addr: %p\n", heap1);
    printf("heap2 addr: %p\n", heap2);
    printf("heap3 addr: %p\n", heap3);
 
    printf("stack addr: %p\n", &heap);//验证栈区(指针变量)
    printf("stack addr: %p\n", &heap1);//如图先创建的heap指针应该在栈空间中地址最大
    printf("stack addr: %p\n", &heap2);//所以&heap应为最大
    printf("stack addr: %p\n", &heap3);//&heap3应为最小
 
    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;
}


通过运行后的结果可以看出,空间所谓的分步情况确实如此,但是接下来这段代码运行后的结果,会让让你很诧异。 

  • 我们之前在进程概念的学习中创建过子进程,那我们刚好可以观察一下当子进程修改某一共享变量时,父子进程读取到的该变量的值是否会发生改变,该变量的地址又呈现出什么样的内容?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int g_val = 100;
 
int main()
{
    if(fork() == 0){
        int ret = 5;
        while(ret){                                                                     
            printf("hello--- %d g_val = %d &g_val = %p\n", ret, g_val, &g_val);
            ret--;                         
            sleep(1);
            if(ret == 3){
                printf("################child更改数据###############\n");                              
                g_val = 200;
                printf("#############child更改数据完成##############\n");
            }   
        }                                          
    }                                  
    else{                  
        while(1){                                              
            printf("I am father:g_val = %d &g_val = %p\n", g_val, &g_val);
            sleep(1);                                                     
        }                                                              
    }                                                              
    return 0;                                               
}

解析代码:3 秒 之前 父子进程 读取变量g_val的值,3秒子进程 对该变量进行修改,观察修改之后父子进程读取该变量的值如何变化,并且是否符合我们之前所讲的写时拷贝,是否会拷贝一份给自己再修改? 


我们发现确实,当子进程对变量进行修改时,子进程对应的g_val发生了改变,而父进程没有改变,进程之间确实具有独立性。 

可是最令人费解的是,父子进程读取该变量的地址竟然相同!?

这也就证实了之前我们所学习的所谓的内存分布图是假的,打印出来的地址也是假的,因为如果是物理内存地址,同一物理地址是不可能存放两个值的!!

结论:

  • 我们所有用到的语言上的地址都不是物理地址,而是虚拟地址(线性地址)
  • 此图不是物理内存分布图,而是进程地址空间分布图。

三、进程地址空间 

现在我们就知道了文章开头给出的图片根本不是什么物理内存分布图,而是进程地址空间分布图。 

真实的物理内存为: 

完了,我们之前所学被颠覆了,那物理内存到底在哪里啊,进程是如何访问到物理内存的?

所以我们继续往下看:


🔥 操作系统是如何建立起进程与物理内存之间的联系的呢?

每一个进程都会存在一个进程地址空间操作系统如何管理这些进程地址空间呢?

-----   先描述,再组织。

所以进程地址空间 --- 本质上就是一种数据结构,PCB中会有一个指针指向该数据结构,该数据结构中存储的就是对应的虚拟地址,所以操作系统对进程地址空间的管理也就变成了对该数据结构的管理。


另外操作系统会为我们维护一张映射表:页表。

  • 该表中存储的就是虚拟地址物理地址,通过虚拟地址就可以找到物理地址,也就建立起来了进程与物理内存的联系。

  • 当创建子进程时,子进程会继承父进程的进程地址空间、页表等。 
  • 所以我们说父子进程代码共享,数据共享,是因为他们的页表是相同的。 
  • 但对共享的变量进行修改时,会发生写时拷贝,拷贝到的代码和数据也是新开辟在物理内存上的,此时子进程只需要修改页表,虚拟地址不变,而物理地址则是新开辟的物理地址 

 所以才会出现虚拟地址相同,而物理地址不同的情况。


🔥什么是进程地址空间?

每一个进程都会存在一个进程地址空间,在32位操作系统下,该空间的大小为[0,4]GB。  

上面说到:进程地址空间其实就是一个数据结构,那该数据结构中都存在有哪些内容呢? 

查看Linux内核源码:查看 PCB(task_struct) 

   So :之前说‘程序的地址空间’是不准确的,准确的应该说成进程虚拟地址空间每个进程都会有自己的地址空间,认为自己独占物理内存。操作系统在描述进程地址空间时,是以结构体的形式描述的,在linux中这种结构体是 struct mm_struct 。它在内核中是一个数据结构类型,具体进程的地址空间变量。

进程地址空间 就类似于一把尺子,每个空间都有对应的起始位置和结束位置。通过这个虚拟地址去间接访问内存; 


🔥为什么不能直接去访问物理内存? 

        如果没有进程地址空间的加持,那么程序就会直接访问物理内存,没有区间可言,会存在恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。有些非恶意的,但是有bug的程序也可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的,因为用户希望使用计算机的时候,其中一个任务失败了,至少不能影响其它的任务。


🔥为什么有进程地址空间和页表?

因为有了 进程地址空间 页表,在物理内存空间上不连续、无序的空间就可以通过页表这一映射关系联系在一起,让进程以统一的视角看待内存。

  • 有了进程地址空间和页表后,每个进程都认为自己在独占内存,这样能更好的保障进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程管理与内存管理进行解耦合。
  • 进程地址空间 + 页表 的设计是保护内存安全的重要手段!

🔥malloc和new开辟空间的原理 

在之前的学习中,我们不知道进程地址空间的概念,所以 malloc 和 new 开辟空间我们总是默认为内存上的操作,而学习完进程地址空间后,你会发现并不是如此。 

  • 当代码执行到 malloc 和 new 时,操作系统(OS)不一定会直接将实际的物理内存分配给你,因为该进程可能不会立即使用该块内存,也就造成了内存浪费,OS一定要确保效率和资源使用率,所以OS给你分配的实际上是进程地址空间,地址也是虚拟地址,而且并不会在页表上建立有效的映射关系。 

当检测到该进程实际要使用该块空间时(写入修改之类的操作,读取不算),会发生缺页中断,然后立即在页表中建立映射关系,此时该进程需要的物理内存空间才被申请。 

这样做有什么好处呢?

  • 充分保证内存的使用率,不会造成空转;
  • 提升new或malloc的速度(因为没有实际在内存上开辟空间)。

🔥什么是写时拷贝?

Linux 中存在一个很有意思的机制:写时拷贝 

  • 这是一种 赌bo 行为,操作系统(OS) 此时就赌你不会对数据进行修改,这样就可以 使多个 进程 在访问同一个数据时,指向同一块空间,当发生改写行为时,再新开辟空间进行读写 
  • 这种行为对于内置类型来说感知还不是很强,但如果是自定义类型的话,写时拷贝 行为可以在某些场景下减少 拷贝构造 函数的调用次数(尤其是 深拷贝),尽可能提高效率 

可以通过一个简单的例子来证明此现象 

//计算 string 类的大小
#include <iostream>
#include <string>
using namespace std;


int main()
{
	string s;
	cout << sizeof(s) << endl;
	return 0;
}

原因: 

  • g++ 中的 string 对象创建后,它就赌你不会直接改写,所以实际对象为一个指针类型(64位环境下为8字节),当发生改写行为时,触发 写时拷贝 机制,再进行其他操作

 操作系统是如何知道什么时候进行写时拷贝的呢? 

  • 在父进程创建子进程时,按之前所学子进程会继承父进程的进程地址空间和页表。 
  • 并且操作系统还会将父子进程的页表中数据对应的权限属性修改为只读!

当父或子进程修改(写入)该数据时,会发生缺页中断,但其实缺页中断做的工作不仅会在物理内存上开辟空间建立映射关系,还会对我们的访问操作做判断: 

  • 操作系统会判断,页表权限为只读,但数据所在的进程地址空间属于可读可写的数据区,操作系统明白了,这是要写时拷贝啊!

所以这就是操作系统判断什么时候进行写时拷贝的原理,根据这个方法,操作系统就能实现按需拷贝!

谁要使用(写入)给谁开辟新的物理空间,否则就不拷贝,共用物理内存空间。 


四、总结 

     简而言之,首先,程序数据加载到内存后,由操作系统分配进程PCB(task_struct和mm_struct(进程虚拟地址空间))和页表。此时我们的进程就算是创建好了。

  • 有了进程地址空间(虚拟地址),每个进程都认为自己独占内存资源,这样对于操作系统来讲,也更加偏于管理进程。
  • 采用间接的地址访问方法访问物理内存。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠。
  •  如果没有进程地址空间的加持,那么程序就会直接访问物理内存,没有区间可言,会存在恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。反之有利于保护物理内存。

五 、共勉 

 以下就是我对【Linux系统编程】进程地址空间 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新【Linux系统编程】请持续关注我哦!!!  

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

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

相关文章

Protobuf(三):理论学习,简单总结

1. Protocol Buffers概述 Protocol Buffers&#xff08;简称protobuf&#xff09;&#xff0c;是谷歌用于序列化结构化数据的一种语言独立、平台独立且可扩展的机制&#xff0c;类似XML&#xff0c;但比XML更小、更快、更简单protobuf的工作流程如图所示 1.1 protobuf的优点…

Python酷库之旅-第三方库Pandas(003)

目录 一、用法精讲 4、pandas.read_csv函数 4-1、语法 4-2、参数 4-3、功能 4-4、返回值 4-5、说明 4-6、用法 4-6-1、创建csv文件 4-6-2、代码示例 4-6-3、结果输出 二、推荐阅读 1、Python筑基之旅 2、Python函数之旅 3、Python算法之旅 4、Python魔法之旅 …

工业废水中镍超标怎么办?含镍废水处理方法有哪些?

镍是一种存在于自然界中的过渡金属。镍在土壤和岩石中的存量丰富&#xff0c;大部分镍已被氧化&#xff0c;或与其他元素结合成化合物。   含镍废水主要来源于电镀、合金制造、金属表面处理、电子等行业。这些行业在生产过程中&#xff0c;通常会使用含有镍离子的化学试剂&a…

kali改回官方源后更新失败

官方源&#xff1a; deb http://http.kali.org/kali kali-rolling main non-free contrib deb-src http://http.kali.org/kali kali-rolling main non-free contrib在文件 /etc/cat/sources.list中将官方源修改为&#xff1a; deb http://http.kali.org/kali kali-rolling ma…

等保测评需要什么SSL证书

在进行信息安全等级保护&#xff08;简称“等保”&#xff09;测评时&#xff0c;选择合适的HTTPS证书对于确保网站的安全性和合规性至关重要。以下是在等保测评中选择HTTPS证书时应考虑的因素&#xff1a; 国产证书&#xff1a; 等保测评倾向于使用国产品牌的SSL证书&#x…

科普文:一文搞懂jvm实战(二)Cleaner回收jvm资源

概叙 在JDK9中新增了Cleaner类&#xff0c;该类的作用是用于替代finalize方法&#xff0c;更有效地释放资源并避免内存泄漏。 在JEP260提案中&#xff0c;封装了大部分Sun包内部的API之余&#xff0c;还引入了一些新的API&#xff0c;其中就包含着Cleaner这个工具类。Cleaner承…

JAVA程序打包时报错,但是运行时正常。

报错&#xff1a;Could not transfer artifact com.alibaba:fastjson:pom:1.2.83 from/to clojars... 背景&#xff1a;需要将fastjson从1.2.70升级到1.2.83&#xff1b;并且编译环境是局域网不可以连接互联网&#xff1b;每个项目组都是独立的私有仓库。 操作&#xff1a;在本…

品牌推广的深层逻辑:自我提升与市场认同的和谐共生

品牌推广的深层逻辑&#xff1a;自我提升与市场认同的和谐共生 著名飞行员查尔斯林德伯格(Charles Lindbergh) 曾写道:“改善生活方式比传播生活方式更重要。如果我们自己的生活方式使别人感到满意&#xff0c;那么它将自动蔓延。如果不是这样&#xff0c;那么任何武力都不可能…

kotlin协程的理解

伴生对象&#xff1a;companion object 其实质等同于Java中的单例模式 协程&#xff1a;通常实现是用户态的任务协作式调度 一段可执行代码可挂起/可恢复执行概念上与语言无关&#xff0c;协程这个概念于1958年提出 依赖框架&#xff1a; 协程的启动&#xff1a; 1.协程体&a…

【SVN的使用-源代码管理工具-命令行的使用 Objective-C语言】

一、接下来,我们来说一个终端的命令行的使用, 1.我们说,你的电脑里边呢,有终端, 在Mac里边,你想新建一个txt,应该怎么写,对,打开文本编辑, 打开这个东西,写点儿东西,然后保存一下,保存的时候,你还要去选择格式, 现在,如果我们用命令行,可以更方便一些, 2.首…

SCT612404通道,高效高集成,摄像头模组电源集成芯片

集成三路降压变换器&#xff0c;1CH高压BUCK,2CH低压Buck >HVBuck1:输入电压4.0V-20V,输出电流1.2A,Voo300mV/500mV >LVBuck2:输入电压2.7V-5V,输出电流0.6A , 固定1.8V输出 ;LVBuck3:输λ2.7V-5V,输出电流1.2A,可设定固定输出&#xff1a; 1 . 1 V / 1 . 2 V / 1 . 3 …

vite项目配置svg图标(vite-plugin-svg-icons)

1.插件地址 网址 , 可以去里面查看中文文档,里面有详情的教程 2.使用, 如果你安装的有element-plus ,可以使用这样的方式来修改大小和颜色 <el-icon size"18" color"red"><SvgIcon name"xing"></SvgIcon></el-icon> …

MViT(ICCV 2021, Meta)论文解读

paper&#xff1a;Multiscale Vision Transformers official implementation&#xff1a;https://github.com/facebookresearch/SlowFast 背景和出发点 这篇文章提出了多尺度视觉Transformer&#xff08;Multiscale Vision Transformers, MViT&#xff09;的概念&#xff0c…

nftables(1)基本原理

简介 nftables 是 Linux 内核中用于数据包分类的现代框架&#xff0c;用来替代旧的 iptables&#xff08;包括 ip6tables, arptables, ebtables 等&#xff0c;统称为 xtables&#xff09;架构。nftables 提供了更强大、更灵活以及更易于管理的规则集配置方式&#xff0c;使得…

中国1km高分辨率高质量逐年近地表CO数据集(2013-2022年)

该数据为中国高分辨率高质量逐年CO数据集&#xff0c;该数据集主要的空间范围覆盖整个中国&#xff0c;其中内容包括中国1km高分辨率高质量逐年CO数据集(2013-2022年)。时间分辨率为年&#xff0c;单位为mg/m3&#xff0c;数据以(.nc/.tif)格式进行存储。

Vscode快捷键崩溃

Vscode快捷键崩溃 Linux虚拟机下使用vscode写代码【ctrlA&#xff0c;CtrlC&#xff0c;CtrlV】等快捷键都不能使用&#xff0c;还会出现“NO text insert“等抽象的指令&#xff0c;问题就是不知道什么时候装了一个VIM插件&#xff0c;让他滚出电脑》》》

快速傅里叶变换(Fast Fourier Transform)

快速算法&#xff08;FFT&#xff09;&#xff0c;即快速傅里叶变换&#xff08;Fast Fourier Transform&#xff09;&#xff0c;是一种用于计算离散傅里叶变换&#xff08;DFT&#xff09;及其逆变换的高效算法。FFT算法由J.W.库利和T.W.图基于1965年提出&#xff0c;显著减少…

T100-XG查询报表的开发

制作XG报表 1、注册程序 azzi900 首先现将程序注册一下,在内部构建基础代码档。 2、注册作业 azzi910 也是直接新增一个,作业跟程序绑定一下。 3、T100签出规格程序 这个时候应该是没签出的,首先将规格迁出。 4、T100画面产生器 规格迁出之后,这个时候还需要生成一个画…

【探索Linux】P.37(传输层 —— TCP协议通信机制 | 确认应答(ACK)机制 | 超时重传机制)

阅读导航 引言一、确认应答(ACK)机制1. 成功接收2. 过程中存在丢包3. 引入序列号&#xff08;1&#xff09;序列号的定义&#xff08;2&#xff09;序列号的作用&#xff08;3&#xff09;序列号的工作原理&#xff08;4&#xff09;序列号和确认应答号 二、超时重传机制1. 超时…

Linux/Ubuntu访问局域网共享文件夹

文件夹中找到“Other Location”&#xff0c;输入“smb:IP地址/共享文件夹名称”&#xff0c;然后点击connect后者直接回车即可&#xff01; End&#xff01;