✨个人主页:****Yohifo
🎉所属专栏:****Linux学习之旅
🎊每篇一句:****图片来源
🎃操作环境:****CentOS 7.6 阿里云远程服务器
- Perseverance is not a long race; it is many short races one after another.- 毅力不是一场漫长的比赛;是许多短跑一个接一个。
文章目录
📘前言
**对于
C/C++
来说,程序中的内存包括这几部分:
栈区
、
堆区
、
静态区
等,其中各个部分功能都不相同,比如函数的栈帧位于
栈区
,动态申请的空间位于
堆区
,全局变量和常量位于
静态区
,区域划分的意义是为了更好的使用和管理空间,那么
真实物理空间
也是如此划分吗?
多进程运行
时,又是如何区分空间的呢?
写时拷贝
机制原理是什么?本文将对这些问题进行解答**
内存条:真实的物理空间,用来存储各种数据
📘正文
📖问题引入
地址是唯一的,对地址进程编号的目的是为了不冲突
**这是个耳熟能详的概念,在
C语言
学习阶段,我们可以通过对变量
&
取地址的方式,查看当前变量存储空间的首地址信息**
#include<stdio.h>intmain(){constchar* ps ="这是一个常量字符串";printf("字符串地址:%p\n", ps);//%p 专门用来打印地址信息return0;}
**利用前面学习的
fork
函数创建子进程,使得子进程和父进程共同使用一个变量**
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<stdlib.h>intmain(){int val =10;
pid_t id =fork();if(id ==0){
val *=2;//刻意改变共享值printf("我是子进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n",getpid(),getppid(), val,&val);exit(0);}waitpid(id,0,0);printf("我是父进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n",getpid(),getppid(), val,&val);return0;}
对于同一块空间,读取到了不同的值,是不可能出现这种情况的
因为真实地址都是 **
唯一
** 的,分析:
- 不同的空间出现同名的情况
- 父子进程使用的真实物理空间并非同一块空间!
原因:
- 当子进程尝试修改共享值时,发生
写时拷贝
机制 - 语言层面的程序空间地址不是真实物理地址
- 一般将此地址称为
虚拟地址
或线性地址
结论:语言层面的地址都是虚拟地址,用户无法看到真实的物理地址,由 OS 统一管理
📖虚拟空间划分
一般用户的认知中,
C/C++
程序内存分布如下图所示,直接表示内存中的各个部分
📖真实空间分布
但实际上的空间分布是这样的:
如果有多个进程(真实地址空间只有一份),此时情况是这样的:
🖋️代码实现
**在实现虚拟地址空间时,是用结构体
mm_struct
实现的**
同
task_struct
一样,
mm_struct
中也包含了很多成员,比如不同区域的边界值
//简单展示其中的成员信息
mm_struct
{//代码区域划分unsignedlong code_start;unsignedlong code_end;//堆区域划分unsignedlong heap_start;unsignedlong heap_end;//栈区域划分unsignedlong stack_start;unsignedlong stack_end;//还有很多其他信息
……
}
**每个进程都会有这样一个
mm_struct
,其中的区域划分就是虚拟地址空间**
通过对边界值的调整,可以做到不同区域的增长,如堆区、栈区扩大
**
mm_struct
中的信息配合
页表
在对应的真实空间中使内存(程序寻址)**MMU
🖋️问题反思
此时可以理解为什么会发生同一块空间能读取到不同值的现象了
- 父子进程有着各自的
mm_struct
,其成员起始值一致 - 对于同一个变量,如果未改写,则两者的虚拟地址通过
页表
+MMU
转换后指向同一块空间 - 发生改写行为,此时会在真实空间中再开辟一块空间,拷贝变量值,让其中一个进程的虚拟地址空间映射改变,这种行为称为
写时拷贝
刚开始,父子进程共同使用同一块空间
当子进程修改共享值后
📖进程地址空间
**下面来好好谈谈
进程地址空间
(虚拟地址)**
🖋️虚拟地址
在早期程序中,是没有虚拟地址空间的,对于数据的写入和读取,是直接在物理地址上进行的,程序与物理空间直接打交道,存在以下问题:
- 假设存在野指针问题,此时可能直接对物理内存造成越界读写
- 程序运行时,每次都需要大小为
4GB
的内存使用,当进程过多时,资源分配就会很紧张,引起进程阻塞,导致执行效率下降 - 动态申请内存后,需要依次释放,影响整体效率
**为了解决各种问题,大佬们提出了
虚拟地址空间
这个概念**,有了
虚拟空间
后,当进程创建时,系统会为其分配属于自己的
虚拟空间
,**需要使用内存时,通过
寻址
的方式,使用物理地址上的空间即可**
- 多个进程互不影响,动态使用,做到
效率
、资源
双赢 - 发生越界行为时,
寻址
机制会检测出是否发生越界行为,如果发生了,能在其对物理地址造成影响前进行拦截 - 因为每个进程都有属于自己的空间,
OS
在管理进程时,能够以统一的视角进行管理,效率很高
光有 虚拟地址空间 是不够的,还需要一套完整的 ‘‘翻译’’ 机制进行程序寻址,如
Linux
中的
页表
+
MMU
🖋️页表+MMU
**
页表
本质上就是一张表,
操作系统
会为每个
进程
分配一个
页表
,该
页表
使用
物理地址
存储。当
进程
使用类似
malloc
等需要
映射代码或数据
的操作时,
操作系统
会在随后马上
修改页表
以加入新的
物理内存
。当
进程
完成退出时,内核会将相关的页表项删除掉,以便分配给新的
进程
**
原话出处:ARM体系架构——MMU
**系统底层机制的研究是非常生涩的,这里简言之就是
页表
记录信息,通过
MMU
机制进行寻址使用内存**,假设目标空间为只读区域(比如数据段、代码段),在进行空间开辟时,会打上只读权限标签。后续对这块进行写入操作时,会直接拒绝
对于这种机制感兴趣的同学可以点击下面这几篇文章查看详细内容:
Linux的虚拟内存详解(MMU、页表结构)
ARM体系架构——MMU
逻辑地址、页表、MMU等
🖋️写时拷贝
**
Linux
中存在一个很有意思的机制:
写时拷贝
**
这是一种 赌bo 行为,
OS
此时就赌你不会对数据进行修改,这样就可以 **使多个
进程
在访问同一个数据时,指向同一块空间,当发生改写行为时,再新开辟空间进行读写**
这种行为对于内置类型来说感知还不是很强,但如果是自定义类型的话,**
写时拷贝
行为可以在某些场景下减少
拷贝构造
函数的调用次数(尤其是
深拷贝
),尽可能提高效率**
可以通过一个简单的例子来证明此现象
//计算 string 类的大小#include<iostream>#include<string>usingnamespace std;intmain(){
string s;
cout <<sizeof(s)<< endl;return0;}
原因:
g++
中的string
对象创建后,它就赌你不会直接改写,所以实际对象为一个指针类型(64位环境下为8字节),当发生改写行为时,触发写时拷贝
机制,再进行其他操作
🖋️内存申请
**值得注意的是,在进行动态内存申请时,
OS
也并非直接去申请好内存,而是先判断是否有足够的内存,如果有,就在
页表
中记录相应信息(这种行为叫做
缺页中断
),当程序实际使用到这块空间时,
OS
才会去申请内存给程序使用**
OS是一个讲究人,不允许任何空间浪费或低效率行为
**假设没有
缺页中断
机制,给程序分配空间后,程序又不用,此时空间属于闲置状态,这是不被
OS
认可的低效浪费行为**
图片来源:3.2.2 OS之请求分页管理方式(请求页表、缺页中断机构、地址变换机构)
📖虚拟地址空间存在的意义
**总结一下,
虚拟内存+页表+MMU
这种管理方式的好处:**
- 防止地址随意访问,保护物理内存与其他进程(权限设置)
- 将
进程管理
和内存管理
进行解耦
,方便OS
进行更高效的管理 - 可以让进程以统一的视角看待自己的代码和数据
📘总结
**以上就是本篇关于
Linux
进程学习【进程地址】的全部内容了,我们从一个有趣的小问题切入,见识到了
虚拟地址空间
与
物理地址空间
的奇妙关系,在种种机制的加持之下,
OS
对进程的管理变得更加得心应手,系统也因此得以高效运行**
如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正
相关文章推荐
Linux进程学习【环境变量】
Linux进程学习【进程状态】
Linux进程学习【基本认知】
===============
Linux工具学习之【gdb】
Linux工具学习之【git】
Linux工具学习之【gcc/g++】
Linux工具学习之【vim】
版权归原作者 夜 默 所有, 如有侵权,请联系我们删除。