在学习C语言对函数调用的时候,我们曾经提到过一个所谓的内存空间,也就是程序地址空间。
进程地址空间
首先我们要明白一个概念:什么是地址?
地址是指向内存区域的一个编号,每一个进程都有4G的进程地址空间,但是我们知道,现在的电脑内存一般来说也就8G左右,这样的话是不是就等于一台电脑只能运行两个进程呢,这个显然看起来不太对。
程序运行时的地址空间:
接下来我们看一个例子:
我们知道fork()函数可以创建子进程,而fork()之后产生的子进程,与父进程共享一份代码,且数据各自私有一份,当我们任意一个写入数据时,就会发生写时拷贝。
写时拷贝:当fork之后创建子进程之后,父子进程共享代码块,其数据起初也是共享的,它们通过其进程控制块(PCB)内的虚拟地址空间通过页表映射到物理内存上的位置也是一样的,当父子之间任意一个写入或修改数据,便以写时拷贝的方式各自拥有数据的一份副本,也就是在这个时候,其对应物理内存上的位置发生了变化。
由上图我们可以看到,子进程与父进程中value的地址是相同的,也就是说他们确实是共用一块代码。
然后我们对这个代码稍加改动:
由上图我们发现,子进程跟父进程的value是同一块地址,但是value的值不一样了,按照我们的程序地址空间来说,同一个地址内的内容应该是一样的,这是为什么呢?
实际上程序的地址空间不是内存,而是由一个mm_struct结构体描述的,因此程序地址空间应该叫进程的虚拟地址空间。
所以我们获取到的地址都是虚拟地址,只是一个编号而已,并不是真正的物理内存地址。
所以当我们调用fork()函数的时候,父子进程在物理地址上共用一份代码,并且数据各自私有,其对应的虚拟地址空间的内容是一样的,虽然父进程与子进程的value地址看似一样,实则在物理内存上并不一致。
那么问题又来了,我们访问一个变量的时候是如何通过虚拟地址访问到内存的呢?
这个时候我们就要引入一个概念:页表
页表的定义:内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page)大小为单位来管理内存中的页表。从虚拟内存的多角度来看,页就是最小单位。
页表是一个结构体,它记录了虚拟地址与物理地址之间的转换关系。
由此可知,页表的作用是将访问的虚拟地址通过映射转换为物理地址进而访问到内存的。
这么做的目的是为了更好的保护物理内存,防止物理内存被破坏以及侵占。
图示:
此外,因为页表中还记录了要访问的这块地址的属性,所以页表还有一个重要功能就是:内存访问控制----通过对虚拟地址的权限标志(只读等)来实现对内存的访问控制。
综上可知:当我们访问一个变量的时候,其实就是通过页表将虚拟地址转换为物理地址而进行访问的。