虚拟地址空间
现代CPU执行程序指令访问内存时,它接受的是虚拟地址。它会先使用硬件将虚拟地址转译为物理地址,然后就可以访问物理内存。通过虚拟地址访问内存有以下优势:
-
程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
-
程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
-
不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
进程可用的虚拟地址范围称为该进程的“虚拟地址空间”。每个用户模式进程都有其各自的专用虚拟地址空间。 对于 32 位进程,虚拟地址空间通常为 2 GB,范围从 0x00000000 至 0x7FFFFFFF。
那么什么又是用户模式?
用户模式与内核模式
这同样是CPU的概念。CPU就有这两种运行方式,两种方式的保护级别不一样。根据CPU上运行的代码的类型,CPU在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。可以这样理解:内核模式的权限更高,可以执行一些用户模式不能执行的指令,比如设备I/O。
在遥远的DOS时代,系统和程序都运行在CPU的实模式下(用户模式就是CPU的)
每个用户模式进程都有其各自的专用虚拟地址空间,但在内核模式下运行的所有代码都共享称为“系统空间”的单个虚拟地址空间。当前用户模式进程的虚拟地址空间称为“用户空间”。
共享内存和映射文件
引入:共享内存的定义是:对于多个进程可见的内存,或者出现在多个进程的虚拟地址空间中的内存。那么共享内存有什么作用?用处大了!设想,在Windows中有两个进程使用了同一个DLL,那么,只需要将该DLL中的代码加载到物理内存中一次,然后再在所有需要使用这个DLL的进程之间共享这块内存,不就可以节省大量的内存,并且节省了重复加载的时间!
下图演示了两个进程之间共享内存的情形。
作为一个现代的操作系统,Windows需要提供这种机制。而Windows的内存管理器实现共享内存的机制叫内存区对象,在Windows API中又被称为文件映射对象。
进程的虚拟地址可以映射到内存区对象上,而内存区对象又可以在不同的地方:①在内存中;②在换页文件中;③在其他文件中(显然是磁盘上的文件)。但是应用程序访问这些文件中的内容却像它们在内存中一样。这是如何实现的呢?下面分别来讨论。在讨论这个之前,先看一下另一个概念:
页面的状态:空闲、保留的、已分配的。
一个进程的地址空间中的页面可以是空闲的(Free)、保留的(Reserved),或者已分配的(Committed)。
- 空闲的很好理解,进程的虚拟地址空间只是一个逻辑概念,因此如果不做任何处理,这个地址空间中所有的页面都是空闲的。
- 保留地址空间,类似于一个线程保留一段虚拟地址范围以便将来使用。保留的地址空间是不能访问的,因为还没有分配。
- 已分配的页面是指这样的页面:当它们被访问的时候,最终被转译至物理内存中的有效页面。已分配的页面可以是私有的、不共享的,也可以是被映射到一个内存区上。如果已分配的页面是私有的,那么别的进程无法访问。而如果已分配的页面被映射为某个映射文件的一部分(称为视图),那么在第一次访问这些页面的时候,系统要将其内容从磁盘读回到内存中。
总结:只有已分配的页面才能访问,而访问页面一定是访问物理内存。因此如果页面被映射到磁盘上,那么第一次访问时,系统需要将磁盘中的内容读到内存中。
应用程序可以先保留地址空间,然后(在合适的时间)再分配地址空间中的页面。当然,它们也可以在同一个函数调用中一次性保留和分配页面。相应的API是VirtualAlloc和VirtualAllocEx。
使用“先保留再分配”的方法可以节省内存,它将提交分配页面的操作推迟到真正需要这些页面的时候,同时又可以保证虚拟地址空间的页面相邻。保留内存操作开销很小,仅仅是更新一些小的内部数据结构(虚拟地址描述符)。对于那些可能需要大块连续的虚拟内存的应用程序来说,这种方法是非常有用的:不需要为整个区域分配页面,而可以先保留地址空间,等以后需要时再分配页面。对于操作系统,一个典型的例子是线程的用户模式栈。线程栈的默认大小是1MB,但系统在创建线程的时候,只是保留一个1MB的栈地址空间,而仅分配栈的初始页面。下一个页面被标记为一个守护页面,但并没有被分配。它的作用是捕捉对栈的已分配页面之外的引用,并扩展这一部分。
----------------------- 我是分割线 --------------------------
回到刚才的话题。从上面的讨论可以看到,不管内存区对象在哪,进程要访问页面,最终还是要将内容载入内存。这就回答了为什么应用程序访问这些文件中的内容就像它们在内存中一样。
当内存区对象是(准确的说是映射到)一个磁盘上的文件时,这个文件就叫做内存映射文件。而当内存区对象映射到一块已分配的内存上时,这个内存区被称为由换页文件支撑的内存区(原谅我,这个名字是潘爱民译的),因为内存中的页面往往对应换页文件,必要时会被写入到换页文件中。此时,它便提供了前面所说的共享内存的功能。
通过调用CreateFileMapping函数来创建一个内存区对象。这个函数的第一个参数要求传递一个文件的句柄。如果传入INVALID_HANDLE_VALUE,则就是要创建一个由换页文件支撑的内存区。其他进程就可以通过这个函数返回的句柄或者是文件映射对象的名字来访问内存区对象。
内存区对象甚至可以引用比进程的地址空间大得多的文件。为了访问一个非常大的内存区对象,一个进程可以只映射该内存区对象中它感兴趣的那部分(称为该内存区的一个视图),其做法是调用MapViewOfFile函数。
文件映射有着广泛的用途。应用程序可以用它来方便地执行文件I/O,因为只需要让映射文件出现在其地址空间中即可(想想通常的ReadFile、WriteFile繁琐的的调用)。系统的映像加载器可以利用文件映射,将可执行映像文件、DLL和设备驱动程序映射到内存中。