第13章Windows的内存结构
13.1 进程的虚拟地址空间
每个进程都有自己的虚拟地址空间,32位下是4GB
,指针的寻址范围是0x00000000
至0xFFFFFFFF
之间的值;64位下是16EB(10的18次方)
,指针寻址范围是0x0000000000000000
至0xFFFFFFFFFFFFFFFF
。每个进程都只能访问自己的私有空间。操作系统的内存也是隐藏的。
虽然两个不同的进程A和B都可以访问同样的0x12345678
,但是它们访问的是各自的数据结构。
而且,这些都是虚拟地址,不是物理地址空间。
13.2 虚拟地址空间如何分区
每个进程的虚拟地址空间都要划分各个分区,根据操作系统的实现方法来划分,不同的Windows内核的分区方式略有不同。
13.2.1 NuLL
指针分配的分区—适用于Windows2000
和Windows98
这个分区使用NULL指针的分配。如果试图读写这个分区,就会发生一个访问违规的情况。
13.2.2 MS-DOS/16位Windows应用程序兼容分区—仅适用于Windows98
这个分区用于兼容Windows98
,大小是4MB
。读写它同样会发生访问违规的情况。
13.2.3 用户方式分区—适用于Windows2000
和Windows98
进程私有的地址空间,不能被其他线程读写或访问。这样让整个系统更加健壮。
实际情况下可以使用的地址空间还不到进程的地址空间的一半。显然另一半是操作系统需要的,用于内核代码、设备驱动程序代码、设备IO高速缓存、非页面内存池的分配和进程页面表等使用。只有在64的Windows2000
下,内核才得到了它要的空间。
- 在
x86
下获取3GB用户方式分区
的方式是将/3GB
这个开关加到系统的BOOT.INI
文件的有关项目中。这还不行,链接程序还要有/LARGEADDRESSAWARE
这个开关供 Microsoft 检查。如果链接的时候没有加这个开关,Microsoft将会保留多出来的1GB
,防止意外的访问。另外,使用了/3GB
之后,内核只能勉强地放入一个1GB
的分区,这将减少系统能创建的线程、堆栈和其他资源的数量。而且系统最多使用16GB
的RAM,而通常最多可以使用64GB的RAM
,因为内核方式没有足够多的虚拟地址空间可以用来管理更多的RAM了。最后,编译DLL
是忽略/LARGEADDRESSAWARE
这个开关的。 - 在
64位Windows2000
中获得2GB用户方式分区
- 当从32位移植到64位的时候,指针还是视为32位地址就会造成访问违规。不过,只要对
0x000000007FFFFFFF的高33位零
,这样就可以截断64位地址位32位而不会造成任何影响了。这样用户地址空间有0x80000000
这么多,对于大多数应用程序来说已经足够了。 - 若一个64位应用程序想要访问它的全部
4TB空间
,必须使用/LARGEADDRESSAWARE
这个链接开关来创建。
- 当从32位移植到64位的时候,指针还是视为32位地址就会造成访问违规。不过,只要对
13.2.4 64KB
禁止进入的分区—仅适用于Windows2000
位于用户方式分区的前64KB
是禁止进入的,访问它只会导致访问违规。保留它只是为了更容易地实现操作系统。当内存的地址和长度传递给Windows函数时,该函数将在执行它的操作前使内存块生效,而让它生效的是内核的代码,方式是让这个分区保持禁止进入的状态。
13.2.5 共享的MMF分区—仅适用于Windows98
这个1GB分区是系统用来存放所有32位进程共享数据的地方,比如共享的动态连接库Kernel32.dll
、AdvAPI32.dll
、User32.dll
和GDI32.dll
等,便于所有32位进程访问。此外每个进程加载DLL时都加载到相同的地址,这里也是内存映射文件映射到的地方。
13.2.6 内核方式分区—适用于Windows2000和Windows98
存放系统代码的地方,用于线程调度
、内存管理
、文件系统支持
、网络支持
和设备驱动程序
的代码都在这里加载,可以被所有进程共享。在Win2000
中,这些组件是受到保护的。
13.3 地址空间中的区域
在进程被创建并拥有地址空间后,如果想要使用其中的部分,必须通过调用VirtualAlloc
函数来分配它里边的各个区域,这个分配的操作就成为保留(reserving)。分配的粒度则是相对固定的64KB
。
当保留一个区域时,系统要确保该区域的大小是页面大小的倍数,页面时系统管理内存的一个内存单位,不同的CPU页面的大小不同,x86下的就是4K
,64位就是8K
。
有时候系统能够代表你的进程来保留地址空间的区域,用来放进程环境块(PEB)
。系统需要创建一个线程环境块(TEB)
以便管理当前进程中存在的线程。
值得注意的是,PEB和TEB
可以不是从64KB这个边界开始的
,即便它们是页面大小的倍数。
释放区块使用函数VirtualAlloc
来完成。
13.4 提交地址空间区域中的物理存储器
分配物理存储器,然后把它映射到已保留的地址空间区域,这个过程称为提交物理存储器,以页面的形式提交,使用函数VirtualAlloc
。提交时,只需要提交到进程部分的地址空间。如果不再需要已提交的物理寄存器,物理寄存器应该被释放,通过VirtualFree
完成。
13.5 物理存储器与页文件
在较老的操作系统中,物理存储器被视为计算机拥有的RAM的容量。今天的操作系统让磁盘空间看起来就像内存了,磁盘上的文件被称为页文件,它包含可供所有进程使用的虚拟内存。
通过页文件,CPU可以操作更多的RAM,尽管其中的部分不是真实的RAM,而是页文件。这个过程里,根据运行的应用程序的需要,页文件的内容和RAM的内容不停的交换。
显然这个过程增加了应用程序可以用的RAM的容量
,因此页文件的使用是视情况而定的,但是尽量使用页文件是值得鼓励的,这样可以对更大的数据集进行操作。使用VirtualAlloc
将物理存储器提交给地址空间的一个区域时,地址空间实际上是从硬盘上的一个文件中进行分配的。页文件的大小直接决定了有多少物理存储器可供应用程序使用,RAM的影响则相对非常小。
以下是RAM和页的交换过程:
从上图可以看出,线程访问的数据有可能是在RAM
、RAM空闲页面
和物理地址
三个地方,所以需要相对复杂地判断数据不在这三个地方的时候的处理方式。
系统在内存页面
和页文件
两端交互倒腾得越频繁,系统运行得越慢。因此提高系统运行速度的方式通过增加RAM而不是提高CPU的速度效果更好
。
但这也不意味着页文件有变得非常大的趋势
,而且只要运行一个程序,系统就会保留一些空间,把物理存储器提交给这些空间,再把代码和数据拷贝过去
。这样的操作需要花费很大的代价来加载和启动这个进程。系统的做法是打开应用程序后,就确定代码和数据的大小,然后系统保留一个地址空间的区域,并指明该区域相关的物理存储器在exe文件自身。
这样就省略了大部分页文件和RAM的交换过程,使得应用程序加载的非常迅速,页文件也可以保持得很小。
可以运行命令sysdm.cpl
打开系统属性,然后选择高级(英文是Advance)选项卡,再单击性能(Performance Option)设置,就可以编辑页文件。
13.6 保护属性
已经分配的物理存储器的各 个页面可以被赋予不同的保护属性。x86
和AlphaCPU
不支持“执行”保护属性,由操作系统软件支持。这意味着将PAGE_EXECUTE
保护属性赋予内存的话,该内存也有读优先权。不过,这仅仅是x86
的情况。
13.6.1 Copy-On-Write访问
关于PAGE_WRITECOPY
和PAGE_EXECUTE_WRITECOPY
两个属性。它们多用于节省RAM的使用量和页文件的的空间。例如对于10个Notepad实例在运行,那么所有这些实例都可以共享应用程序的代码和数据页面
。这样可以大大提高系统性能,但这也要求所有实例都视这片内存为只读或者只执行的内存
。
为了防止造成混乱,操作系统给共享内存块赋予了Copy-On-Write
保护属性。当一个exe
或者dll
被映射到一个内存地址时,系统将计算有多少页面是可以写入的,然后从页文件中分配内存,以适应这些可写如的页面的需要。除非该模块的可写入页面是实际的写入模块,否则这些页文件是不适用的。
当共享的内存块被其中一个线程试图写入,系统必然要干预:
- 查找RAM中的一个空闲页面。这一步不会失败,因为进程在被映射的时候系统,会保证有足够的空闲页面。
- 将试图
被修改的页面
拷贝到第一步找到的页面,并赋予PAGE_EXECUTE_READ_WRITE
和PAGE_READWRITE
两个保护属性,不再变化。 - 系统更新进程的
页(面)表
,使得被访问的虚拟地址被转换成新的RAM页面
。
此外,当使用VirtualAlloc
函数保留地址空间或者提交物理存储器时,不应该传递PAGE_READWRITE
和PAGE_EXECUTE_READ_WRITE
。否则会导致函数失败,GetLastError
的返回值是ERROR_INVALID_PARAMETER
。当exe或者dll映射
时,这两个属性被操作系统使用。
13.6.2 特殊的访问保护属性的标志
还有三个可以属性:
PAGE_CACHE
用于停用提交页面的高速缓存,一般情况下最好不用,多是给处理内存缓冲区的硬件设备驱动开发人员使用。PAGE_WRITECOMBINE
也是给硬件设备驱动开发人员使用,它允许把单个设备的多次写入合并在一起,以提高性能。PAGE_GUARD
可以在页面上写入一个字节时使应用程序得到通知(通过一个异常条件)。Windows2000
在创建线程堆栈时使用这个标志。
13.7 综合使用所有的元素
通过源代码中的VMMap
应用程序,对一张表的说明。
13.7.1 区域的内部情况
对另一张表的说明。
13.7.2 与Windows98
地址空间的差别
略
13.8 数据对齐的重要性
数据对齐并不是操作系统的一部分,而是CPU结构的一部分。当CPU访问对齐的数据时,效率是最高的。当数据大小的数据魔术的内存地址总是0时,数据是对齐的。如果CPU试图读取的数据值没有正确地对齐,CPU可以产生一个异常,也可以执行多次对齐的内存访问,已读取完整的未对齐数据值。
数据如果不对齐,就会导致内存的多次访问,放慢应用程序的运行速度。说明数据的对齐是非常重要的。
x86CPU
实现对齐的方式是通过EFLAGS
寄存器的AC
标志位。CPU首次加电时这个值为0。
- 如果这个标志是0,就通过一系列的操作,不耽误CPU顺利访问未对其的数据。
- 如果这个标志是1,如果CPU访问未对齐的数据,就发出一个
INT 17H
中断。这个标志从不被x86的Windows20000
改变,因此应用程序根本看不到这个异常。
Alpha处理器的情况不必关心。