1. 进程地址空间
简单来说,就是从高地址往低地址,内存分区分别是:
- 内核空间:
命令行参数argv
和环境变量env
等 - 栈区:大部分
局部变量
,栈区内存往低处增长 - 堆区:用于
动态内存管理
,堆区内存往高处增长 - 数据段:存储
全局变量
和静态数据
- 代码段:存储
可执行代码
和只读常量
我们有这样一个test1.c文件,可以把它运行起来查看它的内存地址是否这样安排。
结论:代码段
<数据段
<堆区
<栈区
<内核空间
1.同为数据段的内存,初始化过的init_value地址比未初始化的uninit_value地址更低,也就是数据段中初始化过的数据会存在更低的地址
2.同为堆区的内存,先开辟的heap1出现在最低的地址,后开辟的heap3出现在最高的地址,也就是堆区中越后开辟的内存,地址越高,地址是向高处增长的
3.同为栈区的内存,栈区中越后开辟的内存,地址越低,地址是向低处增长的
这样一套体系,叫做进程地址空间
,事实上,着些地址并不是真实物理地址,而是虚拟地址。
2. 虚拟地址和物理地址
我们有这样一个test2.c文件,其内容为定义了一个value变量,父进程创建一个子进程,我们已经知道子进程和父进程共享代码数据,两个进程一同输出value的值和value的地址,子进程输出五次后将value改变为200,再来看看会发生什么情况。
以上输出结果中,父进程的value一直为100,这是毫无疑问的,因为进程具有独立性,父子进程的数据互不影响。子进程刚被创建时,和父进程共用数据和代码,因此父子进程第一次输出val的值的时候,不论值和地址都是一样的。
但是问题就出在子进程修改了value的值之后,我们发现,父子进程,输出地址是一致的,但是变量内容不一样!这已经不是语法问题了,而是计算机组成原理的问题,一块内存毫无疑问同时只能存储一个值。那为什么此处父子进程的value值不同,但是地址相同?那就只有一个可能:这个地址不是物理地址,而是假的地址!
变量内容不一样 , 所以父子进程输出的变量绝对不是同一个变量 , 但地址值是一样的,说明,该地址绝对不是物理地址! 在Linux 地址下,这种地址叫做 虚拟地址.我们在用 C/C++ 语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由 OS 统一管理 , OS必须负责将 虚拟地址 转化成 物理地址 。
3. 页表
实际中,操作系统中有一个叫做页表
的东西,其会维护虚拟地址与物理地址之间的映射关系,当进程通过虚拟地址在进程地址空间中查找数据,其实本质上是拿着虚拟地址到页表中查找映射关系,进而找到真实的物理地址,再对数据进行访问。
子进程被创建的时候,会继承父进程的大量数据,其中页表
也会被继承,当子进程继承到父进程的页表
时,大部分内容都不会改动,而是直接拷贝,包括虚拟地址
与物理地址
的映射关系在内。
子进程继承到的页表,其虚拟地址和父进程是一样的,比如上图中,两进程ue的虚拟地址是一样的,因为子进程继承到了父进程中value
的虚拟地址。当子进程对val
进行修改的时候,此时发送写时拷贝:
子进程会把原先与父进程共用的value拷贝一份到别的地方,然后修改value = 200。这个过程中,对于子进程来说,value的物理地址改变了,于是对页表的映射关系进行修改,此时val的虚拟地址不变,但是虚拟地址对应的物理地址改变了!因此这个过程只修改物理地址,不修改虚拟地址。所以我们在修改了子进程中的val之后,观察到父子进程的value的地址一样,这是因为父子进程对value的虚拟地址是一样的,但是这个时候由于父子进程的页表不同,映射关系不同,最后访问到的物理地址其实是不一样的。因此我们输出的时候看到了一个地址两个值的情况。
1.页表相当于虚拟地址和物理地址的映射表,前面说过只有在进程运行时才会进行内存的访问。每个运行的进程的页表地址都会存在CPU里的一个名为cr3寄存器里。而当该进程被切换时,该进程会将该页表地址带走存在task_struct里,而当再次运行时又将页表地址放在cr3寄存器里。
2.页表内有对应的权限标志位用来表证它所对应的物理内存是可读还是可写。这也是为什么代码区和字符常量区是只读的,因为页表已经进行了权限管理。
3.页表内也有一个标志位用来判断数据释放在内存里。
4. 程序地址空间
在系统层面,也就是Linux
系统中,进程地址空间被存储在PCB
中,作为进程的一项属性。而进程地址空间本身被一个叫做mm_struct
结构体管理。
整体统揽
意义
1.页表让进程以统一的视角看待内存—即虚拟内存映射物理内存,可以让物理内存任意分布,对应的进程都能找到对应的内存位置。
页表将无序的地址变为了有序的地址,当通过页表映射,把指向相同功能的内存地址放到一起,此时我们就有的栈区
,堆区
,静态区
等等区域,更好地统一管理地址了。
2.进程地址空间可以有效的保护物理内存。
当用于向内存发出非法访问时,进程地址空间就可以检测出来,比如访问越界的内存等等。此时内存中的数据不会受到任何影响,因为该错误已经被进程地址空间检测并处理了。比如我们通过指针向非法的内存进行写入,那么进程地址空间就可以检测出来该地址是超出了某个范围的,在操作系统层面就直接报错,而不会真的等到对内存写入了数据之后,才发现该访问非法。
3.因为有进程地址空间和页表的存在,实现了进程管理模块和内存管理模块实现了解耦合。
由于页表的存在,此时进程管理和内存管理就是互不影响的。进程只需要去读取内存,申请内存等,无需考虑硬件层面的内存是如何管理的。对于磁盘,只需要做好加载数据到内存的工作,加载完数据后,无需考虑进程是如何读取地址,如何获取数据的。
4. 确保了进程的独立性
进程 = 内核数据结构 + 进程自己的代码和数据
。通过进程地址空间的映射,每个进程都有自己的内核数据结构,自己的代码,自己的数据,相互之间完全独立互不影响。