Linux进程(三)---深入理解进程地址空间

news2024/11/17 8:34:08

目录

地址空间的划分及验证

所谓的地址空间是内存吗?

一种奇怪的现象(虚拟地址的引入)

什么是进程地址空间?

我们平常访问到的内存是物理内存吗?

深入理解区域划分

再谈奇怪的现象

fork()中为什么一个变量可以同时保存两个不同的值

拓展:程序在没有编译完成被加载到内存的时候,程序内部有地址吗?


地址空间的划分及验证

我们之前写代码时,经常会定义一些变量,包括静态变量,全局变量,普通变量,或者自己申请空间的变量等等,这些变量在内存中也会有自己相应的位置,这张图我相信大家或多或少见过.

这张图无论见过还是没有,都一定要牢牢记住各区在地址空间中的位置.

注意堆区中的变量地址上向上生长的,而栈区是向下生长. 

我们写如下代码代码验证这张图.

  1 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 //定义未初始化全局数据  
  4 int g_unInitVal;  
  5 //定义初始化全局数据  
  6 int g_InitVal=0;  
  7 int main()  
  8 {  
  9   printf("text:%p\n",main);  
 10   printf("init:%p\n",&g_InitVal);  
 11   printf("Uninit:%p\n",&g_unInitVal);                                                                                                                                                                        
 12     
 13   //定义堆区数据  
 14   char* p1 = (char*)malloc(sizeof(char));  
 15   char* p2 = (char*)malloc(sizeof(char));  
 16   char* p3 = (char*)malloc(sizeof(char));  
 17     
 18   printf("heap1:%p\n",p1);  
 19   printf("heap2:%p\n",p2);  
 20   printf("heap3:%p\n",p3);  
 21   
 22   //定义栈区数据  
 23   int a = 1,b = 2,c = 3;  
 24   
 25   printf("stack:%p\n",&a);  
 26   printf("stack:%p\n",&b);  
 27   printf("stack:%p\n",&c);  
 28   
 29   return 0;  
 30 }  

我们退出vim,make编译,然后运行:

也可以看到堆区heap和栈区stack之间相差了很大一部分地址,这部分空间叫做共享空间,这个后面部分会讲解. 

同时说明一下:以上结论只在Linux下有效,但是大部分进程地址空间都是以linux为主的.

所谓的地址空间是内存吗?

那么有个问题:上面画的那段空间是内存吗?

按我们的认知是,我们每当运行程序起来,各种变量或者代码都会被暂时保存到内存这些划分的区域中,方便后面使用.

但答案是那张图不是真正意义的内存!

那它究竟是什么呢,地址空间这方面很抽象,这个本文后面会说。单独讲解一个方面会扯出很多相关的知识,所以我们先是搭好架子,然后再逐步引入讲解.

我们观察下面这种现象:

一种奇怪的现象(虚拟地址的引入)

我们知道,父进程fork()创建一个子进程,子进程会共享父进程的全局变量,所以它们的地址也是一样的.

 我们make编译一下:

发现两个全局变量的地址,确实是一样的.

我们如果此时对代码稍加改动,在子进程中,将g_val值修改为20,然后再次观察父子进程的值.

  我们退出,然后make编译一下:

 此时很奇怪的现象便发生了,父子进程全局变量g_val的值竟然不一样.

但很怪的是它们的地址却是相同的同一个地址,同时读取的时候,却出现了不同的值,这怎么可能呢?

但我们可以从中得出一个重要结论:

这里这些变量的地址,绝对不是物理内存的地址.因为如果是物理地址,那么同一个时刻读取时,变量值绝对不可能不一样.一定是相同的.

这个其实是叫做虚拟地址(线性地址).

而且还需要说的是,几乎所有的语言,如果它有"地址"的概念,那么这个地址一定不是物理地址,而是虚拟地址.

那么虚拟地址是什么呢?该怎么理解呢?为什么要这么设计呢?和物理地址又有什么关系呢?等等相关的问题.我会以尽量多的例子来帮助大家理解.

什么是进程地址空间?

 这个以例子辅助理解:

有一个美国大富翁,拥有10亿美金,然后它有3个私生子,分别为a,b,c.

有一天,他把a叫到自己面前说:"你好好学习,到时候如果有所成就,我这10个亿美金就会全部给你",a一听可高兴的不行,就更加努力学习,努力搞科研成果

同样地,第二天,又把b叫到自己面前,说:"你是一个商人,将来如果能把自己的事业做大,我这10亿美金以后就是你的了",b一听,这还了得,立马充满动力和干劲的去干自己的事业去了.

第三天,对c也是同样地话语.

到这里,每个儿子都认为自己会拥有这10亿美金,但是这些钱现在并没有到他们手中,而且他们也不知道彼此的存在,然后都更加的拼命努力了.相当于富翁给每个儿子都花了一张大饼.

此时呢,儿子a为了得到10亿美金,然后拼命地学习,但是需要点钱买更多的书和资源,于是某一天,就去和富翁说:"爸,能先给我100美金吗,我需要买点资源",富翁一听,可以毕竟是用在正事上。于是给了,b和c也同样如此.

但即使这样,每个人依然认为自己有10个亿

我们站在上帝视角,肯定知道三个儿子不能得到这个10个亿,三个儿子每次也只是零星的想富翁要,这些富翁是完全给的起的,而且只要富翁只要一直给,这些儿子心里也一定认为老爹有这10个亿,就算某一天某个儿子要的很多,比如一个亿,老爹说没这么多,给不了,即申请空间失败,这些儿子也会认为老爹有这么10个亿,只是现在还不愿意给我.

你理解了这个意思,这个时候便在理解地址空间:

这里的他们的富翁老爹对应操作系统. 他的这些儿子(a,b,c)便是3个进程,而老爹给这几个儿子画的这10个亿的饼,便是进程地址空间.

这个现实中的饼即这个富翁真正拥有的钱财便是物理内存,他给每个儿子画的这些饼不只一个,很多个儿子会有很多个,所以这些饼到时候也需要管理起来,如何管理?先描述,再组织!

内核中的地址空间,本质将来也是一种数据结构,将来也要和一个特定的进程联系起来. 

怎么去理解呢?后面会说.

我们平常访问到的内存是物理内存吗?

我们要知道:物理内存本身是可以随时被读写的,不存在不可读,不可写这些等等的情况.

如果直接访问物理内存,会发生什么问题呢?

比如进程1由于操作不当,产生了野指针,指向了进程2的地址,此时在进程1中对那个野指针做修改就会直接修改了进程2的数据或代码.

再或者,进程3里有一些私密的密码数据,我们在进程1中直接指针指向这块空间,然后直接访问,就得到了密码数据,这是万万不可的.

所以,直接访问物理内存存在一个致命问题:特别不安全!而且也会导致一些内存碎片问题.

所以我们的计算机不可能直接采用这种直接让用户访问物理地址的的方式的.

所以,为了解决这种问题,现代计算机提出了这种方式:

我们知道每个进程启动,都会被OS创建一个task_struct结构体用来标识这个进程的所有属性信息,然后OS也会为每个进程创建一个进程地址空间,这个地址空间被叫做虚拟地址,然后系统也会存在一种映射机制(页表),这个暂且不谈,然后把虚拟内存经过映射机制映射到物理内存.

 task_struct结构体里面会有一个属性来指向这块虚拟地址空间.

可是如果虚拟地址中也是一个非法地址呢,经过映射,到了物理地址中还是非法,这不没什么用啊,还多套了一层,费这个麻烦做什么呢?

其实,当进程尝试访问一个违法的地址,例如访问未映射的内存区域或者非法的访问权限,MMU会检测到这个错误,并触发一个异常或中断,这个异常或中断称为缺页异常,后面会讲解.

就比如过年你得到了500零花钱,直接访问这500块固然很爽,但当时还小,万一被别人骗怎么办,此时我们妈妈一般会收走我们的零花钱。然后说当你需要的时候我再给你,然后你说你想买一本书看,你妈妈说嗯可以,于是便给了你,于是过了一会,你说我想买个玩具,你妈妈说买什么玩具,把钱花在正确的地方!QAQ.

这里面,这个妈妈便扮演了一个管理的角色,具有甄别是非的能力.来辨别你的需求是否是合法的,同样如果遇到了非法的虚拟地址,这个虚拟地址中的MMU就会禁止你访问.

这点就相当于变相保护了物理内存!

深入理解区域划分

那我们如何理解区域划分呢?

区域划分,本质是一在一个范围处定义start和end.用来标识起止和结束.

比如小学的时候,可能两个人闹别扭,画所谓的“三八线”.

 这里需要强调的是:每个进程都要有进程地址空间!

地址空间是一种内核数据结构,它里面至少要有各个区域的划分.

我们把每一块区域都抽象成了数据结构.

每个区域大小并不是固定的.例如堆向上生长,实则是大小在变化.

所谓的范围变化,本质就是对start或者end 的标记 +/- 特定的范围即可. 

在Linux下,我们把进程虚拟地址空间这个结构体抽象成 的结构叫做mm_struct.

我们直接看Linux源代码,找到task_struct,从里面找到mm_struct,因为每个进程都会有一个进程地址空间,所以它一定作为一个属性存放在task_struct里面.

看看里面究竟有什么东西 

看到了吗,这就是区域划分,用unsigned int来表示各个范围数据类型的.start 和 end来标识范围.

所以最终一个进程访问物理内存过程是如下图:

地址空间和页表(用户级)是每个进程都会有一份. 

那么多个进程万一映射到物理内存上位置有冲突呢

只要保证,每一个进程的页表,映射的是物理内存的不同区域,就能做到进程之间不会相互干扰,保证进程的独立性!

再谈奇怪的现象

现在回过头来,看一开始父子进程同时访问全局变量g_val造成值不同的原因:

这里有一张图来解释.

一开始子进程没有修改g_val时,父进程和子进程都是先经过虚拟地址空间,再通过页表,同时访问到物理内存中父进程的g_val

当子进程要修改g_val时,此时子进程通过自己的进程地址空间,再经过页表映射的时候,此时会重新在物理空间开辟一份空间,然后保存子进程修改后的数据,然后后面再映射的时候,就映射到新开辟的这块空间上了.这个操作,就叫做写时拷贝.

fork()中为什么一个变量可以同时保存两个不同的值

同时也可以回答上一章遗留的问题:一个变量怎么会同时保存两个不同的值.

我们知道fork()之所以有两个返回值,是被return了两次.

而return的本质就是对id进行写入.

此时写入便发生了写时拷贝,所以父子进程其实在物理内存中,有属于自己的变量空间,只不过在用户层用同一个变量(虚拟地址)标识了!

拓展:程序在没有编译完成被加载到内存的时候,程序内部有地址吗?

先来说答案:已经有地址了,可执行程序编译的时候,内部就已经有地址了.

地址空间不要仅仅理解为是OS内部需要遵守的,编译器也要遵守!即编译器编译代码的时候,就已经给我们形成了 各个区域 , 代码区,数据区..等等,并且,采用和Linux内核中一样的方式,给每一个变量,每一行代码都进行了编址。故,在程序编译的时候,每一个字段早已经具有了一个虚拟地址!

我们画张图可以理解.

首先在磁盘中,我们编写的代码,编写的时候,就已经有了对应的虚拟地址,例如第一行代码是0x1,第二行是0x10,第三行时0x100.但是为了执行的逻辑,第一行代码中得保存第二行的虚拟地址,第二行代码中也得保存第三行的虚拟地址。如下:

然后当程序运行起来时,会加载到物理内存中,此时每一行代码中都有下一行代码的虚拟地址,而每一行代码本身在物理地址中,也有一个地址.例如分别为0xA,0xAA,0xAAA.

那么此时,进程通过虚拟地址,再访问页表就能找到对应的物理地址然后访问了.

但问题是:地址空间和页表一开始的数据是哪里来的呢? 

编译完成后,编译器会将这些代码的地址自动填充到虚拟地址空间中,而且这些代码地址都是编译器给你的.这些虚拟地址会被填充到了页表左侧,当加载到内存的时候,每个代码又有自己的物理地址,此时便把物理地址填充到页表的右侧.这样每个进程便可以通过进程地址空间中的虚拟地址,通过映射关系,在物理地址上找到对应的代码并执行了.

 

这个时候CPU开始执行了.CPU有第一条指令,拿到了0x1,所以根据地址空间中代码区,经过页表映射,在物理内存找到了0x1这条指令,然后再读到CPU,此时CPU读到的指令的内部是虚拟地址0x10!CPU执行完0x1指令后,再根据0x10经过虚拟地址空间,再经过页表又找到了0x10这条指令并且读回到CPU,重复这样,CPU便可以执行完所有指令了.、

这便是全部过程了. 

关于进程地址空间讲解就到这里结束了,它的本质其实就是一段虚拟的空间!

再经过页表映射到物理地址上.

如果有任何疑问或不懂的地方,欢迎评论区或私信哦~

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

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

相关文章

网络安全—入职大厂经验之谈

大三想去实习&#xff0c;趁现在该干什么才能去大厂实习呢&#xff1f;想做一些事丰富一下自己的简历&#xff0c;只有打ctf&#xff1f;还是挖洞&#xff1f;非常迷茫。 或者入职转行网络安全行业应该怎么做&#xff1f;对于接下来的职业规划学习计划有什么打算&#xff1f; …

PETRv2: A Unified Framework for 3D Perception from Multi-Camera Images

PETRv2: A Unified Framework for 3D Perception from Multi-Camera Images 作者单位 旷视 目的 本文的目标是 通过扩展 PETR&#xff0c;使其有时序建模和多任务学习的能力 以此建立一个 强有力且统一的框架。 本文主要贡献&#xff1a; 将 位置 embedding 转换到 时序表…

漏洞复现 || Bitrix cms文件上传

免责声明 技术文章仅供参考&#xff0c;任何个人和组织使用网络应当遵守宪法法律&#xff0c;遵守公共秩序&#xff0c;尊重社会公德&#xff0c;不得利用网络从事危害国家安全、荣誉和利益&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此…

Go语言之流程控制语句,for循环

程序是由语句构成&#xff0c;而流程控制语句 是用来控制程序中每条语句执行顺序的语句。可以通过控制语句实现更丰富的逻辑以及更强大的功能。几乎所有编程语言都有流程控制语句&#xff0c;功能也都基本相似。 其流程控制方式有 顺序结构,分支结构,循环结构 1、switch比if el…

javaWeb之文件上传和下载

文件上传下载(场景): * 文件上传 * 客户端 * 文件上传页面(form) * 请求方式一定是POST. * 文件上传域(<input typefile>)必须具有name属性. * 表单的enctype属性值设置为"multipart/form-data". * 扩展:浏览器内核产品不同(不建…

剖析C语言字符串函数(超全)

目录 前言&#xff1a; 一、strlen函数 功能&#xff1a; 参数和返回值&#xff1a; 注意事项&#xff1a; 返回值是无符号的易错点&#xff1a; strlen函数的模拟实现 1、计数器算法 2、递归算法 3、指针减去指针 二、strcpy函数 功能&#xff1a; 参数和返回值 …

git使用代码

git init //生成一个.git的子目录&#xff0c;产生一个仓库。 git status //查看当前目录下所有文件的状态。 git aad . //将该目录下所有的文件提交到暂存区 git add文件名/将该目录下指定的文件提交到暂存区 git commit -m v1.0//将暂存区的文件提交到版本库 git log //…

网络协议与攻击模拟-21-HTTP协议

HTTP 协议 1、 HTTP 协议结构 2、在 Windows server 去搭建 web 服务器 3、分析 HTTP 协议流量 一、 HTTP 协议 1、概念 HTTP &#xff08;超文本传输协议&#xff09;是用于在万维网服务器上传输超文本&#xff08; HTML &#xff09;到本地浏览器的传输协议 属于 TCP / …

树与图的(深度 + 广度)优先遍历

目录 一、树与图的存储1.树的特性2.图的分类3.有向图的储存结构 二、树与图的深度优先遍历的运用树的重心题意分析代码实现 三、树与图的广度优先遍历的运用图中点的层次题意分析代码实现 一、树与图的存储 1.树的特性 树是一种特殊的图,具有以下两个重要特性: 无环 树是一个…

Redis数据类型 — Set

目录 Set内部实现 源码片段 Set 类型是一个无序并唯一的键值集合&#xff0c;它的存储顺序不会按照插入的先后顺序进行存储。一个集合最多可以存储 2^32-1 个元素。 Set 类型除了支持集合内的增删改查&#xff0c;同时还支持多个集合取交集、并集、差集。Set 的差集、并集和…

Bean 的作用域和生命周期

目录 一、 Bean 的作用域 1. 安装Lombok插件 1.1 Lombok 简介 1.2 Lombok 安装 2. 创建一个 User 对象&#xff0c;然后将 User 对象 存储到 Spring 容器中 2.1 创建User 对象 2.2 将User 对象存储到 Spring 中 2.3 修改 User 对象中的属性&#xff0c;然后看结果&#…

概率论的学习和整理--番外12:2个概率选择比较的题目

目录 1 题目 2 结论 3 算法 3.1 错误算法 3.2 算法1&#xff0c;用期望的方式解方式 3.3 算法2&#xff0c;直接解方程 3.4 算法3&#xff0c;用递归--等比数列求和来算 4 上述比较的意义-回到问题本身 1 题目 题目 3个A合成1个B 方案1&#xff1a;1/4 几率返还一个A…

【ONE·Linux || 地址空间与进程控制(二)】

总言 进程地址空间和进程控制相关介绍。 文章目录 总言2、进程控制续2.3、进程等待2.3.1、为什么需要进程等待2.3.2、阻塞式等待2.3.2.1、使用wait2.3.2.2、使用waitpid2.3.2.3、参数status基本介绍 2.3.3、一些细节与问题</font>2.3.3.1、进程独立性说明2.3.3.1、父进程…

【网络安全带你练爬虫-100练】第13练:文件的创建、写入

目录 目标&#xff1a;将数据写入到文件中 网络安全O 目标&#xff1a;将数据写入到文件中 开干 &#xff08;始于颜值&#xff09;打开一个&#xff0c;没有就会创建 with open(data.csv, modew, newline) as file: &#xff08;忠于才华&#xff09;开始写入数据 writer cs…

LinuxC/C++开发工具——make/makefile和gdb

linux开发工具 前言Linux项目自动化构建工具&#xff08;make/makefile&#xff09;makefile文件的组成如何使用make.PHONY关键字 项目清理 gdb调试器背景使用list&#xff08;l&#xff09;调试命令break&#xff08;b&#xff09;&#xff1a;设置断点info break&#xff1a;…

[STL] vector 模拟实现详解

目录 一&#xff0c;准备工作 二&#xff0c;push_back 1&#xff0c; 关于引用 2. 参数const 的修饰 补充 三&#xff0c;迭代器实现 四&#xff0c;Pop_back 五&#xff0c;insert 1. 补充——迭代器失效 六&#xff0c; erase 七&#xff0c;构造函数 1. 迭代…

合并当天Log

1.原因&#xff0c; 我们程序运行Log很多时&#xff0c;如果因为要写Log话费很多时间&#xff0c;这时我们可以把log保存按照更短的时间保存&#xff0c;比如一分钟一个Log,一个小时一个log&#xff0c;。。。。但我们查看Log时很麻烦&#xff0c;需要把分散的Log合并起来的工…

移动端深度学习部署:TFlite

1.TFlite介绍 &#xff08;1&#xff09;TFlite概念 tflite是谷歌自己的一个轻量级推理库。主要用于移动端。 tflite使用的思路主要是从预训练的模型转换为tflite模型文件&#xff0c;拿到移动端部署。 tflite的源模型可以来自tensorflow的saved model或者frozen model,也可…

ylb-定时任务task

总览&#xff1a; 在api模块service包&#xff0c;创建IncomeService类&#xff1a;&#xff08;收益计划 和 收益返还&#xff09; package com.bjpowernode.api.service;public interface IncomeService {/*收益计划*/void generateIncomePlan();/*收益返还*/void generate…