【Linux进程】——进程的程序地址空间
目录
前言
1.程序地址空间
1.1区域划分
1.2程序地址空间的本质
1.3程序地址空间分配原则
2.数据寻找
2.1补充:进程挂起
结语
前言
在Linux系统的神秘世界里,进程就像是一个个小工匠,各自忙碌地完成着不同的任务。你是否想过,每个进程在自己的“工作空间”——程序地址空间里,是如何有条不紊地管理数据、代码和各种资源的呢?这就如同每个人有自己的房间,里面摆放着不同的东西,每一个角落都有其特定的用途。对于想要深入了解Linux系统运行机制的朋友来说,探究进程的程序地址空间就像是拿到了一把打开理解操作系统内部奥秘的钥匙。今天,就让我们一起踏入这个充满趣味和挑战的领域,揭开Linux进程程序地址空间的神秘面纱。
1.程序地址空间
当一个程序开始运行,它的代码和数据都会被拷贝一份,放在内存空间中
那么这些代码和数据,具体在内存空间的哪里呢?——在程序地址空间当中
程序地址空间是操作系统在进程被创建后,为进程分配的一处内存空间,进程的代码和数据都将放在这一处内存空间上。
但实际上,这处内存空间,并不真实存在,是操作系统在画饼
幽默点讲,当进程被创建的时候,操作系统大手一挥,对这个新进程说道:"你是新人,这片内存空间是我给你的,足足有16个GB,你随便用!"
16个GB?可能吗?显然是不可能的。
操作系统为了节省空间,一开始给进程分配的内存空间并不真实存在,而是虚拟的
既然是虚拟的,为什么还是要这样分配?
这是在告诉进程,你最多可以拥有16GB,但我不会真正给你这么多!
操作系统对进程的内存分配,采用 需要就给 的策略
进程什么时候需要内存,操作系统什么时候给,而不是一开始就给一个固定的内存。
因为进程在运行期间,需要多少内存,操作系统也不清楚,这种需要就给的策略,且满足了进程的需求,又不会让内存被浪费,碎片化。
举个例子:顶级富豪与私生子
有个顶级富豪,他资产过亿,有四个私生子
四个私生子都不知道彼此的存在,富豪分别找到他们
对他们说:"我是你的亲生父亲,我有十个亿,你是我唯一的继承人,将来这些都是你的!"
在这四个私生子眼中,他们就知道自己最大的财富将会达到十个亿
其中一个私生子对富豪说:“我现在有点困难,你能救济一下我吗?给我XX”
这个XX,会是十个亿吗?
如果是,富豪也不会满足的,因为他只有十个亿,他不可能满足如此荒唐的要求,但给个几十万还是小意思。
在这个例子中,富豪相当于操作系统,四个私生子相当于进程们。
- 富豪给私生子画饼,就是操作系统给进程分配程序地址空间
- 富豪满足私生子的要求,就是操作系统在不断给进程分配真实空间
操作系统会对每个进程说,有多少内存,但真正给起来,是挤牙膏一样地给
操作系统这种 需要则给 的内存分配策略,是为了防止内存被浪费,碎片化
1.1区域划分
操作系统给进程的程序地址空间,一般长这样
我们可以清晰地看到,这个空间是被划分了几个区域,分为堆、栈、代码区等,不同的数据将存储在不同的区域当中,互不干扰
为什么要划分?
区域划分是为了保护进程数据安全
如代码是只能读,不能写的,为了保证百分百的代码安全,操作系统就会将其保护起来,不允许进程修改,同样,对待其他的区域初衷也是如此
1.2程序地址空间的本质
内存中,会有很多进程,也就会有很多程序地址空间
面对数量庞大的程序地址空间,操作系统如何管理——先描述,再组组织
- 先描述:操作系统会为每一个进程的程序地址空间都创建一个结构体对象
- 再组织:将这些结构体对象通过某种数据结构集合在一起,或者直接与进程的PCB建立映射
程序地址空间的本质,也不过只是一个内核数据结构罢了
struct mm_struct {
unsigned long code_start; // 代码区起始地址
unsigned long code_end; // 代码区结束地址
unsigned long init_start; // 初始化区起始地址
unsigned long init_end; // 初始化区结束地址
unsigned long uninit_start; // 未初始化区起始地址
unsigned long uninit_end; // 未初始化区结束地址
unsigned long heap_start; // 堆区起始地址
unsigned long heap_end; // 堆区结束地址
unsigned long stack_start; // 栈区起始地址
unsigned long stack_end; // 栈区结束地址
};
1.3程序地址空间分配原则
当进程需要新创建一个变量,操作系统就会根据这个变量的类型,将符合其特征的一块区域的地址分配给它
例如:如果一个指针存储malloc申请的空间地址,那它指向的地址就是堆区;如果是一个字符串常量,该常量的地址就是在代码区
每个区的空间分配也是有讲究的,并不是有就分配,随意分配
分配规律:
- 堆区分配空间向上增长,先分配的空间地址比后分配的空间地址小
- 栈区分配空间,遵循全局向下增长,局部向上增长
堆区好理解,但栈区还是需要好好说一下
- 全局向下增长,是指如果你连续创建多个单体变量,这些变量的地址是越来越小的。
- 局部向上增长:如果创建的是数组,结构体这样内部有数据元素的,那么他们内部的元素地址是越来越大
例如:int arry1[2] 和 int arry2[2]
arry1 的地址比 arry2 的地址高(全局向下分配)
arry1[0] 的地址比 arry1[1] 的地址低(局部向上分配)
2.数据寻找
操作系统给你分配的程序地址空间是假的,是虚拟的?那么我们平常打印的地址呢?
在进程fork中,我们就已经认识到,一个变量会有两个不同的值
如果进行深究,在子进程和父进程中分别打印出一个变量的地址,你会发现一个震惊的现象,同一个地址,居然会有两份不一样的值
学到今天就会知道,这个地址,肯定不是物理地址,而是操作系统给我们的虚拟地址
程序地址空间上的地址是虚拟的
我们打印,在表面看到的地址就是程序地址空间上的虚拟地址,而真实的地址,其实需要通过操作系统为进程分配的页表来找到
页表,也是内核中的众多数据结构之一,它是一张记录虚拟地址和物理地址映射关系的一张表。
当程序被加载到内存空间的时候,其代码和数据会优先在内存空间中找到自己的位置,操作系统会记录该位置,然后创建PCB,创建程序地址空间,创建页表并建立虚拟地址和物理地址的映射
浅浅看一下页表内容
内容说明
- 虚拟地址和物理地址的映射关系
- 分配检查物理地址是否已经被其他进程占用
- 内容位检查物理地址当中是否已经有内容
- 读写位表示是否可以读写,或只读只写
注意:其中分配位和内容位只有两种状态,0和1
进程得到变量具体数据的过程
- 拿到变量的虚拟地址
- 通过页表的映射关系找到物理地址
- 通过物理地址在内存中找到对应的值
即虚拟地址——物理地址——真实数据的数据寻找方式,现在我们就可以解答,为什么fork函数的返回值是两个不同的值,却又有相同的地址
它们的虚拟地址一样,但物理地址不一样,值也就不一样
- 打印的地址是虚拟地址,所以是一样的
- 而值是物理地址对应的值,所以值是不一样的
一开始可能觉得麻烦,但熟悉起来,就会发现这种数据寻找方式有诸多的妙处,例如
- 进程以统一的视角看待内存,任意一个进程,可以通过 地址空间+页表 就可以将乱序的内存数据,变成有序、分门别类地规划好
- 存在虚拟地址空间,可以有效地进行进程访问内存的安全检查,读写位保护内存安全
- 将进程管理和内存管理进行解耦
- 通过页表,让进程映射到不同的物理内存中,并且分配位保护物理内存,从而实现进程的独立性
2.1补充:进程挂起
如果一个进程被挂起,它的代码和数据都会被调出到磁盘中,内存中就不再有这些代码和数据,即我们将页表当中的存储进程代码和数据的物理地址的分配位改为 0 ,就代表进程没有被分配物理内存,就是被挂起了
结语
Linux进程的程序地址空间是整个Linux系统高效稳定运行的关键所在。理解它不仅能帮助我们解决实际工作中遇到的各种问题,还为我们在系统开发、优化和安全管理等方面提供了坚实的基础。随着技术的不断发展,对进程程序地址空间的研究和应用也将不断深入。让我们持续关注这个领域,探索更多的可能性。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/593636.html 如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!