现代处理器基本都支持虚拟内存管理,在开启虚存管理时,程序只能访问到虚拟地址,处理器的内存管理单元(MMU)会自动完成虚拟地址到物理地址的转换。基于虚拟内存机制,操作系统可以为每个运行中的进程创建独享的虚拟地址空间,在这个空间中执行的程序,无法感知系统中其它进程的存在,从而使得不同的进程在运行时可以互不干扰。
虚拟地址空间的最大长度与系统中实际可用的物理内存数量无关,而是取决于硬件平台支持的寻址空间大小,即处理器的位数。32位处理器平台下,支持的虚拟地址空间范围为0x00000000~0xFFFFFFFF,总大小为2^32 字节,即4GB;而在64位处理器平台上,虚拟地址空间的范围从0x0000000000000000扩展到了0xFFFFFFFFFFFFFFFF,总大小为2^64 字节。用户进程通常无法直接访问全部地址空间,操作系统会将一部分地址空间划分给内核程序,具体的划分策略依赖于操作系统的实现。
下图是在典型的32位和64位Linux操作系统上,运行时进程的地址空间布局:
基于内核的管理策略,进程虚拟地址空间并没有全部分配给进程使用。Linux将进程地址空间划分成两个部分:位于高地址部分的内核地址空间和位于低地址部分的用户地址空间,其中
由上图可以看出,无论是32位系统或是64位系统,进程用户地址空间除了地址空间大小不同外,内存布局基本相同,通常都包含以下几个部分:
图中有一个特殊的区域没有画出,就是内存映射区域。内存映射区域通常位于堆和栈之间,用于将磁盘中的内容直接映射到内存中进行访问。Linux系统中的程序可以通过mmap系统调用来请求这种功能,相较于直接读写磁盘文件,内存映射提供了一种更加高效的文件IO方式,典型的应用场景就是动态库的装载。
尽管系统中每个进程都拥有巨大的虚拟地址空间,实际可使用的物理内存确是有限的,因此内核必须考虑如何合理地安排有限的物理地址到虚拟地址空间区域的映射。Linux内核为每个进程维护了独立的进程页表,并管理着系统中所有的物理内存,通过动态建立虚拟地址和物理地址的页表映射,每个进程只会访问所需要的那一部分物理内存,从而实现了内存的高效使用。下图简要显示了进程虚拟地址空间中的地址到实际物理地址的转换过程:
内核遵循按需分配的原则,在进程实际需要某个虚拟内存区域的数据之前,内核不会为其建立虚拟内存到物理内存的页面映射。若进程访问的虚拟地址空间部分没有与具体的物理页帧建立关联,处理器会自动触发缺页异常,并调用内核设置的缺页异常处理函数进行处理。内核缺页异常处理函数会检测触发缺页异常的虚拟地址合法性,并在通过后,为其分配物理页帧和建立页面映射关系,并返回重新执行触发异常的指令。
完整的进程地址空间被划分成了不同的区域,对于不同的区域,其设置的访问权限也都不相同,这同时也就意味着,用户程序在随意访问了某个内存地址时,如内核空间部分的部分,则可能触发不可恢复的错误。典型的几种访问了非法地址的情况如下所示: