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

news2024/12/25 9:33:58

 

👀樊梓慕:个人主页

 🎥个人专栏:《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.coloradmin.cn/o/1326505.html

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

相关文章

Git 储藏(stash)用法

储藏的基本用法 保存当前的更改 1、查看储藏 git stash list2、更改保存到一个储藏中: git stash save "info"其中,“info” 是可选的注释信息,可以简要描述这个储藏的内容。 3、恢复之前保存的更改 可以使用下面的命令将之前…

Qt之判断一个点是否在多边形内部(射线法)

算法思想: 以被测点Q为端点,向任意方向作射线(一般水平向右作射线),统计该射线与多边形的交点数。如果为奇数,Q在多边形内;如果为偶数,Q在多边形外。计数的时候会有一些特殊情况。这种方法适用于任意多边形,不需要考虑精度误差和多边形点给出的顺序,时间复杂度为O(n)…

Java 第14章 集合 课堂练习

文章目录 HashSet判断是否两次add都能加入成功HashSet编码遍历HashMap判断输出中是否有"abc" HashSet判断是否两次add都能加入成功 HashSet set new HashSet(); set.add(new String("hsp")); set.add(new String("hsp"));第一次可以&#xff0…

HTML期末复习,重要知识点摘录

HTML复习知识点摘录 1.html基本模板 <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>菜鸟教程(runoob.com)</title> </head> <body><h1>我的第一个标题</h1> <p>我的第一个段落。…

Docker 文件和卷 权限拒绝

一 创作背景 再复制Docker影像文件或访问Docker容器内已安装卷上的文件时我们常常会遇到&#xff1a;“权限被拒绝”的错误&#xff0c;在此&#xff0c;您将了解到为什么会出现“权限被拒绝”的错误以及如何解决这个问题。 二 目的 在深入探讨 Docker 容器中的 Permission De…

如何使用Docker搭建青龙面板并结合内网穿透工具发布至公网可访问

文章目录 一、前期准备本教程环境为&#xff1a;Centos7&#xff0c;可以跑Docker的系统都可以使用。本教程使用Docker部署青龙&#xff0c;如何安装Docker详见&#xff1a; 二、安装青龙面板三、映射本地部署的青龙面板至公网四、使用固定公网地址访问本地部署的青龙面板 正文…

TikTok获客工具开发必不可少的功能!

随着TikTok在全球范围内的走红&#xff0c;越来越多的企业开始关注这个平台并尝试从中获取客户&#xff0c;为了满足这一需求&#xff0c;市场上涌现出了许多TikTok获客工具。 这些工具的功能各有千秋&#xff0c;但有一些功能是必不可少的&#xff0c;本文将为您介绍TikTok获…

FMCW雷达仿真:基于L形阵列4D点云获取

摘要&#xff1a;本期内容为3D点云目标获取的延续工作&#xff0c;在距离、速度、方位角估计的基础上&#xff0c;通过设计L型阵列结构&#xff0c;进一步实现目标俯仰角的估计&#xff0c;最终实现目标4-D点云的获取。首先&#xff0c;通过中频信号建立仿真信号模型&#xff0…

MySQL中替换字符串中的指定部分之REPLACE函数

REPLACE函数是用来替换字符串中的指定部分内容的。在本文中&#xff0c;将介绍如何在MySQL中使用REPLACE函数进行字符串替换 REPLACE函数的语法&#xff1a; REPLACE(str, search_str, replace_str) 其中&#xff0c;str是要进行替换操作的字符串&#xff0c;search_str是要搜…

WebGL开发虚拟旅游应用

WebGL可以用于开发虚拟旅游应用&#xff0c;提供用户在浏览器中探索虚拟景点和环境的交互体验。以下是在WebGL中开发虚拟旅游应用的一般流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.需求分析…

用于电磁炉、电压力锅等小家电的LED电源芯片型号汇总表

LED电源芯片是一种用于电磁炉、电压力锅等小家电的关键元器件。它具有高效、稳定、可靠的特点&#xff0c;能够满足小家电对电源的各种需求。 首先&#xff0c;LED电源芯片具有高效能的特点。它采用先进的功率转换技术&#xff0c;能够将输入的电压稳定地转换为输出所需的电压…

Redis原理之网络通信协议笔记

目录 1. RESP协议 ​2. 自定义Socket连接Redis 1. RESP协议 2. 自定义Socket连接Redis public class MyRedisClient {static Socket s;static PrintWriter writer;static BufferedReader reader;static Object obj;public static void main(String[] args) {try {// 1.建立连…

使用阿里云性能测试工具 JMeter 场景压测 RocketMQ 最佳实践

作者&#xff1a;森元 需求背景 新业务上线前&#xff0c;我们通常需要对系统的不同中间件进行压测&#xff0c;找到当前配置下中间件承受流量的上限&#xff0c;从而确定上游链路的限流规则&#xff0c;保护系统不因突发流量而崩溃。阿里云 PTS 的 JMeter 压测可以支持用户上…

ros2/ros 4轮2驱机器人xacro/urdf文件示例代码

这个实验中最重要的是&#xff1a;colcon build 之后要记得source install/setup.bash.否则修改的文件是不会更新的。知道了吧 <robot name"half" xmlns:xacro"http://wiki.ros.org/wiki/xacro"><xacro:property name"PI" value"3…

紫光展锐T820与百度飞桨完成I级兼容性测试 助推端侧AI融合创新

近日&#xff0c;紫光展锐高性能5G SoC T820与百度飞桨完成I级兼容性测试&#xff08;基于Paddle Lite工具&#xff09;。测试结果显示&#xff0c;双方兼容性表现良好&#xff0c;整体运行稳定。这是紫光展锐加入百度“硬件生态共创计划”后的阶段性成果。 本次I级兼容性测试完…

什么猫粮比较好?5款自用质量好的主食冻干排行榜

冻干猫粮因其高营养和适口性&#xff0c;受到了众多铲屎官们的喜爱和追捧。冻干猫粮的喂养方式非常简单&#xff0c;可以直接喂食&#xff0c;也可以将冻干复水后喂食&#xff0c;根据猫咪的不同喜好可以选择不同的喂养方式。然而&#xff0c;有些铲屎官在选择冻干猫粮时可能会…

Electron窗口标题栏位置异常?教你妙招解决!

Electron 是一个开源的桌面应用程序开发框架&#xff0c;它允许使用常用的 web 技术&#xff08;HTML、CSS、JavaScript&#xff09;构建跨平台的桌面应用。然而&#xff0c;在使用 Electron 开发应用时&#xff0c;你可能会面临不同操作系统之间的兼容性问题 本文将探讨在 El…

Latex-algorithm2e中将Algorithm修改为中文 “算法”

方法&#xff1a; 中文就是要放在中文环境里&#xff0c;使用如下 \renewcommand{\algorithmcfname}{算法} 效果由&#xff1a; 变成 参考&#xff1a; LaTeX技巧&#xff1a;算法标题 Algorithm如何重命名_name{algorithm}{算法}-CSDN博客 &#xff08;该文章提供了两个算法…

H5小游戏加固方案

今年的中国游戏产业年会上&#xff0c;小游戏成了万众瞩目的行业新风口。据伽马数据统计&#xff1a;2023年小游戏市场规模可达200亿元&#xff0c;同比增长300% 。 小游戏有着分发更精准、用户转化率更高、研发成本更低、场景适用性更强等优势&#xff0c;具备巨大的市场潜力…

五、从0开始卷出一个新项目瑞萨RZN2L之RZT2L BaseProject coremark的移植

感谢来自b站热心帅气的同学分享的RZT2L移植经验总结的md文档 1 需要注意的小点 1.1 使用Flash运行调试前&#xff0c;新板子需要erase 1.2 在线debug&#xff0c;需要修改startup.c 2 coremark工程建立 2.1 工程创建 2.2 src 用户c代码移植 2.3 debug调试 …