本文主要是介绍浅谈linux下的进程地址空间(虚拟地址/线性地址),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
什么是地址空间 - 虚拟地址空间
地址空间是如何设计的
为什么要有地址空间
什么是地址空间?
示例:
运行之后发现:同一个变量,同一个地址,在运行一段时间后,竟然会在同一时间出现两个不同的值?这是完全违背常理的,按道理来说一个 int类型 ,只能存储一个整数,为什么这里会出现两个完全不同的值呢???
要想了解这个问题,我们需要先了解一些东西。
linux下的地址空间:注意,这里指的是虚拟地址空间
【说明】
1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等,栈是向下增长的。
⒉.共享区:内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3.堆区:堆用于程序运行时动态内存分配,堆是可以上增长的。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
4.数据段:--存储全局数据和静态数据。(未初始化/初始化数据)
5.代码段:--可执行的代码/只读常量。(正文代码)
上述的地址空间,也可以通过代码验证:示例:
打印结果如下:可以发现的确是符合进程空间地址排布规律的,地址也是由低到高进行增长的。(注:这里栈区之所以很大,是因为使用的是云服务器)
上述的进程地址空间被称为 --- 虚拟地址空间 / 线性地址空间,它并不是实际上存储数据的物理内存空间,而是一种Linux下的内核数据结构,并且每一个进程都有独立私有的虚拟地址空间。
这里在Linux内核的源码中也有体现:进程的虚拟地址空间
那么如何管理各个区域呢?
在一个范围内定义start,end本质上就是在进行区域划分
所谓的范围变化,本质其实就是对start or end标记值 + - 特定的范围即可
地址空间是Linux内核数据结构,它里面一定有区域划分
地址空间是如何设计的
我们在虚拟地址和物理内存之间会设计一个页表,来维系二者的关联。
我们可以把各个区域及其对应的地址通过页表映射到不同的物理内存中,也就是说页表是维护映射关系的
这种映射关系是来维护虚拟地址和物理地址之间的的一种映射关系
换句话说,每个进程即使它的虚拟地址空间是完全一样的,但是只要它的页表映射关系是不同的,映射到物理内存的不同区域,那么它们就可以做到每个进程是具有独立性的
注:这里的页表是底层是哈希表,存储的是key_value结构,左边存储的的虚拟地址,右边存储的是物理内存地址
回答最开始的现象,为什么同一个地址,在同一个时刻,同时读取的时候,出现不同的值?
是因为这里的地址,绝对不是物理内存的地址,而是虚拟地址(线性地址)
所以,几乎所有的语言,如果它有”地址“的概念,那么这个地址一定不是物理地址,而是虚拟地址
子进程继承了父进程大部分属性,页表和地址空间都相同,刚开始父进程对应的全局变量g_val的虚拟地址被映射到了物理内存上
创建子进程的时候,因为页表的映射关系相同,此时父进程与子进程指向的是同一个变量g _val,所以地址相同,甚至值也相同
但是当子进程尝试去修改g_val,因为要保障进程的独立性,当操作系统识别到当前的子进程想去通过页表去访问g_val,想要写入的时候
那么操作系统会重新开辟一块空间,然后有必要的情况下,拷贝原来的值,然后再更改子进程的映射关系,防止子进程修改到原来地址的g_val
完成更改之后,因为虚拟地址并不受影响,所以虚拟地址的值是相同的,但是物理数据经过页表的映射,被映射到了不同的区域,所以看到的g _val的值是不同的
补:最开始创建指向同一个位置,当子进程尝试去做修改的时候,才发现地址是不同的,这种策略叫做写时拷贝
深入了解虚拟地址空间:
当我们的程序,在编译的时候,形成可执行程序的时候,没有被加载到内存中的时候,其实已经有地址了! !
可执行程序其实编译的时候,内部已经有地址了!
地址空间不要仅仅是系统内部要遵守的,其实编译器也要遵守!
即编译器编译代码的时候,就已经给我们形成了各个区域代码区,数据区,...并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址!
物理内存中,可执行程序内部的地址,依旧用的是编译器编译好的虚拟地址
当程序加载到内存的时候,每行代码,每个变量边具有了一个物理地址,外部的
1.进程访问数据时,地址空间和页表(虚拟地址的一部分)会被编译时生成的虚拟地址填充,页表(物理地址的一部分)会被加载时产生的物理地址填充。
2例子:假设函数A调用函数B
此时,进程开始运行,cpu拿到的是编译时函数A产生的虚拟地址,通过函数A的虚拟地址映射到了物理内存
此时,访问内存空间,通过函数A中存储B函数的虚拟地址,获现到了的B函数的虚拟地址,再次通过映射关系访问到B函数(其中,A函数调用B函数,编译时A函数中一定会存储B函数的虚拟地址)
为什么要有地址空间
1.保障安全
历史原因:
在当初,可执行程序被加载到内存中,进程是可以直接访问物理内存的,但是我们需要明白,内存本身是随时都可以被修改的,假如出现野指针的问题,进程之间是会被相互干扰,此时安全是很难保障的。
于是,现代计算机在此基础之上,提出了以下方式,虽然通过虚拟地址最终还是会访问到了物理内存,但是需要映射,而页表会对指针进行监测,如果出现非法访问,是可以禁止映射的。
凡是非法的访问或者映射,系统都会识别到,并终止你这个进程,此时有效的保护了物理内存
因为地址空间和页表是系统创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问!!
也就保护了物理内存中的所有的合法数据,包括各个进程,以及内核的相关有效数据!
2.方便管理
a.解耦合
因为有地址空间的存在,因为有页表映射的存在,我们的物理内存中,可以对未来的数据进行任意位置的加载
物理内存的分配就可以和进程的管理,做到没有任何关系!!
内存管理模块 vs 进程管理模块就完成了
注:所以,我们在C、C++语言上,new,malloc空间的时候,本质是在虚拟地址空间上申请的
因为如果我申请了物理空间,但是如果我不立马使用? 是不是空间的浪费呢? 是的!!
本质上,(因为有地址空间的存在,所以上层申请空间,其实是在虚拟地址空间上申请的,物理内存可以甚至一个字节都不给你! !而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系),然后,再让你进行内存的访问。这都是由操作系统,自动完成,用户,包括进程,完全0感知 ---缺页中断!
b.有序性
因为在物理内存中理论上可以任意位置加载,那么物理内存中的几乎所有的数据和代码在内存中是乱序的!
但是,因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,那么在进程视角所有的内存分布,都可以是有序的!!
地址空间+页表的存在可以将内存分布,有序化!
c.独立性
通过进程映射到不同的物理内存,很容易做到让不同的进程具有独立性
地址空间+页表的存在可以实现进程的独立性
其实这也符合linux下,一切皆文件的概念
上帝视角下,内存可以任意读写,那么我们的非法访问也就是权限不够,也就是Linux下,一切皆文件的概念
注:以上仅代表个人观点,仅供参考
这篇关于浅谈linux下的进程地址空间(虚拟地址/线性地址)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!