Linux | 进程地址空间

目录

前言

一、初始进程地址空间

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.mzph.cn/news/120491.shtml

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

相关文章

Vue引入异步组件

defineAsyncComponent 函数&#xff1a;异步引入组件。 Suspense 标签&#xff1a;异步引入组件时&#xff0c;显示默认的内容。 异步引入组件的基本使用&#xff1a; 异步引入组件&#xff1a; import { defineAsyncComponent } from vue; const Child defineAsyncComponen…

没有电脑也不用担心,在Android设备上也可以轻松使用ppt

PowerPoint是制作幻灯片的好工具&#xff0c;无论是工作、学校还是个人使用。但有时你无法使用电脑或笔记本电脑&#xff0c;你必须在旅途中做演示。 这就是PowerPoint for Android派上用场的地方。它允许你在移动设备上创建、编辑和呈现幻灯片。以下是要遵循的步骤&#xff1…

【单例模式】饿汉式,懒汉式?JAVA如何实现单例?线程安全吗?

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 单例设计模式 Java单例设计模式 Java单例设计模…

031-从零搭建微服务-监控中心(一)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;mingyue: &#x1f389; 基于 Spring Boot、Spring Cloud & Alibaba 的分布式微服务架构基础服务中心 源…

KMS在腾讯云的微服务实践助力其降本50%

背景介绍 KMS 是一家日本的游戏公司&#xff0c;主要经营游戏业务、数字漫画业务、广告业务、云解决方案业务等&#xff0c;出品了多款在日本畅销的漫画风游戏&#xff0c;同时有网络漫画专业厂牌&#xff0c;以内容创作为目标&#xff0c;拥有原创 IP 创作、游戏开发等多元化发…

第三篇:实践篇 《使用Assembler 实现图片任意切割功能》

实现原理&#xff1a; 共用一个texture、material、渲染状态等。紧通过修改vertex、uvs、indexes数据即可实现任意切割功能。 一、线段分割多边形&#xff0c;并分散多边形 线段分割多边形 已知多边形points&#xff0c;线段sp、ep。线段分割多边形得到两个多边形。 publi…

竞赛选题 深度学习人体跌倒检测 -yolo 机器视觉 opencv python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习的人体跌倒检测算法研究与实现 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满…

oracle,CLOB转XML内存不足,ORA-27163: out of memory ORA-06512: at “SYS.XMLTYPE“,

通过kettle采集数据时&#xff0c;表输入的组件&#xff0c;查询报错。 ORA-27163: out of memory ORA-06512: at “SYS.XMLTYPE”, line 272 ORA-06512: at line 1 通过 ALTER SESSION SET EVENTS ‘31156 trace name context forever, level 0x400’; 修改会话配置 或直接修改…

网络协议--广播和多播

12.1 引言 在第1章中我们提到有三种IP地址&#xff1a;单播地址、广播地址和多播地址。本章将更详细地介绍广播和多播。 广播和多播仅应用于UDP&#xff0c;它们对需将报文同时传往多个接收者的应用来说十分重要。TCP是一个面向连接的协议&#xff0c;它意味着分别运行于两主…

安卓开发实例:方向传感器

调用手机的方向传感器&#xff0c;X轴&#xff0c;Y轴&#xff0c;Z轴的数值 activity_sensor.xml <?xml version"1.0" encoding"utf-8"?> <androidx.constraintlayout.widget.ConstraintLayoutxmlns:android"http://schemas.android.c…

结构伪类选择器

伪类选择器&#xff1a;用来描述一个元素的特殊状态&#xff01;比如第一个元素、某个元素的子元素、鼠标点击的元素 1 first-child/last-child /*ul的第一个子元素*/ ul li:first-child{ background: #0f35ad; } /*ul的最后一个子元素*/ ul li:last-child{ background: #0f3…

ExcelPatternTool 开箱即用的Excel工具包现已发布!

文章目录 ExcelPatternTool功能特点&#xff1a;快速开始使用说明常规类型高级类型Importable注解Exportable注解IImportOption导入选项IExportOption导出选项单元格样式StyleMapping样式映射使用数据库作为数据源 示例Sample1&#xff1a;不同类型字段导出Sample2&#xff1a;…

ChatGLM系列七:LangChain+ChatGLM-6B

背景介绍 在开发过程中&#xff0c;我们经常会遇到需要构建基于本地知识库的问答系统的问题。这种系统需要能够根据用户提供的问题&#xff0c;在本地的知识库中查找并返回相关答案。然而&#xff0c;要实现这样的功能并不容易&#xff0c;涉及到语言模型的选择、知识库的管理…

Vsan数据恢复—Vsan存储断电导致虚拟机无法启动的数据恢复案例

Vsan分布式存储故障&检测&#xff1a; 异常断电导致一台vsan存储设备上层虚拟机无法启动。 将故障Vsan存储上的所有磁盘编号后取出&#xff0c;由北亚企安的硬件工程师对vsan存储上的所有硬盘进行物理故障检测&#xff0c;经过检测没有发现硬盘存在物理故障。 vsan存储结构…

问界新M7也扛起“遥遥领先”大旗,华为究竟做对了什么?

循着时间脉络&#xff0c;华为似乎正在实现全方面的“遥遥领先”。 继Mate 60系列手机产品的强势回归之后&#xff0c;华为参与的汽车项目也登上了“热搜榜”。近段时间&#xff0c;问界新M7正处于“卖爆”状态。 据了解&#xff0c;2023年9月12日&#xff0c;华为发布了问界…

公网IP怎么设置?公网ip有哪些优点和缺点?

随着互联网的普及&#xff0c;越来越多的人开始关注网络安全和隐私保护。其中&#xff0c;公网IP的设置成为了一个备受关注的话题。本文将详细介绍公网IP的设置方法以及公网IP的优点和缺点。 一、公网IP设置方法 1. 路由器设置 在家庭或企业网络中&#xff0c;路由器通常是最重…

【Linux】开发工具

目录 Linux编译器-gcc/g使用执行命令&#xff1a;我们的.o和库是如何链接的? make/Makefile依赖关系、依赖方法 Linux编译器-gcc/g使用 gcc只能编译c语言&#xff0c;g可以编译c语言也可以编译g 背景知识&#xff1a; 预处理&#xff08;进行宏替换)编译&#xff08;生成汇编)…

荣耀推送服务消息分类标准

前言 为了提升终端用户的推送体验、营造良好可持续的通知生态&#xff0c;荣耀推送服务将对推送消息进行分类管理。 消息分类 定义 荣耀推送服务将根据应用类型、消息内容和消息发送场景&#xff0c;将推送消息分成服务通讯和资讯营销两大类别。 服务通讯类&#xff0c;包…

JS清除字符串中的空格

一、replace()方法 replace方法在字符串中搜索值或正则表达式&#xff0c;返回已替换值的新字符串&#xff0c;不会更改原始字符串。 去除字符串内所有的空格&#xff1a;str str.replace(/\s*/g,“”) 去除字符串内两头的空格&#xff1a;str str.replace(/^\s*|\s*$/g,“…

Mac怎么清理磁盘空间?释放Mac磁盘空间有效方法

相信很多使用macOS系统的小伙伴都收到过提示“磁盘空间已满”消息&#xff0c;尤其是采用SSD固态硬盘的MacBook系列&#xff0c;120G的硬盘空间本就捉襟见肘&#xff0c;使用一段时间后&#xff0c;即使自己没有存放很多大文件&#xff0c; Mac的磁盘很快就满了。那么&#xff…