深入Linux系列之进程地址空间
深入Linux系列之进程地址空间
1.引入
那么在之前的学习中,我们知道我们创建一个子进程的话,我们可以在代码层面调用fork函数来创建我们的子进程,那么fork函数的返回值根据我们当前所处进程的上下文是返回不同的值,它在父进程中返回子进程的PID,在子进程中则会返回0,那么我们可以利用返回值然后定义一个变量来接收这个返回值,然后通过if else逻辑达到让我们的父子进程有着不同的执行流的目的,那么想必刚才所说的过程对于你来说已经十分熟悉,对于fork函数如何使用也已经轻车熟路,但是在刚才的过程中,我们唯独会有一个疑问,也就是我们定义的一个变量来接收这个返回值,同一个变量在不同的进程中竟然会出现两个不同的值,那么这就和我们的理解有点冲突,因为一个变量当前赋值且没被修改的情况下不可能具有两个不同的值,就像一个人只能拥有一种性别一样。
那么我们之前的文章也详细介绍过我们的fork,但是对于这个情况,当时并没有提及,那么我们现在来思考一下为什么会出现这样的现象,那么我们知道我们进程是具有独立性,那么要做到独立性也就意味着我们父子进程的数据不能做到共享,为此做到该数据独立性,那么我们的fork函数会采取写时拷贝的机制,那么我们如果子进程读取我们父进程的数据,那么此时子进程会与父进程共享一个物理内存页面,但是如果我们子进程要对数据进行写入操作的话,那么这时则会触发写时拷贝机制,那么我们操作系统会为其要写入的数据专门拷贝得到一个副本,然后在这个副本中做写入,从而做到独立性,所以根据我们之前对fork函数的学习以及认识,有了这个写时拷贝机制,我们认为此时我们父子进程关于接收这个返回值的变量,那么它一定是有两份副本,一份是父进程,一份是子进程,所以也就解释了它为什么能够具备有两个值。
那么接下来可以用c语言写一段简单的代码来实验,验证我们刚才的推理的成立,代码逻辑也很简单,那么在代码中定义一个int类型全局变量g_val,然后用fork返回值让父子进程有着不同的执行流,然后我们让子进程对g_val进行修改,然后接着父子进程分别打印这个全局变量的值以及对应的地址,如果打印的结果是子进程中的全局变量的值是修改后的结果,并且对应的地址与父进程不同,那么就能够验证我们之前的结论。
那么我们根据结果发现,值确实不同,但是地址是相同的,那么值不同证明父子进程确实对于g_val有各自对应的副本,所以子进程的值与父进程的不同,那么能够验证我们刚才所说的结论,那么既然g_val在父子进程中有着各自对应的一份副本,那么地址不可能是相同的,那么既然是相同的,那么只能说明一个原因,那就是这个地址不是实际的物理内存地址,那么这个地址究竟是什么呢,那么这就和本文的主题进程地址空间有关了,那么这个地址就是虚拟地址。
2.进程地址空间
那么在我们之前对于进程的理解上有得添加一个新的内容,也就是进程地址空间。
我们知道我们进程的内核数据是被加载到我们的内存中的,而当我们的进程被调度到CPU当中运行的话,那么我们CPU得从我们内存中获取我们进程的各种内核数据,我们知道我们CPU以及内存以及外部的io设备都是一个独立的工作单元,那么它们要协同工作的话,就得用“线”来连接,那么数据通过该线在各个工作单元中流通,那么其中这里的线我们可以分为数据线与地址线。
那么同理我们的CPU与我们的内存也有数据线以及地址线连接,那么顾名思义,地址线就是用来专门传输地址内容,而数据线是用来传输数据内容的,那么以32为机器为例,那么我们的CPU与我们的内存的地址线有32根,那么每一根地址线根据其高低电频的电信号分别用来表示二进制的0和1,那么我们的CPU就能根据各个地址线的高低电频信号组合得到一个二进制序列,而这个二进制序列就是我们的地址,那么内存的寻址器获取到地址后,会到内存的对应位置得到CPU要获取的数据,然后通过数据线传给CPU当中的寄存器,那么这32根地址线每一根线只能表示两种不同的信息,要么是0或者1,所以我们这32根地址线总共就能表示出2的32次方个不同的地址,那么它的范围是从0000 0000到FFFF FFFF。
那么我们知道我们这32根地址线只能表示0000 0000到FFFF FFFF范围上的所有地址,那么我们这0000 0000到FFFF FFFF范围上连续的地址便是我们的进程的地址空间,也就意味着,我们进程的各种数据,那么他们的地址只能在这个范围上,不能超出这个范围,因为我们CPU的32根地址线只能表示这么多,那么我们操作系统为了管理这个进程地址空间,采取的方式是我们最熟悉的先描述,再组织,那么它在我们进程所对应的task_struct结构体中,定义了一个指向mm_struct结构体。
而没错这个mm_struct结构体就是用来描述我们的进程地址空间,那么它里面会有两个字段来表示这个进程地址空间的起始位置与结束位置,那么通过这个mm_struct结构体就能在抽象建模出我们的进程地址空间。
而进程地址空间的设计还不至于此,那么我们进程地址空间还专门进行了内存的布局,将其分为了代码段以及数据段,然后再数据段中的不同范围还分为了常量区,堆区,栈区等,那么这些名词在我们的语言的学习上想必都听闻过。
那么至于为什么对我们的进程地址空间进行内存布局,那么我们可以拿生活中的一个例子来理解,就假设你是一个读者,要去图书馆借阅书籍,那么如果我们的图书馆的各种类型的数据是随意摆放,各种类型的书籍都在同一个书架上,那么我们读者要寻找到目标书籍的时间开销是很大的,但是如果图书馆将这些数据按照类型分不同楼层,按首字母分不同书架,那么我们查找以及图书馆管理起来就很方便。
根据这个例子我们便知道要为进程地址空间布局的一个重要原因,那么便是对于数据的查找以及内存管理,那么具体实现则是我们的mm_struct结构体在包含其他各个数据段的结构体指针,那么对应该数据段的结构体就包含起始位置以及结束位置的两个字段或者直接包含各个数据段的起始以及结束的字段,那么通过我们的mm_struct结构体,我们就对内存有着统一的布局和视角来看待。
而在我们进程地址空间上的各个地址,这些地址也就是所谓的虚拟地址,所谓虚拟,也就是说它并不是我们该数据真实在物理内存当中的地址,那么之所以我们不给我们的数据直接分配物理地址,原因也很简单,那么就是安全,那么如果我们用户直接接触到了物理地址,那么我们可以对数据的篡改以及覆盖的行为是不受阻拦的,那么这个虚拟地址就像是一个屏障,将其与物理地址隔开,那么我们对物理内存的任何操作意味着都要受到约束或者阻拦,这维护了数据以及系统的安全。
那么既然我们只能看到虚拟地址,那么我们内存器要获取数据那么肯定得是要我们的真实的物理地址,那么必然得需要将我们的虚拟地址转换为我们的物理地址,那么就有了我们页表的存在。
那么页表是一个数据结构,那么它建立了虚拟地址到物理地址的一个映射,并且还为每个数据设置了是否具有读写权限的字段信息,那么我们可以通过页表将虚拟地址转化为物理地址。所以在我们创建进程的时候,我们进程的内核数据被加载到内存中,同时我们还会为进程创建对应的task_struct结构体,那么其中就包括了进程地址空间的创建以及页表的创建,那么也就意味着我们每一个进程都有自己的进程地址空间也就是对应的mm_struct结构体和页表,但是我们要注意的是,我们不同的进程的进程地址空间都是相同的,也就意味着我们每一个进程的mm_struct结构体的各个字段都是相同的,那么这也很好理解,因为我们让各个进程都以统一的逻辑来看待内存,但是看不到物理内存,那么我们每个进程对应的进程地址空间都是全部的从0000 0000到FFFF FFFF的4GB的空间,但是对于页表不同进程肯定是不一样的,因为虚拟到物理对应的映射各个进程是不同的,那么在我们创建我们的进程的同时,我们会初始化页表的部分数据的映射,那么之所以是部分,而不是全部数据,则是由于有以下原因:
我们知道我们操作系统会为了缓解进内存的压力会对某些进程采取挂起的手段,也就是将他的内核数据先放回到外部设备比如磁盘当中,那么此时它的内核数据都没加载到内存中,那么自然无法完成对应的映射。
其次,我们知道整个进程地址空间的大小是4GB,那么我们在不同的计算机的构造下,有可能会出现实际物理内存比虚拟内存小的情况,比如有的物理内存是2GB,那么就会导致有的数据没有加载到物理内存,所以无法完成所有数据的映射的初始化。
那么对于有些不在物理内存无法完成初始化的数据,那么页表会专门会有一个字段来标记它是否在物理内存,如果不在,那么则会被标记为缺页,那么对于缺页中断的数据的话,那么我们到时候进程被调度的时候,操作系统一定会为缺页中断的数据重新生成对应的映射。
所以有了虚拟地址的概念,那么我们对之前的我们语言层面上无法解释的东西,那么我们现在就能够找到源头了,比如我们知道我们c语言我们为什么不能修改字符常量,那么是因为常量区的数据的权限会在页表中标记为只读权限,如果我们要对访问字符常量并且执行写操作,那么它就会对我们该操作进行阻拦。
重新认识fork系统调用
那么有了虚拟地址空间这个概念,那么我们再来重新理解一下我们的fork函数创建子进程的一个原理,那么我们调用fork函数,那么我们会拷贝父进程的task_struct结构体,其中就包括进程地址空间以及页表,这也就是为什么说我们子进程与我们父进程是共享一份物理内存页面的,调用fork函数后,我们操作系统会将父进程的页表中的数据段的内容的权限全部改成只能读不能写,那么如果我们子进程要对数据段的内容进行写入的话,由于权限只读,从而就会触发写时拷贝机制,那么操作系统会在物理内存重新为该数据开辟一份空间,然后接着修改我们子进程所对应的物理内存地址,而虚拟内存地址则不需要修改,从而做到父子进程的数据的独立,有了虚拟地址的概念,便解决了我们开头所说的那个问题。
有了页表这个概念之后,那么我们CPU要读取我们进程在内存中的数据,那么CPU的MMU(内存管理单元)那么它会获取到该进程的页表,然后将我们进程的各种数据的虚拟地址转换成物理地址,然后通过地址线传递给我们的内存的寻址器去获取目标数据然后再传到我们的CPU的寄存器当中
3.结语
那么本篇文章介绍了我们进程地址空间的概念,那么我们知道进程地址空间的出现的意义第一是能让用户以及操作系统能够以统一的视角与布局看待我们的内存,因为我们实际上各个数据在物理内存中的位置是离散的乱许的,但是在进程空间的视角下,逻辑上认为他们是有序排列,比如栈区以及堆区在高地址处,那么这样的内存布局的好处还方便于我们对于数据的查询以及管理。
那么第二是我们的虚拟进程地址空间以及页表的存在,建立一道用户与物理内存的屏障,那么用户不能通过代码直接访问到物理内存,不让用户的各个行为不会受到阻拦,那么可能会出现数据的修改以及越界访问等等问题。
第三是我们虚拟地址以及页表的出现,那么让我们的进程管理以及内存管理相互分离开,那么进程管理我们操作系统就管理我们task_struct结构体所对应的各个数据结构,而内存管理,则是管理我们的页表结构即可,实现了进程管理与内存管理的解耦
那么这就是本篇文章对于进程地址空间的全部内容,那么希望能够让你有所收获,那么我会持续更新,那么下一篇文章我将会解析我们进程的终止与等待,那么希望你能够多多关注,多多支持!
相关文章:

深入Linux系列之进程地址空间
深入Linux系列之进程地址空间 1.引入 那么在之前的学习中,我们知道我们创建一个子进程的话,我们可以在代码层面调用fork函数来创建我们的子进程,那么fork函数的返回值根据我们当前所处进程的上下文是返回不同的值,它在父进程中返…...

HAL库外设宝典:基于CubeMX的STM32开发手册(持续更新)
目录 前言 GPIO(通用输入输出引脚) 推挽输出模式 浮空输入和上拉输入模式 GPIO其他模式以及内部电路原理 输出驱动器 输入驱动器 中断 外部中断(EXTI) 深入中断(内部机制及原理) 外部中断/事件控…...

网络安全-HSTS
什么是HSTS? HTTP严格传输安全协议(HTTP Strict Transport Security,简称:HSTS) 是互联网安全策略机制。网站可以选择使用HSTS策略,来让浏览器强制使用HTTPS与网站进行通信,以减少会话劫持风险。…...

全程Kali linux---CTFshow misc入门(38-50)
第三十八题: ctfshow{48b722b570c603ef58cc0b83bbf7680d} 第三十九题: 37换成1,36换成0,就得到长度为287的二进制字符串,因为不能被8整除所以,考虑每7位转换一个字符,得到flag。 ctfshow{5281…...

HarmonyOS:时间日期国际化
一、使用场景 在不同的国家和文化中,时间和日期格式的表示方法有所不同,使用惯例的不同点包括:日期中年月日的顺序、时间中时分秒的分隔符等。若应用中需展示时间日期,要确保界面以合适的方式显示,以便用户能够理解。 …...

使用miniforge代替miniconda
conda作为Python数据科学领域的常用软件,是对Python环境及相关依赖进行管理的经典工具,通常集成在anaconda或miniconda等产品中供用户日常使用。 但长久以来,conda在很多场景下运行缓慢卡顿、库解析速度过慢等问题也一直被用户所诟病…...

LIMO:少即是多的推理
25年2月来自上海交大、SII 和 GAIR 的论文“LIMO: Less is More for Reasoning”。 一个挑战是在大语言模型(LLM)中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据(通常超过 100,000 个示例),但本文展…...

【玩转 Postman 接口测试与开发2_018】第14章:利用 Postman 初探 API 安全测试
《API Testing and Development with Postman》最新第二版封面 文章目录 第十四章 API 安全测试1 OWASP API 安全清单1.1 相关背景1.2 OWASP API 安全清单1.3 认证与授权1.4 破防的对象级授权(Broken object-level authorization)1.5 破防的属性级授权&a…...

如何编写测试用例
代码质量管理是软件开发过程中的关键组成部分,比如我们常说的代码规范、代码可读性、单元测试和测试覆盖率等,对于研发人员来说单元测试和测试覆盖率是保障自己所编写代码的质量的重要手段;好的用例可以帮助研发人员确保代码质量和稳定性、减…...

复原IP地址(力扣93)
有了上一道题分割字符串的基础,这道题理解起来就会容易很多。相同的思想我就不再赘述,在这里我就说明一下此题额外需要注意的点。首先是终止条件如何确定,上一题我们递归到超过字符串长度时,则说明字符串已经分割完毕,…...

zzcms接口index.php id参数存在SQL注入漏洞
zzcms接口index.php id参数存在SQL注入漏洞 漏洞描述 ZZCMS 2023中发现了一个严重漏洞。该漏洞影响了文件/index.php中的某些未知功能,操纵参数id会导致SQL注入,攻击可能是远程发起的,该漏洞已被公开披露并可被利用。攻击者可通过sql盲注等手段,获取数据库信息。 威胁等级:…...

Redis03 - 高可用
Redis高可用 文章目录 Redis高可用一:主从复制 & 读写分离1:主从复制的作用2:主从复制原理2.1:全量复制2.2:增量复制(环形缓冲区) 3:主从复制实际演示3.1:基本流程准…...

系统URL整合系列视频四(需求介绍补充)
视频 系统URL整合系列视频四(需求补充说明) 视频介绍 (全国)大型分布式系统Web资源URL整合需求(补充)讲解。当今社会各行各业对软件系统的web资源访问权限控制越来越严格,控制粒度也越来越细。…...

激活函数篇 03 —— ReLU、LeakyReLU、ELU
本篇文章收录于专栏【机器学习】 以下是激活函数系列的相关的所有内容: 一文搞懂激活函数在神经网络中的关键作用 逻辑回归:Sigmoid函数在分类问题中的应用 整流线性单位函数(Rectified Linear Unit, ReLU),又称修正线性单元&a…...

山东大学软件学院人机交互期末复习笔记
文章目录 2022-2023 数媒方向2023-2024 软工方向重点题目绪论发展阶段 感知和认知基础视觉听觉肤觉知觉认知过程和交互设计原则感知和识别注意记忆问题解决语言处理影响认知的因素 立体显示技术及其应用红蓝眼镜偏振式眼镜主动式(快门时)立体眼镜 交互设…...

python 语音识别方案对比
目录 一、语音识别 二、代码实践 2.1 使用vosk三方库 2.2 使用SpeechRecognition 2.3 使用Whisper 一、语音识别 今天识别了别人做的这个app,觉得虽然是个日记app 但是用来学英语也挺好的,能进行语音识别,然后矫正语法,自己说的时候 ,实在不知道怎么说可以先乱说,然…...

docker常用命令及案例
以下是 Docker 的所有常用命令及其案例说明,按功能分类整理: 1. 镜像管理 1.1 拉取镜像 命令: docker pull <镜像名>:<标签>案例: 拉取官方的 nginx 镜像docker pull nginx:latest1.2 列出本地镜像 命令: docker images案例: 查看本地所有…...

DeepSeek-R1 云环境搭建部署流程
DeepSeek横空出世,在国际AI圈备受关注,作为个人开发者,AI的应用可以有效地提高个人开发效率。除此之外,DeepSeek的思考过程、思考能力是开放的,这对我们对结果调优有很好的帮助效果。 DeepSeek是一个基于人工智能技术…...

Java_双列集合
双列集合特点 存放的是键值对对象(Entry) Map 因为都是继承Map,所以要学会这些API,后面的类就都知道了 put 有两个操作,添加(并返回null)或者覆盖(返回被覆盖的值)…...

.net的一些知识点6
1.写个Lazy<T>的单例模式 public class SingleInstance{private static readonly Lazy<SingleInstance> instance new Lazy<SingleInstance>(() > new SingleInstance());private SingleInstance(){}public static SingleInstance Instace > instance…...

无须付费,安装即是完全版!
不知道大家有没有遇到过不小心删掉了电脑上超重要的文件,然后急得像热锅上的蚂蚁? 别担心,今天给大家带来一款超给力的数据恢复软件,简直就是拯救文件的“救星”! 数据恢复 专业的恢复数据软件 这款软件的界面设计得特…...

常见数据库对象与视图VIEW
常见的数据库对象 表 TABLE 数据字典 约束 CONSTRAINT 视图 VIEW 索引 INDEX 存储过程 PROCESS 存储函数 FUNCTION 触发器 TRIGGER 视图VIEW 1、引入 为什么使用视图? 视图可以帮助我们使用表的一部分,针对不同的用户制定不同的查询视图。 …...

【Vue2】vue2项目中如何使用mavon-editor编辑器,数据如何回显到网页,如何回显到编辑器二次编辑
参考网站: 安装使用参考:vue2-常用富文本编辑器使用介绍 html网页展示、编辑器回显二次编辑参考:快速搞懂前端项目如何集成Markdown插件mavon-editor,并回显数据到网页 安装命令 npm install mavon-editor2.9.1 --save全局配置 …...

2、Python面试题解析:如何进行字符串插值?
Python字符串插值详解 字符串插值是将变量或表达式嵌入字符串中的一种技术,Python提供了多种方式实现字符串插值。以下是常见的几种方法及其详细解析和代码示例。 1. 百分号(%)格式化 这是Python早期版本中的字符串插值方法,类似…...

计算机网络-SSH基本原理
最近年底都在忙,然后这两天好点抽空更新一下。前面基本把常见的VPN都学习了一遍,后面的内容应该又继续深入一点。 一、SSH简介 SSH(Secure Shell,安全外壳协议)是一种用于在不安全网络上进行安全远程登录和实现其他安…...

doris:MySQL 兼容性
Doris 高度兼容 MySQL 语法,支持标准 SQL。但是 Doris 与 MySQL 还是有很多不同的地方,下面给出了它们的差异点介绍。 数据类型 数字类型 类型MySQLDorisBoolean- 支持 - 范围:0 代表 false,1 代表 true- 支持 - 关键字&am…...

mysql 存储过程和自定义函数 详解
首先创建存储过程或者自定义函数时,都要使用use database 切换到目标数据库,因为存储过程和自定义函数都是属于某个数据库的。 存储过程是一种预编译的 SQL 代码集合,封装在数据库对象中。以下是一些常见的存储过程的关键字: 存…...

C++ 中的 cJSON 解析库:用法、实现及递归解析算法与内存高效管理
在现代软件开发中,JSON(JavaScript Object Notation)作为一种轻量级的数据交换格式,因其易于阅读和编写、易于机器解析和生成的特性,被广泛应用于各种场景。C 作为一种强大的编程语言,自然也需要一个高效的…...

websocket自动重连封装
websocket自动重连封装 前端代码封装 import { ref, onUnmounted } from vue;interface WebSocketOptions {url: string;protocols?: string | string[];reconnectTimeout?: number; }class WebSocketService {private ws: WebSocket | null null;private callbacks: { [k…...

【C语言】球球大作战游戏
目录 1. 前期准备 2. 玩家操作 3. 生成地图 4. 敌人移动 5. 吃掉小球 6. 完整代码 1. 前期准备 游戏设定:小球的位置、小球的半径、以及小球的颜色 这里我们可以用一个结构体数组来存放这些要素,以方便初始化小球的信息。 struct Ball {int x;int y;float r;DWORD c…...