linux进程地址空间

文章探讨了进程地址空间的概念,解释了虚拟地址与物理内存的映射机制,以及为何地址空间的存在能保护内存安全和维护进程独立性。通过故事和实例,深入剖析了mm_struct和页表在地址空间管理中的作用,以及编译过程中的地址生成规则。
摘要由CSDN通过智能技术生成

以前学习的地址空间

其中堆区和栈区的空间会随着程序的改变而发生相应的改变,栈区向低地址的方向生长,堆区向高地址的方向生长,而数据区和代码区的大小是固定的,不会随着代码的改变而改变。这就是我们以前掌握的知识。但现在的问题是我们之前学习的内存指的是我们电脑硬件的内存吗?我们由一段代码验证一下。

结果我们却得到了一个很奇怪的运行结果:

上面那段代码通过死循环语句不断地打印val的值和val的地址,在五秒之后,子进程将val的值修改成了300,之后子进程的val则为300,父进程的val还是100。以上的现象可以理解为进程具有独立性,相互之间不发生影响。可为什么父子进程打印的val的地址还是一样的,如果这里的地址就是指的计算机物理内存,那么所打印的val地址既然相同,两者的值怎么可能不相等?所以,我们引出知识点:我们之前在语言上学习的地址,不是指的物理地址而是虚拟地址(也可称谓逻辑地址,线性地址),printf打印出来的虚拟地址空间,那我们使用printf函数将所有地址空间全部打印出来,那不就构成虚拟地址空间了嘛,这是我们初次见到进程地址空间,下面我们来看看进程地址空间是什么?

进程地址空间感性认识(通过父亲和三个私生子的故事理解)

故事:一个爸爸是一个大富翁,有10亿美金,他由三个私生子,三个私生子之间彼此都不知道另外两个儿子的存在。父亲为了激励他的三个私生子好好工作学习,于是给他的儿子们画大饼,对每个儿子都说:我只有你一个儿子,你好好干,等你干好了,我把我这10亿美金都给你。三个儿子听到这话都干劲十足的工作学习。一天儿子a的工场需要资金流转,需要10w美金周转一下,a找爸爸要,爸爸很愉快地给a了。后来,儿子b找爸爸要20w美金出国留学,爸爸也很愉快的给他了。儿子c比较傻,他跟爸爸说,你把10亿美金都给我吧,反正早晚都是要给我的。结果他爸爸把他打了一顿,轰出去了。在这个故事里,父亲是操作系统,10亿美金是内存,三个儿子是进程,10亿美金的大饼就是进程地址空间。在三个儿子看来,那十亿美金都是自己的,类似的,在操作系统中,所有进程都认为内存都是自己的。但是在故事中,儿子a,b每次都向爸爸要一部分钱用于周转花销,爸爸同意了,而儿子c一下子想把10亿美金都从爸爸那要出来,结果被轰出去了。类似的,在操作系统中,我们的进程也不能一次性地向内存申请巨额空间,例如我们不可能用malloc函数,new操作符等操作一次性向操作系统申请16G的内存空间,这就是对进程地址空间的感性认识。

进程地址空间的本质

刚刚我们说了,给进程画的大饼就是进程地址空间。那么我们再讲一个例子:在公司中:领导总是会给员工画大饼,比如说领导前一天跟我说,让我好好干,干得好给我总经理当当,结果今天领导忘了之前对我说的那句话,跟我说让我好好干,干得好给我个经理当当。而这时我就发现了不对劲,之前说让我当总经理,现在又说让我当经理,这个大饼就被我识破了。所以对于领导来说,员工(进程)要被管理起来,饼(进程地址空间)也是要被管理起来的。而管理的本质就是对数据进行管理,管理的方法是先描述再管理,那么我们如何描述虚拟地址空间呢?类似的,我们先来描述一遍大饼。大饼无非就是许诺什么职位,工资有多高,早9晚5的上班时间,一年有多次假期,我们描述这样的大饼就可以用下面的这个结构体实行:

类似的:描述虚拟地址空间结构体要包含虚拟地址空间中的内容,例如代码区什么范围,数据区什么范围,栈区什么范围,堆区又是什么范围。。。那么我们如何描述内存上的一段空间呢?答案是通过名叫mm_struct的结构体来描述。mm_struct结构体内容类似于下图:

所以地址空间的本质其实是内核的一种数据结构mm_struct,定义局部变量,使用malloc,new都会扩大栈区或堆区,函数调用完毕后,局部变量自动销毁,free掉申请的堆区空间,又会使栈区或堆区变小。heap和stack所谓的区域调整,本质上就是修改某个区域的start或者end。这些操作实际上就是调整本进程对应的mm_struct内部的两个指针,控制空间的变大变小。当每一个程序加载到内存中时,操作系统都会发一个mm_struct给这个进程,而这个mm_struct就是虚拟地址空间。其中操作系统为了很快能够找到每个进程分配的虚拟地址空间,就会在每个进程对应的PCB中放置一个指向mm_struct对象的指针,每个进程都有他自己的虚拟地址空间,虚拟地址空间的本质就是一个mm_struct内核数据结构,该结构维护着虚拟地址空间。

虚拟地址空间如何对应物理内存(页表)

我们知道了一个程序加载到内存时操作系统会给进程发一个虚拟地址空间,且进程在内存中必定有自己实际的物理空间,当cpu执行对应的进程的指令时肯定要在物理内存上找对应的代码和数据,所以就引出了虚拟地址空间和物理内存如何对应的问题。

程序加载进内存之后会有对应的起始地址和该程序对应的大小,所以当操作系统想访问该程序的数据时就可以直接根据程序在内存上的起始地址加上对应的字节的大小来访问到对应位置的数据,在起始位置加上几个字节就是访问第几个字节的数据。当一个程序加载进内存之后操作系统为了管理这个进程就会创建对应的PCB并且还会给这个进程分配一个虚拟地址空间,PCB就可以通过内部的指针找到该程序所对应的虚拟地址空间。那么我们如何通过虚拟地址找到物理地址呢?

操作系统为了能够通过内核数据结构找到硬件上的内容,这时候就引出页表的这个概念。

程序A加载到物理内存中的地址为0x11112222,在虚拟地址空间的地址为0x11223344,页表一边就会记录A在虚拟地址空间上的地址,另一边就会记录A在物理内存上的地址。

当程序需要访问内存中的数据及指令时,首先需要通过该进程的PCB内部的mm_struct指针先找到该数据及指令的虚拟地址,再找到该虚拟地址在页表中的位置,然后根据页表找到虚拟地址对应的物理地址,再根据物理地址找到其在内存中的内容。而以上内容都是操作系统所做工作。

以32位进程地址空间为例:进程地址空间上的地址时从0,1,2.……一直到2^32,是连续的地址,所以虚拟地址又叫做线性地址。

存在虚拟地址空间的原因

保护内存的安全

就如同我们小时候的压岁钱要交给大人保管一样,如果不对我们的钱加以控制,我们去买一些正常的书籍玩具还好,如果去买很多威力巨大的鞭炮或者仿真枪怎么办,到时候就会出问题。我们的操作系统也是一样,如果没有我们的虚拟地址空间,我们的进程就可以通过PCB直接访问到内存上的内容,万一该进程非法越界了怎么办?万一该进程存在恶意程序怎么办?擅自访问我们存储在内存上的密码了怎么办?这显然会出现很大的安全问题。我们要明白,页表不是单纯地只显示一种映射关系,如果进程非法访问了,页表在映射的时候是会检查并且拦截的。所以这就是虚拟地址空间的一个重要作用,保护内存上的数据不受到恶意进程的破坏。

维护进程的独立性

让我们的思绪回到我们所写的第一段代码。

访问同一个地址却能够打印出来两个不一样的值,之所以会出现这样的现象就是因为虚拟地址空间存在,一开始父子进程都可以通过虚拟地址空间访问到内存上的一个数据变量,在虚拟内存上的地址为:0x11223344,该变量在内存上的地址为0x11112222,那么在父子进程页表的一边就都是0x11223344,页表的另一边就都是0x11112222:(因为子进程是由父进程创建的,所谓的创建激就是将父进程的PCB,页表以及虚拟地址空间拷贝给子进程,此时父子进程打印出的val值都一样)

在五秒之后,子进程改变了val值为300,此时子进程打印val值为300,而父进程打印val值仍为100。这是因为进程之间具有独立性,如果一个进程对一个数据进行修改会影响另一个进程的话,就会产生写时拷贝,即:当父进程或子进程有一方想对共享数据进行写入的话,操作系统会在内存重新开辟一段空间,然后将原来的内容拷贝至新的空间里去,并且修改页表的映射关系,然后再让进程对新开辟的空间做对应的写入工作,如下图:

操作系统为了保证进程的独立性,做了很多工作,其中之一就是通过虚拟地址空间,通过页表让不同的进程映射到不同的物理内存处,每个进程都有独立的内核数据结构(PCB,页表,虚拟地址空间),在数据层面又会通过写时拷贝的方法将不同进程的数据进行分离,所以进程的内核数据结构与是独立的,进程的数据也是独立的,因为父子进程的代码不会被修改所以是共享,但是两个毫不相关的进程的代码一定是分开的,进程等于内核数据结构+进程对应的代码和数据,加号的两边都是独立的所以进程具有独立性,那么这就是虚拟地址空间的一大好处,地址空间的存在,可以更加方便的进行进程和进程之间的数据代码的解耦,保证了进程独立性这样的特征。

重新再一次理解地址空间(将磁盘带入)

首先我们要知道:我们写的可执行程序就算没有加载进内存在硬盘上也是有地址的,这里的地址不是磁盘上的地址而是程序内部的地址,比如说某个函数所对应的地址某个全局变量所对应的地址。我们可以通过调试代码,通过汇编指令看到。

一段代码翻译成一个可执行程序之前要经过预处理,汇编,编译,链接,然后才生成一个可执行程序并加载进内存执行内部的指令,但是我们在汇编期间就发现了这个代码中存在地址,所以当一个程序在磁盘中时就已经存在地址了,而这里的地址就是逻辑地址,这里的地址为了方便程序之间的跳转,可以理解为和该程序加载到内存中生成的虚拟地址一样,不是只有操作系统会遵守对应的规则,在磁盘中的程序也会遵循对应的规则,编译器在编译代码的时候,就是按照虚拟地址空间的方式对我们的代码和数据进行编址的,所以一个程序在操作系统和磁盘中遵循着同一个地址规则,所以程序在磁盘中的虚拟地址和在操作系统中的虚拟地址是一样的。下面举个例子:

如图:将磁盘中的程序加载到内存中时,该程序的函数和全局变量会产生物理地址。

此时,进程就会有两个地址,一个是在程序内部方便跳转时的地址(注意,这个不是本篇所讲的虚拟地址,是函数内部的地址),一个是在内存中的物理地址,当程序加载进内存的时候操作系统会为这个进程创建PCB和虚拟地址空间,由于操作系统中的虚拟地址空间和磁盘中的虚拟地址空间遵循的是同一个规则,所以操作系统就会拿着磁盘上的地址直接填入操作系统的虚拟地址空间,比如说哪里到哪里是栈区,哪里到哪里是堆区等等。

而后,创建页表,将虚拟地址空间和内存上的地址联系起来。

当cpu开始执行这段程序时,cpu会首先通过PCB找到虚拟地址空间中main函数的地址,然后再通过页表找到物理地址,最后再执行内存上的main函数里面的内容,这时main函数里面会调用fun函数,虽然cpu不知道fun函数在哪里的,但是内存中存在着func函数的虚拟地址,所以操作系统就将func函数的虚拟地址加载进cpu里面,然后cpu再使用该地址通过页表映射找到该函数在物理内存上的位置,在调用fun函数的时候会用到全局变量val,变量val也存在着自己的虚拟地址,所以cpu在执行与变量val有关的指令时又会将val的虚拟地址加载进cpu,cpu又会使用该地址通过页表找到这个地址在内存上的内容,所以程序在整个运行的过程中完全见不着物理地址(所有的数据或指令都由cpu拿着虚拟地址通过页表映射在内存中找到),这里进程地址空间的最后一个重要性就是为了让进程以统一的视角来看待进程对应的代码和数据的各个区域,方便编译器也以统一的视角来进行编码。

  • 31
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值