Linux | 进程地址空间

news2024/12/16 5:33:11

目录

前言

一、初始进程地址空间

1、实验引入

2、虚拟地址空间

二、什么是进程地址空间

1、基本概念 

2、深入理解进程地址空间

3、进程地址空间的本质

4、遗留问题解决

三、为什么要有进程地址空间

1、知识扩展

2、进程地址空间存在意义

3、重新理解挂起


前言

         本章节主要介绍关于进程地址空间相关概念,我们从一个实验引出我们的进程地址空间,接着一步一步深入了解进程地址空间,细化周边概念;

一、初始进程地址空间

1、实验引入

        我们有如下代码,观察代码运行现象;

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int g_val = 10; // 已初始化全局变量 
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        // fork执行失败
        perror("fork");
        exit(-1);
    }
    else if(id == 0)
    {
        // 子进程
        int cnt = 0;
        while(1)
        {
            if(cnt == 5)
            {
                g_val = 20;
            }
            printf("我是子进程,g_val:%d, &g_val:%p\n", g_val, &g_val);
            cnt++;
            sleep(1);
        }
    }
    else 
    {
        // 父进程
        while(1)
        {
            printf("我是父进程,g_val:%d, &g_val:%p\n", g_val, &g_val);
            sleep(1);
        }
        
    }                                                                                                                                                                                   
    return 0;
}

        我们编译运行上述代码,结果如下所示;

        这跟我们前面fork函数返回值遗留下的问题一模一样,到底为什么会出现这种神奇现象呢?本文主要探究的就是这个;

2、虚拟地址空间

        不知道大家在以前的学习中是否见过下图(32位机器下);

        我想这是每一个学计算机的都应该见过的图吧,这就是我们今天的核心虚拟地址空间,我们将空间按上面划分,分配地址,我们可以根据上图写出相应代码,是否如图所示;

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

// 未初始化全局变量
int g_unval;
// 已初始化全局变量
int g_val = 10;

int main(int argc, char* args[], char* env[])
{
    // 代码段
    printf("code addr: %p\n", main);
   
    // 常量
    const char* str = "hello world";
    printf("constant quantity: %p\n", str);
    
    // 已初始化全局变量
    printf("init global var: %p\n", &g_val);
    // 未初始化全局变量
    printf("uninit global var: %p\n", &g_unval);
    // 堆
    char* p1 = (char*)malloc(10);
    char* p2 = (char*)malloc(10);
    char* p3 = (char*)malloc(10);
    printf("heap: %p\n", p1);
    printf("heap: %p\n", p2);
    printf("heap: %p\n", p3);
    // 栈 
    printf("stack: %p\n", &p1);
    printf("stack: %p\n", &p2);                                                                                                                        
    printf("stack: %p\n", &p3);

    // 命令行参数与环境变量
    printf("args[0]: %p\n", args[0]); 
    printf("args[1]: %p\n", args[1]); 
    printf("args[2]: %p\n", args[2]);

    printf("env[0]: %p\n", env[0]);
    printf("env[1]: %p\n", env[1]);
    printf("env[2]: %p\n", env[2]);
    return 0;
}

        测试结果如下图所示;

        结果与我们预料的相同,整体地址都在增大,与我们上面的进程地址空间分布图一样,这是在Linux下的测试结果,在window下测试结果可能会有差异,这是可能由于编译器的优化造成的结果;

二、什么是进程地址空间

1、基本概念 

        进程地址空间就是从进程的视角看到的内存空间,实际上,我们会通过一种数据结构记录从虚拟地址到物理地址的映射;

2、深入理解进程地址空间

        要理解这个,我们首先把时间线拉到以前,计算机刚开始时,没有进程地址空间这一概念,我们写的程序是直接使用内存的物理地址来访问内存上的数据的;如下图所示;

        此时,我们要执行一个程序,我们首先将可执行程序加载进内存,并生成对应的PCB控制块,在CPU下的就绪队列排队等待调度;看着好像没啥问题,若此时我们调用A时,A程序越界访问了,直接修改了我们B进程的代码,导致B进程直接崩溃了,这时进程还哪里来的独立性,进程的独立性完全靠程序员代码的正确性;故这种让进程直接访问真实的物理地址是不可靠的;

        而我们现代计算机,不会使用上述策略,我们引入了一个虚拟地址,使得程序无法直接访问真实物理内存,如下图所示;

        当我们程序加载进内存时,首先,操作系统会生成对应该进程的PCB控制块(task_struct)、进程地址空间(mm_struct)和用户级页表,这些合起来我们称作进程,即 进程 = 内核数据结构 + 代码和数据;对于每个进程来说,它们都认为自己的地址是从 0x0000 0000 到 0x FFFF FFFF,这些地址都是虚拟地址,CPU通过这些虚拟地址经过页表映射到真实的物理地址来操作内存的数据;

        问题来了,多搞出了一个虚拟地址,最终还不是通过虚拟地址映射到物理地址来访问内存中数据,那么虚拟地址也是一个非法的地址呢?那不也越界访问了?

        实际上,若虚拟地址也是一个非法地址,在页表这就会被发现出来,根本不会映射到非法的物理地址,也就不可能影响到别的进程;

3、进程地址空间的本质

        仔细想一下,既然每一个进程都要配一个进程地址空间,那内存中不可能只有一个进程,因此进程地址空间也不可能只有一个,既然有很多个,那么我们的操作系统是否需要将这些进程地址空间管理起来,那么如何管理这些进程地址空间呢?同样,“先描述,再组织”,我们首先用一个结构体将这个进程地址空间描述起来,再用一种数据结构将这些结构体组织起来,方便我们对这些结构体进行增删查改,这不就跟操作系统对PCB控制块的管理同出一辙吗?在Linux下,这个数据结构就叫做mm_struct;

        如何描述呢?我们想一想,进程地址空间不就是一个又一个区域吗?那我们可以肯定的是,肯定会进行分区,那么如何进行分区呢?不就是一个记录其实位置一个记录结束位置吗?如下所示;

strcut mm_strcut
{
    // 代码段
    int code_start, code_end;
    // 栈区
    int stack_start, stack_end;
    // 堆区
    int heap_start, heap_end;
    // 等等... 其他属性

};

        这样不就将进程地址空间描述起来了吗?对于栈区和堆区这种区间会增长的呢?如何维护?我们直接改变其start或end值不就可以了吗?进程地址空间,实际上就是一个结构体,且我们之前学过的PCB控制块 task_struct 中也保存了 mm_struct 的指针;

4、遗留问题解决

        我们开始那个实验同一个变量,同一个地址,为什么会有不同的值呢?还有fork函数的返回值,为什么也是一个变量有两个值呢?这些问题就很好进行解答了,如下图;

        当创建一个子进程后,若没有进程对g_val进行修改,也就是我们程序的前5秒钟,父进程和子进程都是通过页表映射到同一块物理地址空间,由于子进程是继承于父进程,因此页表、PCB和进程地址空间等信息也有很多是拷贝于父进程,它们的g_val的虚拟地址都是相同的,若此时子进程修改了g_val;如下图;

        此时,由于子进程要对数据进行修改,故我们的OS会重新开辟一块空间,并修改页表映射关系,此时我们的子进程就会将数据写入新开辟的那一块空间,而页表项中仅仅更改映射到物理地址那一块数据,所以虚拟地址并没有发生改变,故我们会看到地址相同,里面存的值不同的现象;fork的返回值也是如此,发生了写时拷贝现象;

三、为什么要有进程地址空间

1、知识扩展

        在我们回答这个问题之前,我们首先补充一个知识,我们的程序编译完以后,生成可执行程序,那么这个可执行程序里有地址吗?如果有,是什么地址?

        实际上,虚拟地址的概念不仅仅只是存在我们的操作系统内部,我们的编译器也要遵守这个,因此我们在编译后,程序中内部已经使用了虚拟地址,也会存在各种段,如数据段,代码段等;直到我们的程序加载进内存后,会给我们的程序分别配物理空间,填充页表的映射;

2、进程地址空间存在意义

        其一,一旦我们有了进程地址空间,我们的CPU看到的一切地址都是虚拟地址,所有的地址都需要通过页表映射才可以找到真实的物理地址,这样做可以有效的保护内存,防止越界修改数据,影响其他进程;一旦用户有越界修改数据行为,我们在页表就可以发现这种行为,拒绝进行操作,直接杀死当前进程,从而保证了内存的安全;

        其二,有了地址空间后,我们的程序由于页表映射在真实内存中不一定是连续的,如何给我们的程序分配内存可以由我们的内存管理模块决定,如何分配与我们的进程管理模块无关,因为我们有了进程地址空间,我们有了虚拟地址即可,真实物理地址在哪我们并不关心,如何分配我们也不关心,我们只需要通过页表与物理内存空间建立映射即可,这样就完成了 进程管理模块内存管理模块 的解耦合;

补充:

        我们C语言的 malloc函数 与C++的 new 申请的空间地址是什么地址?根据上述,我们不难判断,申请的是虚拟地址,那么问题又来了,若我们申请完这块空间不立即使用,OS系统会为我们申请的这块虚拟空间申请物理内存空间吗?答案是否定的,当然不会,若我们就申请malloc虚拟空间不使用,OS同时为我们申请了物理空间,那么这块物理空间也不会被使用,那么资源不是白白的浪费掉了吗?

        操作系统的做法是当我们调用 malloc 这种函数时,操作系统首先会为我们申请虚拟内存空间,并填充进页表,但是不会为我们申请真实物理空间,一旦我们要使用时,操作系统会为我们申请真实物理空间,且完成页表映射关系;

        这种延时分配的策略大大的提高了内存的利用率,且对进程来说是0感知的,因为我们的进程并不关心 内存管理,只关心虚拟内存空间,在页表中找对应映射;

        其三,由于页表+进程地址空间,我们的可以将我们的代码和数据分散放在物理内存的任意位置,但在进程视角看着就像连续的,比如我们创建一个变量a,接着再创建一个变量b,我们这两个变量在虚拟地址上可以看作是连续的,但是再物理内存种不一定连续,因此我们物理地址是通过页表映射到任意位置,可以是无序的;对于计算机里的多个进程来说,只要能保证页表映射的正确性,就能保证进程间的独立性!

3、重新理解挂起

        我们可以通过上面理论再次理解挂起现象,所谓挂起,就是由于某种情况,我们需要将内存种某些进程的代码和数据暂存到磁盘中的交换区处;那么我们再次思考一下,我们加载进程的时候有没有可能不将代码数据放进内存中呢?

        实际上是有可能的,当我们的内存资源极度紧张时,我们运行一个可执行程序,也就是加载这个程序进内存,可是我们内存已经非常紧张了,我们可以先在OS上创建这个可执行程序对应的PCB控制块、进程地址空间与页表等等,代码和数据暂存在内存中,这不也是我们挂起的本质吗?

        有了以上认知,再想一想我们平时在电脑上玩的大型游戏,动辄就是一百多GB,例如GTA5,而我们的内存却只有8G或者16G,那么是如何将这么大的可执行程序加载进内存中的呢?首先,完全加载进内存肯定是不现实的,上面我们谈挂起是只创建内核数据,而不将代码和数据加载进内存,那么我们肯定也可以部分加载呀,当我们需要用哪些代码和数据是就加载进内存,长期不用时,换出内存,这样就可以造成一个我们将整个程序加载进内存的假象了;

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

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

相关文章

[SQL开发笔记]SQL 别名:为表名称或列名称指定别名

一、功能描述&#xff1a; 通过使用 SQL&#xff0c;可以为表名称或列名称指定别名。基本上&#xff0c;创建别名是为了让列名称的可读性更强。 二、SQL 别名语法详解&#xff1a; &#xff08;1&#xff09;列的 SQL 别名语法&#xff1a; Select column_name AS alias_nam…

X86(32位)汇编指令与机器码转换原理

X86&#xff08;32位&#xff09;汇编指令与机器码转换原理 1 32位寻址形式下的ModR/M字节2 汇编指令转机器码2.1 mov ecx,[eaxebx*2]2.1.1 查Opcode和ModR/M2.1.2 查SIB 2.2 mov ecx,[eaxebx*210h]2.3 mov ecx,[eaxebx*200000100h] 本文属于《 X86指令基础系列教程》之一&…

Spring Cloud Gateway + Knife4j 4.3 接口文档整合和网关聚合

目录 前言Spring Cloud 整合 Knife4jpom.xmlapplication.ymlSwaggerConfig.java访问单服务接口文档 Spring Cloud Gateway 网关聚合pom.xmlapplication.yml访问网关聚合接口文档 接口测试**登录认证**获取登录用户信息 结语源码 前言 youlai-mall 开源微服务商城新版本基于 Sp…

数据库SqlServer笔试题

数据库SqlServer笔试题 一、数据库基础知识&#xff08;通用&#xff09;篇 1.说说主键、外键、超键、候选键 超键&#xff1a;在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键&#xff0c;多个属 性组合在一起也可以作为一个超键。超键包含…

CSGO游戏搬砖的10个冷知识,90%的人还不知道

敲黑板了&#xff0c;同学们&#xff0c;很多人号一多就蒙圈&#xff0c;接下来解释一下buff号、Steam号、选品软件、识别码&#xff0c;桌面令牌&#xff0c;手机令牌&#xff0c;几者的关系: 1、前期一套实名只能绑定两个buff号&#xff0c;steam账号无需实名&#xff0c;后期…

安装和配置Maven,IDEA中配置Maven

一、安装 Maven 首先创建一个空的文件夹&#xff0c;文件夹不能是中文或者其他特殊字符&#xff0c;然后将maven文件解压到这个空的文件夹 解压 二、配置环境变量 电脑右键 高级系统设置 输入以下内容 编辑好之后一直点击确定 验证是否安装成功&#xff0c;winR输入cmd进入命令…

docker 部署 若依 Ruoyi springboot+vue分离版

本篇从已有虚拟机/服务器 安装好dokcer为基础开始讲解 1.部署mysql 创建conf data init三个文件夹 conf目录存放在mysql配置文件 init目录存放着若依数据库sql文件&#xff08;从navicat导出的并非若依框架自带sql&#xff09; 创建一个属于本次若依部署的网段&#xff08;只…

nc65单据穿透

nc65单据穿透 jych项目 1.支出合同台账合同号字段可以穿透到对应的单据上 一个合同号穿透到一个物质设备采购合同上 1.支出合同台账单据模板合同号字段卡片下和列表下高级属性勾选是否超链接 2.支出合同台账中增加监听类 HeadItemLinkListener.java package nc.ui.jych.…

如何处理 Python 报错 can‘t multiply sequence by non-int of type float

大多数时候&#xff0c;当你在编程时遇到错误&#xff0c;你可以在错误信息中发现错误发生的原因以及如何解决它。 Python 错误 “TypeError: can’t multiply sequence by non-int of type float” 也不例外。 我准备了这篇文章&#xff0c;向你展示这个错误发生的原因以及如…

目标检测及锚框、IoU

1. 目标检测 物体检测&#xff08;目标检测&#xff09;是计算机视觉和数字图像处理的热门方向&#xff0c;意在判断一幅图像上是否存在感兴趣物体&#xff0c;并给出物体分类及位置等&#xff08;What and Where&#xff09;。本文主要进行物体检测研究背景、发展脉络、相关算…

MySQL的概念和sql语句

数据库的概念 数据库的概念&#xff1a;数据库是用来组织&#xff08;各个数据之间是有关联&#xff0c;是按规则组织起来的&#xff09;&#xff0c;存储和管理&#xff08;增&#xff0c;删&#xff0c;改&#xff0c;查&#xff09;的仓库 数据库管理系统&#xff08;DBMS&…

写保护设置——三、I2C EEPROM

三、I2C EEPROM I2C通讯的EEPROM只有硬保护&#xff0c;没有软保护。 以AT24C01A/02/04/16型EEPROM和AT24C02A/04A/08A/16A型EEPROM为例&#xff0c;管脚定义和写保护WP功能分别如下。 &#xff08;1&#xff09;AT24C01A/02/04/16型EEPROM 规格书&#xff1a; AT24C01A/02…

软件测试行情不好,我还是啃下了27K的offer

o “会代码吗&#xff1f;” o “会&#xff0c;Java、Python我都会一些&#xff01;” o “有没有用代码开发过一些测试工具平台呢&#xff1f;” o “额。。。这个。。。没做过。。。” o “那你回去等消息吧” 软件测试行业发展到今天&#xff0c;测试人员会代码&#x…

Jtti:Apache服务的反向代理及负载均衡怎么配置

配置Apache服务的反向代理和负载均衡可以帮助您分散负载并提高应用程序的可用性和性能。下面是一些通用的步骤&#xff0c;以配置Apache反向代理和负载均衡。 1. 安装和配置Apache&#xff1a; 确保您已经安装了Apache HTTP服务器。通常&#xff0c;Apache的配置文件位于/etc…

城中村智能水电表改造,提升居民生活品质

随着我国城市化进程的加快&#xff0c;城中村成为了城市发展的焦点。然而&#xff0c;由于历史原因&#xff0c;城中村的水电设施普遍存在老化、破损等问题&#xff0c;给居民生活带来诸多不便。为了提升城中村居民的生活品质&#xff0c;智能水电表的推广和改造已成为当务之急…

css:button实现el-radio效果

先看最终效果&#xff1a; ​​​ 思路&#xff1a; 一、 首先准备好按钮内容&#xff1a;const a [one,two,three] 将按钮循环展示出来&#xff0c;并设置一些样式&#xff0c;将按钮背景透明&#xff1a; <button v-for"(item,index) in a" :key"in…

创造健康、造福一方,强强联合共筑新疆妇幼健康梦

改善优生优育全程服务&#xff0c;提供优质生育全程医疗保健服务&#xff0c;强化孕前优生健康检查、产前筛查以及不孕不育诊治服务等&#xff0c;是国家《“十四五”国民健康规划》的重要内容。为向全疆群众提供国内一流的妇幼保健诊疗服务&#xff0c;满足群众“生的出、生的…

【威联通】共享文件夹设置

1. 前期准备 下载 QNAPQsyncClient 然后安装此软件&#xff0c;安装的目标文件夹可以更换到其他盘&#xff0c;如下图所示 点击 确定 点击 完成 弹出框中选择 中国 然后点击 套用 点击 跳过 点击 完成 勾选方框&#xff0c;然后点击 确定 第一个框中输入需要同步的 NAS IP 地址…

MFA-Conformer

基于多尺度特征聚合Conformer说话人识别模型的创新与应用 论文&#xff1a;https://arxiv.org/abs/2203.15249 代码&#xff1a;GitHub - zyzisyz/mfa_conformer 收录于 INTERSPEECH 2022 1. 简介 本文由清华大学与腾讯科技&#xff08;北京&#xff09;有限公司、台湾大学…

Android intent的一些小使用

目录&#xff1a; 1. Test5.java2. activity_main5.xml3. Empty.java (这个是用来带参数打开Activity按钮用的)4. activity_empty.xml5. 总结 一些基本的问题就不进行说明了&#xff0c;直接上代码&#xff01;&#xff01;&#xff01; // 最后的隐形intent和带返回值没有解决…