前言:
一个系统中的进程是与其他进程共享CPU和主存资源的。早期的PC使用物理寻址,而且诸如数字信号处理器、嵌入式微控制器以及Cray超级计算机这样的系统仍然继续使用这种寻址方式。然而现代处理器使用的是一种称为虚拟寻址(virtual addressing)的寻址形式。
物理寻址和虚拟寻址
物理寻址:
CPU想要访问哪个地址就直接访问,中间没有翻译的过程。
虚拟寻址:
使用虚拟地址,CPU通过访问页表得到一个虚拟地址,这个虚拟地址在被送到内存之前需要先转换成物理地址。将一个虚拟地址转换成物理地址的过程我们叫做地址翻译。这个过程需要CPU中的内存管理单元(MMU) 的专用硬件来操作。页表的内容由操作系统管理。
虚拟内存
为了更加有效的管理内存并且减少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的私有地址空间。
虚拟内存的意义:
1、它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保留活动区域,并根据需要在磁盘和主存之间来回的传送数据,高效的使用了主存。
2、它为每个进程提供了一致的地址空间,简化了内存管理。
3、它保护了进程的地址空间不被其他进程破坏。
以上是我们之前介绍的程序的地址空间,但是这是不准确的,应该称为进程的地址空间。
由于虚拟内存概念的引入,每一个进程都拥有一个一致的虚拟地址空间,这使得我们操作系统能以统一的视角来看待每一个进程。
进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所以进程共享内核虚拟地址空间,但每个进程有独立的用户虚拟地址空间。在用户地址空间内,又划分出堆区、栈区、数据区等区域。
mm_struct结构体
实际上,在我们的task_struct(PCB)中,进程的整体虚拟地址空间是由一个结构体mm_struct来描述的,每一个进程的task_struct里面都有一个mm_struct类型的指针变量,指向的内容用来描述整个进程的虚拟内存。
mm_struct源码片段
unsigned long start_code, end_code, start_data, end_data; //开始代码段,结束代码。开始数据,结束数据 unsigned long start_brk, brk, start_stack; //堆的开始和结束。 unsigned long arg_start, arg_end, env_start, env_end; //参数的起始和结束,环境变量的起始和终点
页表
页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。 (百度百科)
页表本身并不直接包含在task_struct中,因为它们通常是由操作系统管理的,而且可能会因为转换表的更新而变得非常大,所以不适合包含在控制结构中。页表是在物理内存中,由操作系统的内存管理系统维护。
简单来说,页表存放实现虚拟地址和物理地址映射的一种数据结构,通过查询页表我们就能找到相对应的物理地址。
解释父子进程的虚拟地址以及页表的关系
当某一个创建一个子进程,除了拷贝各种资源外,子进程会拷贝(写时拷贝)一份父进程的页表,页表内容与父进程一致。
由于写时拷贝的机制,当子进程想要修改某个物理地址上的内容时,系统会重新给子进程找一片空间。该空间上的内容与父进程的一致,子进程在此空间上做修改不影响父进程,并且系统会调整子进程页表的映射的物理空间,使得子进程页表的映射不发生冲突。
观察以下代码:
果然,虽然虚拟地址一样,但是父子进程的页表中映射的物理地址不一样。(开始的时候一样,但是由于子进程发生了写入操作,使得系统重新找了一个物理空间)
知道了页表的作用,我们也能知道为什么不允许数组越界访问,因为在查页表的时候根本就没找到所访问的虚拟地址,更别谈去访问物理地址了。这样一来,我们也能理解页表其实也起到了保护内存的作用。