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