【Linux】进程周边006之进程地址空间

 

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

1.程序地址空间

1.1验证地址空间的排布

 1.2利用fork函数观察当子进程修改某个共享变量时父子进程分别读取到的值和地址

2.进程地址空间

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

2.2什么是进程地址空间?

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

2.4malloc和new开辟空间的原理

2.5页表与写时拷贝的更多细节


前言

在之前学习进程概念时我们提到过fork函数,了解了如何创建进程,并且知道了fork之后的父子进程代码共享,当父子对共享的变量做修改时会拷贝一份到自己这再做修改(写时拷贝),但当时对于一个变量为什么能有两个值我们的讲解仍然十分局限,今天在学习完进程地址空间后,我想你就会明白原因所在。

 欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

========================================================================= 


1.程序地址空间

在之前学习内存管理时我相信你一定见过这张图:


 当时我们说这是底层物理内存的分布,那今天我可能要告诉你他其实并不是,而只是操作系统创造出来的一个虚拟的结构,而真实的物理内存分布其实并不是如此。

但正式开始之前:我们还是来验证一下数据是不是按如图所示进行排列的呢?


1.1验证地址空间的排布

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;
}

 打印出来看看是不是这样呢?

补充知识:当一个变量被定义为static变量时,其实该变量的地址就被放到了全局变量的区域,他在某种意义上来讲就是全局变量,但是由于编译器的原因会对他进行语法上的检查等,才呈现出了静态变量的特性。 


 1.2利用fork函数观察当子进程修改某个共享变量时父子进程分别读取到的值和地址

既然我们之前在进程概念的学习中创建过子进程,那我们刚好可以观察一下当子进程修改某一共享变量时,父子进程读取到的该变量的值是否会发生改变,该变量的地址又呈现出什么样的内容?

int g_val = 100;

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);//获取子进程信息以及变量g_val的值与地址
            sleep(1);
            cnt++;
            if(cnt == 2)//2s后修改全局变量g_val的值为200
            {
                g_val = 200;
                printf("child change g_val: 100->200\n");
            }
        }
    }
    else
    {
        while(1)
        {
            printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", 
                    getpid(), getppid(), 
                    g_val, &g_val);//获取父进程信息以及变量g_val的值与地址
            sleep(1);
        }
    }
}

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


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

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

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

结论:

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

2.进程地址空间

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

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

所以我们继续往下看:


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

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

先描述,再组织。

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

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

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

 当创建子进程时,子进程会继承父进程的进程地址空间、页表等

所以我们说父子进程代码共享,数据共享,是因为他们的页表是相同的。

但对共享的变量进行修改时,会发生写时拷贝,拷贝到的代码和数据也是新开辟在物理内存上的,此时子进程只需要修改页表,虚拟地址不变,而物理地址则是新开辟的物理地址

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


2.2什么是进程地址空间?

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

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

查看Linux内核源码:


我们找到mm_struct的定义:

struct mm_struct
{
    struct vm_area_struct* mmap;    
    struct rb_root mm_rb;           
    struct vm_area_struct* mmap_cache;    

    //....

    unsingned long start_code, end_code, start_data, end_data;  
    //代码段的开始start_code ,结束end_code,数据段的开始start_data,结束end_data

    unsigned long start_brk, brk, start_stack;    
    //start_brk和brk记录有关堆的信息,
    //start_brk是用户虚拟地址空间初始化,
    //brk是当前堆的结束地址,
    //start_stack是栈的起始地址

    unsigned long arg_start, arg_end, env_start, env_end;     
    //参数段的开始arg_start,结束arg_end,
    //环境段的开始env_start,结束env_end

}

 那么如何理解各个数据存放的区域呢,如上面的源码所示:就是利用首尾的位置信息。

通过这些信息我们就可以:

  • 判断是否越界
  • 可以进行扩大和缩小范围 

区域划分的本质就是区域内的地址我们可以使用。 

可是我们又知道进程地址空间是不具备保存实际的代码和数据的能力的。

这些代码和数据实际是放置在物理内存上的。

所以就需要页表的存在来将虚拟地址转化为实际的物理内存地址

那转化的工作是谁来做呢?

  • 粗浅的说是CPU,在转化的过程中,CPU中的CR3寄存器会记录页表的地址(注意:CR3中存储的地址一定是真实的物理地址,如果是虚拟地址,那CPU还不知道页表在哪,那怎么通过映射关系找到CR3中虚拟地址映射到实际的物理地址呢),当CPU开始执行正文代码时,假设遇到了a++这样的指令,那么CPU就会根据CR3寄存器中页表的地址进行查表,从而就得到了物理内存地址,也就找到了a的值。
  • 准确的说,这个转化工作是由CPU中的硬件单元MMU(内存管理单元)完成的。

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

  • 因为有了进程地址空间和页表,物理内存空间上不连续、无序的空间就可以通过页表这一映射关系联系在一起,让进程以统一的视角看待内存。
  • 有了进程地址空间和页表后,每个进程都认为自己在独占内存,这样能更好的保障进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程管理与内存管理进行解耦合。
  • 地址空间+页表的设计是保护内存安全的重要手段!

2.4malloc和new开辟空间的原理

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

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

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

这样做有什么好处呢?

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

2.5页表与写时拷贝的更多细节

 页表其实不光存放虚拟地址和物理内存地址,还有其他的属性,比如会存放权限属性。

什么意思呢?

我们平时写代码时常量不可修改究竟是谁决定的?

  • 其实就是操作系统在页表中该数据的权限属性上放置的是'r',当你要对该数据进行修改时(写入)时,首先需要进行虚拟地址与物理地址的转化,转化的过程中操作系统发现权限为只读,所以才不可修改不可写入。

那const修饰的数据是不是也是由页表决定的呢?

  • 不是const与系统没有任何关系,const是编译器检查前后语法的问题。const的意义是将可能在未来运行时出现的错误提前在编译阶段发现并报错。所以我们说const能加则加,是一种好的编程习惯,防御性编程。

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

在父进程创建子进程时,按之前所学子进程会继承父进程的进程地址空间和页表。

并且操作系统还会将父子进程的页表中数据对应的权限属性修改为只读!

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

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

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

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


=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 每日一题 Day 17 || 二分

1901. 寻找峰值 II 一个 2D 网格中的 峰值 是指那些 严格大于 其相邻格子(上、下、左、右)的元素。 给你一个 从 0 开始编号 的 m x n 矩阵 mat ,其中任意两个相邻格子的值都 不相同 。找出 任意一个 峰值 mat[i][j] 并 返回其位置 [i,j] 。 你可以假设整个矩阵周…

怎么检测DC-DC电源模块稳定性?电源测试系统测试有什么优势?

DC-DC电源模块稳定性测试 稳定性是衡量DC电源模块的重要指标,电源模块的稳定性直接影响着电源产品和设备的工作稳定性。DC-DC电源模块的稳定性,可以通过检测输出电压、输出电流、负载、波形、效率等参数来评估。 1. 静态测试方法 静态测试是通过直流电压…

sparksql介绍

1.1 SparkSQL介绍 SparkSQL,顾名思义,就是Spark生态体系中的构建在SparkCore基础之上的一个基于SQL的计算模块。 SparkSQL的前身不叫SparkSQL,而叫Shark,最开始的时候底层代码优化,sql的解析、执行引擎等等完全基于H…

基于ssm酒店客房管理系统论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本酒店客房管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…

基于CNN+数据增强+残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)+数据集+模型(五)

系列文章目录 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型(一) 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xf…

seaborn库图形进行数据分析(基于tips数据集)

目录 一、相关性 二、变量分析 三、统计数 四、 特征值分布 五、多变量 Seaborn 是一个基于 matplotlib 的数据可视化库,可以用来绘制各种统计图表,包括散点图、条形图、折线图、箱线图等。Seaborn 提供了一些用于美化图表的默认样式和颜色主题&am…

macOS 安装 oh-my-zsh 后 node 报错 command not found : node

最近为了让终端中显示 git 分支的名称,安装了 oh-my-zsh ,安装之后呢,我原先安装的 Volta、 node 都没法用了,报错如下: 这时候粗略判断应该是系统变量出了问题,oh-my-zsh 的变量文件是 ~/.zshrc&#xff0…

旅游景区项目信息化建设运营方案:PPT47页,附下载

关键词:智慧景区解决方案,智慧景区建设,智慧景区开发与管理,智慧景区建设的意义,智慧景区管理 一、旅游景区项目信息化建设背景 1、旅游业发展迅速:随着旅游业的不断发展,游客对旅游体验的需求…

简历摘要:它是什么、为什么重要以及如何编写

然而,在这里,你有绝佳的机会用自己的语言总结你最伟大的职业品质——就像用文字创作一幅自画像一样。如果做得好,你的简历摘要可以让你的简历引人注目,立即引起招聘经理的注意。但如果做得不好,可能会立即让人倒胃口。…

排序嘉年华———快速排序优化版和非递归思想

文章目录 一.单趟排序的优化1.“挖坑法”排序2.双指针法 二.递归次数的缩减优化三.非递归方式的快排 一.单趟排序的优化 在之前文章中介绍过,霍尔大佬的单趟排序,虽然思想很厉害,但存在许多坑点,比如While循环内条件判定的繁琐&a…

延迟消息队列的几种实现方案,哪种更适合业务,要看具体情况分析

延迟消息队列的几种实现方案,延迟消息怎么实现,很多人可能一想到的是rabbitmq的死信队列来实现,但是一旦引入mq的话,就依赖这个中间件,另外维护成本,开发成本都很大,那有么有简单点的实现方式呢…

基于蓝牙传输的PM2.5测量仪(论文+源码)

1. 系统设计 当前人们对家居环境的要求越来越高,因此本课题设计了一款基于蓝牙传输的PM2.5测量仪,在功能上设计如下: 可以实时检测当前环境的PM2.5浓度;检测的PM2.5浓度可以在液晶上进行显示;检测的参数可以通过蓝牙传…

微信小程序开发从零到壹(持续更新)

1、注册或者登录到微信小程序; 小程序 补充小程序的基本信息,如名称、图标、描述等 补充小程序的服务类目,设置主营类目 AppID(小程序ID): wx710efeb42778d131 AppSecret(小程序密钥): d12a7e2b135593f6fxxxxbe35666 2…

关于“Python”的核心知识点整理大全30

目录 12.2.3 在 OS X 系统中安装 Pygame 12.2.4 在 Windows 系统中安装 Pygame 12.3 开始游戏项目 12.3.1 创建 Pygame 窗口以及响应用户输入 首先,我们创建一个空的Pygame窗口。使用Pygame编写的游戏的基本结构如下: alien_invasion.py 12.3.2 设…

电子科大软件测试~第一次作业

第一次作业及参考答案 第一题 针对电子科技大学信息门户的“密码找回”界面的邮箱输入域进行验证, 采用等价划分法设计相应的测试用例,包括尽量多的无效等价类。 答: 有效等价类如下: (1)邮箱输入学符串格式***uestc.edu.cn或***UESTC.ED…

引入sortablejs插件实现表格列拖拽功能的封装

1 参考其他文章 VueElementUI 实现 动态调整表格列 显示隐藏&显示顺序 2 具体实现 2.1 将列拖拽功能封装到通用表格动态列组件里 关于表格动态列组件的具体代码,可以看我的另一篇博客:Vue - 基于Element UI封装一个表格动态列组件。 2.2 实现思…

linux中deadline调度原理与代码注释

简介 deadline调度是比rt调度更高优先级的调度,它没有依赖于优先级的概念,而是给了每个实时任务一定的调度时间,这样的好处是:使多个实时任务场景的时间分配更合理,不让一些实时任务因为优先级低而饿死。deadline调度…

openGauss学习笔记-165 openGauss 数据库运维-备份与恢复-导入数据-使用COPY FROM STDIN导入数据-通过本地文件导入导出数据

文章目录 openGauss学习笔记-165 openGauss 数据库运维-备份与恢复-导入数据-使用COPY FROM STDIN导入数据-通过本地文件导入导出数据165.1 示例1:通过本地文件导入导出数据 openGauss学习笔记-165 openGauss 数据库运维-备份与恢复-导入数据-使用COPY FROM STDIN导…

Hutool--DFA 敏感词工具类

使用hutool的dfa工具类可以很好的帮助我们来实现敏感词过滤的功能,下面从用例入手来逐步地去j简单了解一下dfa工具类。 字典树 DFA算法的核心是建立了以敏感词为基础的许多敏感词树(字典树)。 它的基本思想是基于状态转移来检索敏感词。 字…

C++复合数据类型:vector|string

文章目录 模板类vector初始化访问修改添加 标准库类型string初始化访问拼接比较字符串 模板类vector 初始化 访问 修改 添加 数组长度在初始化时已经定义,访问范围也有限,数组长度还得通过计算 所以C中定义了很多扩展的“抽象数据类型”&#xff0c…
最新文章