进程地址空间

空间布局图如下图所示:

32位下,一个进程的地址空间,取值范围为0x0000 000~0xFFFF FFFF,

[0,3GB]:用户空间;3GB~4GB]:内核空间

malloc申请多少空间自己知道,越界会崩溃,为什么free的时候只传了起始地址,free怎么知道要释放多少空间?

实际上,malloc会申请更多的字节,多出来的空间用来记录堆的属性信息。

回答前面的问题:在fork之后,怎么可能同一个地址,同时读取的时候,出现了不同的值!

这里的地址,绝对不是物理内存的地址!

这个地址是虚拟地址。(Linux称为线性地址)!

所以,几乎所有的语言,如果它有“地址“的概念,这个地址一定不是物理地址,而是虚拟地址。

在以前的计算机中,OS是直接访问物理内存的,如下图

OS直接使用的是物理内存,特别不安全,内存是可以直接被读写的,可能会出现野指针访问其他进程。

现代计算机,提出了下面的方式:

可是最终访问的是物理内存啊?万一我的虚拟地址是一个非法地址呢?——禁止映射。

要访问物理内存,需要先进行映射。

虚拟地址空间是一种数据结构,它里面至少有各个区域的划分。所谓的区域划分,本质是在一个范围里定义start和end;而范围变化,本质就是对start和end标记值的++--。

每一个进程都会有自己的地址空间和页表。只要保证,每一个进程的页表映射的是物理内存的不同区域,就能做到程序之间不会互相干扰,从而保证进程的独立性。

回答最开始的现象,当fork之后,fork的返回值一个变量怎么会有两个值呢?

fork函数的主体功能是在return之前就已经完成了的。return会被执行两次,return的本质,不就是对id进行写入吗?发生了写时拷贝,所以父子进程各自其实在物理内存中,有属于自己的变量空间!只不过在用户层用同一个变量(虚拟地址)来标识了。

当程序在编译时,形成可执行程序的时候,没有被加载到内存中的时候,请问:我们的程序内部,有地址吗?

其实已经有地址了。可执行程序其实编译的时候,已经有地址了

地址空间不要仅仅理解成为是OS内部要遵守的,其实编译器也要遵守!即编译器编译代码的时候,就已经给我们形成了各个区域代码区、数据区……并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经有了一个虚拟地址!!

地址空间概念

因此,什么叫做地址空间?

操作系统为进程虚拟抽象出来的一种看待物理内存和外设的方案。它的做法是让进程pcb通过指向自己的地址空间数据结构结合页表来完成到物理内存之间的映射。

为什么要存在地址空间?

1.凡是非法的访问或者映射,OS都会识别,并终止你这个进程。

例如:尝试对字符串常量做写入时,页表当中还存在rwx权限,OS识别出了在非法访问,OS终止了,那么就有效保护了物理内存。因为地址空间和页表是OS创建和维护的,是不是就意味着凡是想使用地址空间和页表进行映射,也一定要在操作系统监管之下来运行!那么这样也便保护了物理内存中的所有的合法数据包括各个进程,以及内核的相关有效数据。

所有的进程崩溃,不就是进程退出吗!!

2.因为有地址空间的存在,因为有页表映射的存在,我们的物理内存中,是不是可以对未来的数据进行任意位置的加载?

当然可以。内存的分配就可以和进程的管理,可以做到没有关系。内存管理模块 vs 进程管理模块就完成了解耦合。

所以,我们在C、C++语言上new、malloc空间的时候,本质是在哪申请的呢?虚拟地址空间,非物理内存。

如果我申请了物理空间,但是如果我不立马使用,是不是空间的浪费?——是!

本质上,(因为有地址空间的存在,所以上层申请空间,其实是在地址空间申请的,物理内存可以甚至一个字节都不给你,而你真正进行对物理地址空间访问的时候,才执行内存相关的管理算法,帮你申请空间,构建页表映射关系),然后,再让你进行内存的访问。()里的内容是由操作系统自动完成,用户,包括进程,完全0感知。

因为,我们有地址空间的存在,它可以将内存管理模块和进程管理模块进行解耦,并且我们可以在分配内存的时候采用延迟分配的策略来提高整机的效率。

3.因为,在物理内存中理论可以任意加载,那么是不是物理内存中几乎所有的数据和代码在内存中是乱序的?

但是,因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,那么是不是在进程视角,所有的内存分布,都可以是有序的?

地址空间+页表的存在,可以将内存分布,有序化。

但同时,地址空间是OS给进程画的大饼

结合第二条,进程要访问的物理内存中的数据和代码,可能目前并不在物理内存中。同样的,也可以让不同的进程映射到不同的物理内存。是不是便很容易做到进程独立性的实现。

进程的独立性可以通过地址空间+页表的方式实现。

因为有地址空间的存在,每一个进程都认为自己拥有4GB空间(32),并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。

每一个进程不知道也不需要知道其他进程的存在。

重新理解挂起

加载本质就是创建进程,那么是否必须非得立马把所有程序的代码和数据加载到内存,并创建内核数据结构建立映射关系?否

在最极端的情况下,甚至只有内核结构被创建出来了——新建状态

理论上,可以实现对程序的分批加载。既然可以分批加载,当然可以分批换出——挂起。

甚至,这个进程短时间不会再被执行了,比如阻塞了。

总结

fork创建了子进程,系统里就多了一个进程。而进程=内核数据结构+进程代码和数据。创建子进程,给子进程分配对应的内核结构,必须子进程自己独有了,因为进程具有独立性!理论上,子进程也要有自己的代码和数据!可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据,子进程只能使用父进程的代码和数据。

数据:可能被修改,必须被分离。OS选择采取了写时拷贝,为什么?

1.用的时候再分配,是一种高效使用内存的表现;写时拷贝是一种延迟申请技术,可以提高整机运行效率。因为写时拷贝的存在,所以,父子进程得以彻底分离。完成了进程独立性的保证。

2.OS无法在代码执行前预知哪些空间会被访问。

fork之后,父子进程的代码共享(是after共享的,还是所有的?——所有的)

fork之前的代码子进程其实是有的,那为什么不从最开始的位置开始执行,而是从fork之后位置开始执行?

1.我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后,都会有对应的地址。

2.进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始哦!),就要求CPU必须随时记录下当前进程执行的位置,所以,CPU内有对应的寄存器数据,用来记录当前进程的执行位置;并且寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的。这些数据,叫做进程的上下文数据。

父子进程各自调度的时候,因为有了自己的上下文数据,所以会接着上次调度的时候执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值