1 进程虚拟地址空间
每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如32位的硬件平台决定了虚拟地址空间的地址为0到 2 32 − 1 2^{32}-1 232−1,即0x00000000~0xFFFFFFFF,也就是我们常说的4GB虚拟空间大小:而64位的硬件平台具有64位寻址能力,它的虚拟地址空间达到了 2 64 2^{64} 264字节,即0x0000000000000000~0xFFFFFFFFFFFFFFFF,总共17179869184GB。
从程序的角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节:64位平台下的指针为64位,即8字节。
在下文的介绍中以32位的地址空间为主,64位的与32位类似。
32位平台下的4GB虚拟空间程序不可以任意使用。操作系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操作系统的掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。我们经常在Windows下碰到的“进程因非法操作需要关闭”或Linux下的“Segmentation fault”很多时候是因为进程访问了未经允许的地址。
在Linux操作系统中,32位进程的虚拟地址空间默认分为两部分:操作系统使用高地址的1GB(0xC0000000到0xFFFFFFFF),用户进程使用低地址的3GB(0x00000000到0xBFFFFFFF)。因此,用户进程的代码、数据以及通过动态分配(如malloc)申请的空间,总计最多可使用3GB的虚拟地址。
然而,3GB的空间在某些现代应用中显得不足,例如大型数据库、数值计算、图形处理、虚拟现实和游戏等场景。虽然64位处理器能够将虚拟地址空间扩展到极高的容量(17179869184GB),但升级硬件并非总是可行,尤其是一些软件只能运行在32位平台上。
对于Windows操作系统来说,它的进程虚拟地址空间划分是操作系统占用2GB,那么进程只剩下2GB空间。2GB空间对些程序来说太小了,所以Windows有个启动参数可以将操作系统占用的虚拟地址空间减少到1GB,即跟Linux分布一样。方法如下:修改Windows系统盘根目录下的Boot.ini,加上“/3G”参数
1.1 PAE
32位CPU的虚拟地址空间最大为4GB,因此程序的虚拟地址空间无法超过4GB。然而,自1995年起,Intel在Pentium Pro处理器中引入了36位物理地址,支持最多64GB的物理内存访问。
通过扩展地址线至36位,Intel修改了页面映射机制,使32位CPU能够访问超过4GB的物理内存。这种地址扩展技术称为PAE(Physical Address Extension)。扩展的物理地址空间通常由操作系统管理,对于普通应用程序而言,其虚拟地址空间仍然限制在32位范围内。为了使用超出4GB的物理内存,操作系统提供了一种窗口映射机制,将额外的物理内存映射到进程的虚拟地址空间。
以书架和书本的例子来解释这个机制:
- 书架的容量有限:想象一个图书馆(计算机系统)里的书架(虚拟地址空间)只能放下32本书(对应32位地址空间的4GB限制)。然而,图书馆里实际藏有100本书(物理内存大于4GB)。显然,这些书不能同时摆在书架上。
- 窗口映射的机制:图书馆管理员(操作系统)设计了一个机制:把书架分成固定大小的格子,比如一层有4个格子,每个格子可以放一本书(比如256MB)。当需要查看某本书时,管理员会从存储室(更大的物理内存空间)中拿出那本书,放进一个空格子里。这样,虽然书架总共只能放32本书,但通过不断替换,可以访问更多书。
- 映射机制的意义:虽然每次只能看到书架上的书(虚拟地址空间),但实际上你可以通过管理员来取用整个存储室的藏书(物理内存)。这就是AWE(Address Windowing Extensions)的原理——利用虚拟地址的固定窗口,将超出4GB的物理内存映射到虚拟地址空间中。Linux和UNIX系统中使用mmap()系统调用来实现。
2 装载的方式
程序执行时,所需的指令和数据必须加载到内存中才能运行。最简单的方式是静态装入,即将程序运行所需的全部内容一次性加载到内存中。但当程序所需内存超过物理内存时,增加内存虽能解决问题,但成本较高。因此,为了更高效地利用内存,提出了动态装入方法。
动态装入基于程序的局部性原理:将常用部分保留在内存中,较少使用的数据存放在磁盘中。覆盖装入(Overlay)和页映射(Paging)是典型的动态装入方法,其核心思想是按需加载模块——用到时装入内存,不用时保留在磁盘中,从而节省内存资源。
2.1 覆盖装入
省略 178
2.2 页映射
页映射是虚拟存储机制的重要组成部分,其在动态装载中得到了广泛应用。与覆盖装入类似,页映射并不将程序的所有数据和指令一次性加载到内存,而是将内存和磁盘数据划分为固定大小的页(Page),以页为单位进行装载和操作。
当前硬件支持的页大小包括4096字节、8192字节、2MB、4MB等,其中Intel IA32处理器常用的页大小为4096字节。例如,在512MB物理内存中页的数量为512*1024*1024/4096=131072个页。
为了演示页映射的基本机制,假设我们的32位机器有16 KB的内存,每个页大小为4096字节,则共有4个页,如下表所示。
页编号 | 地址 |
---|---|
F0 | 0x00000000-0x00000FFF |
F1 | 0x00001000-0x00001FFF |
F2 | 0x00002000-0x00002FFF |
F3 | 0x00003000-0x00003FFF |
假设程序的指令和数据总大小为32 KB,划分为8个页(P0~P7)。由于内存大小为16 KB,无法同时加载整个程序,因此采用动态装入机制逐页加载。
程序入口地址位于P0时,装载管理器检测到P0尚未加载,便将内存框F0分配给P0,并将P0的数据装入F0。执行过程中,当程序需要访问P5时,装载管理器将P5的数据加载到内存框F1。随后,当访问P3和P6时,P3和P6分别被加载到内存框F2和F3中。程序的页与内存框的映射关系如图6-4所示。
如果程序此时仅需要访问P0、P3、P5和P6,程序可以继续正常运行。然而,当程序需要访问P4时,装载管理器必须从当前加载的4个页(P0、P3、P5、P6)中选择一个页释放,以便为P4腾出内存空间。这种页替换过程通常基于特定的算法:
- FIFO(先入先出算法):选择最早装入的页,例如释放F0;
- LRU(最近最少使用算法):选择一段时间内访问频率最低的页,例如释放F2。
假设选用FIFO算法,释放P0所占用的F0页框,则F0将用于加载P4。程序随后继续按照动态装载的逻辑运行。
所谓的装载管理器实际上是现代操作系统的存储管理模块。当前主流操作系统均通过这种方式加载可执行文件。例如,Windows通过此机制加载PE文件,Linux加载ELF文件。接下来将从操作系统的角度分析可执行文件的加载过程。
3 从操作系统角度看可执行文件的装载
3.1 进程的建立
182