关于内存管理
32位的Windows系统,每个进程都有4GB(Gigabytes)大小的虚拟内存空间可以寻址(因为32位指针可以使用从0x00000000 – 0xFFFFFFFF内的任何一个值),而64位的Windows系统,每个进程有8TB(Terabytes)大小的虚拟内存空间可以寻址(因为64位指针可以使用从0x0000000000000000 – 0xFFFFFFFFFFFFFFFF内的任何一个值)。一个进程中的所有线程都可以访问属于该进程的虚拟地址空间,但是该线程不允许访问那些属于其他进程的虚拟地址空间,这种保护规则可以防止进程虚拟地址空间被其他进程所破坏。
虚拟地址空间(Virtual Address Space)
被进程使用的虚拟地址空间不是对象所在的实际的物理位置。操作系统为每个进程保留着一个页面映像(Page Map),页面映像是操作系统将虚拟地址空间转换成实际的物理地址空间的内部数据结构。每次一个线程使用一个地址,操作系统会自动将该虚拟地址转换成相应得实际物理地址。
虚拟地址空间被分成几个部分:
内存的低2GB虚拟地址空间(0x00000000 --- 0x7FFFFFFF)是进程可以使用的。
内存的高2GB虚拟地址空间(0x80000000 --- 0XFFFFFFFF)是保留给系统使用的。
注意:Windows Server 2003 Enterprise Edition ,Windows 2000 Advanced Server,Windows NT 4.0 SP3 Server Enterprise Edition内存的低3GB虚拟地址空间(0x00000000 --- 0xBFFFFFFF)是可以被进程随意使用,而内存的高1GB虚拟地址空间(0xC0000000 --- 0xFFFFFFFF)是作为系统使用而保留的。
对于Windows ME/98/95的虚拟内存空间划分如下
0k -- 64K(0xFFFF) | 不可写,这个范围是系统载入时所使用的。是进程私有的。 |
64K(0x10000)-- 4MB(0x3FFFFF) | 为MS-DOS兼容所保留的。这个内存空间是可以被进程可写可读的。这个范围的地址空间包含了一些MS-DOS关系结构和代码,所以进程不应该随意的读写这个区域。是进程私有的。 |
4MB(0x400000)-- 2GB(0x7FFFFFFF) | 可以被用户所使用。该段用户数据可以被进程读写,但是代码只能在该段运行而不能写。是进程私有的。 |
2GB(0x80000000)-- 3GB(0xBFFFFFFF) | 共享区域,可以被所有进程读写。许多系统DLL和其他系统数据存放在该区域。 |
3GB(0xC0000000)-- 4GB(0xFFFFFFFF) | 系统内存,可以被任何进程读写,可是这个区域是一些系统代码驻留区,所以写入该区域可能会破坏操作系统,引发一些潜在的灾难性的后果。 |
虚拟地址空间和物理地址空间
虚拟地址空间比实际物理空间大很多,可以被所有进程所使用。为了增加物理存储的大小,系统使用硬盘作为而外的存储空间。所有运行进程可使用的存储总数是所有的实际物理内存和在硬盘上可以被页文件(paging file)利用的空闲空间。磁盘文件经常被用来增加物理存储空间总数。进程的物理存储空间和虚拟内存地址空间使用内存单位-页(pages)来组织。页面的大小是跟主机有关的。比如在x86计算机上,主机页面大小是4KB。
为了使管理内存有最大的灵活性,系统可在物理内存的页面(pages)和磁盘文件之间相互移动。当一个页面(page)被移动到物理内存,操作系统就会更新受影响进程的页映射(page maps)文件。当系统需要物理内存空间,而此时可能物理内存空间已经没有足够的空间,那么系统会把近来最少使用的物理内存页面(page)移动到页文件(paging file)。操作系统操作物理内存的过程对于应用程序来说是完全透明的。
虚拟地址和物理地址地址转换示意图
页面状
空闲(Free):该页面既没有被提交也没有被保留,不受任何进程的影响。该页面可以用于提交、保留或者同时提交和保留。尝试读或者写一个空闲(free)页面的结果是引发一个违规异常
保留(Reserved):这个页面被保留给将来使用,这个范围的地址不能被用于分配。该页面没跟任何有物理存储空间相联系,并且可以用于提交
提交(Committed):物理存储空间可以被分配给页面,通过内存管理选项来控制访问。当第一次尝试去读写一个页面,操作系统将提交页面初始化并载入物理内存。当进程结束,系统为提交页面释放存储空间。
内存保护状态
一个进程的内存被它的虚拟地址空间隐含保护。另外Windows通过改变处理器状态提供了虚拟内存的保护的硬件措施。例如:进程地址空间的代码页面通过用户模式线程被标记为只读状态,从而防止被修改。
以下列出了Windows提供的内存保护选项,当你分配或者保护一个内存页面的时候你必须明确指明其相应的保护选项。
PAGE_EXECUTE:允许在被提交的页面区域运行。如果尝试去读写该提交的区域,会引发一个访问违规。
PAGE_EXECUTE_READ:允许在被提交的页面区域运行和读取。如果尝试去写入该提交区域,会引发一个访问违规。
PAGE_EXECUTE_READWRITE:允许在被提交的页面区域运行、读取、写入。
PAGE_EXECUTE_WRITECOPY:允许在被提交的页面区域运行、读取、写入。该页面被read-on-write和copy-on-write共享。
PAGE_NOACCESS:不允许在被提交的页面区域进行任何操作。如果尝试去读取、写入或者运行该提交页面,会引发一个访问违规异常,调用通常的保护故障(GP fault)。
PAGE_READONLY:允许在被提交的页面区域进行读操作,如果在该区域尝试写入,会引发一个访问违规。如果系统区分只读访问和执行访问,尝试在该区域运行代码,也会产生一个访问违规。
PAGE_READWRITE:允许在被提交的页面区域进行读写操作。
PAGE_WRITECOPY:给提交区域的页面一个copy-on-write保护。注意:Windows ME/98/95不支持该标志。
PAGE_GUARD:在该区域的页面变成保护页面(Guard Pages)。任何尝试访问一个保护页面将导致系统唤起STATUS_GUARD_PAGE异常,并且关闭保护页面状态。当一次访问尝试导致系统关闭保护页面状态,下面的页面保护将接管。如果一个保护页面异常发生在系统服务期间,系统通常会返回一个故障状态标志。该选项不能跟PAGE_NOACCESS同时使用。注意:Windows ME/98/95不支持这个标志。可以通过PAGE_NOACCESS来完成该过程。
PAGE_NOCACHE:不允许该被提交区域页面使用CPU cache。物理内存的硬件属性应该被指明为“no cache”,但是通常并不推荐这么使用。因此如果两个请求将写入同一个内存地址,只有最后一次写入会发生。
PAGE_WRITECOMBINE:允许与写操作结合的内存访问。如果允许处理器cache内存的写请求被最优化执行。因此如果两个请求写入同一个内存地址,只有最后一次写操作会发生。注意:PAGE_GUARD和PAGE_NOCACHE标志不跟PAGE_WRITECOMBINE同时指定。如果尝试这样做了,函数会返回SYSTEM_INVALID_PAGE_PROTECTION NT错误代码。
Copy-on-Write 保护
Copy-on-Write保护是允许多进程映射它们的虚拟地址空间最优化。它们共享一个物理页面直到其中某个进程修改了该页面。Copy-On-Write是lazy evaluation技术的一部分,该技术允许系统保存物理内存共享,直到某个进程修改了映射页面。
例如:假设两个进程从同样的DLL载入页面到它们的虚拟内存空间。这些虚拟内存页面被映射到同样的无力页面。只要进程不对这些页面进行写操作,页面将会被映射和共享同样的物理页面。
如图:
如果进程1写入这些页面的某一个,这个物理页面的内容被拷贝到另外一个物理页面,进程1的虚拟内存映射也同时被更新。现在两个进程在物理内存空间中有了它们自己的页面实例。因此,一个进程不可能写入共享内存。
如下图所示:
虚拟内存函数
虚拟内存分配 VirtualAlloc
该函数保留或者提交调用进程虚拟内存空间内一个页面区域。除非指定了MEM_RESET标志,否则内存分配回自动初始化为零。
函数原形:
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T swSize,
DWORD flAllocationType,
DWORD flProtect
);
参数:
lpAddress
分配区域的开始地址。如果内存被保留,这个指定的地址被圆整为64K。也就是说如果你分配的内存地址在64K的整数倍内,那么系统自动圆整为64K的整数倍。如果内存准备被保留和提交,那么地址被圆整为一个页面大小(在0x86下一个页面为4K)。确定主机的页面大小可以调用GetSystemInfo函数。如果lpAddress为NULL,那么由系统决定分配的区域。
dwSize
区域的大小(单位byte)。如果lpAddress参数为NULL,这个值被圆整为下一个页面的边界范围。另外,分配页面包括所有页面范围,即包含了从lpAddress到(lpAddress+dwSize)的一个或多个字节范围。例如:分配一个跨越一个页面边界范围的页面,导致该页面和被圆整地页面两个页面包含在分配区域里。
FlAllocationType
内存分配类型。这个参数必须包含下面参数值之一。
MEM_COMMIT:根据指定的内存页面区域在内存或者磁盘的页文件里分配物理存储空间。这个函数初始化内存为零。
尝试去提交一个准备被提交的内存页面时不会引起函数调用失败。
如果lpAddress的值为NULL,指定MEM_COMMIT而没有指定 MEM_RESERVE会导致函数保留和提交内存页面。如果lpAddress的值不为NULL,指定MEM_COMMIT而不指定MEM_RESERVE,当虚拟内存空间还没有被保留,那么将导致函数调用失败。
MEM_RESERVE:保留进程的虚拟地址空间范围,但是在内存或者磁盘的页文件里不分配任何实际的物理存储空间。
你可以在VirtualAlloc函数调用后提交保留的内存页面。
MEM_RESET: 指定在内存范围内由lpAddress和dwSize指定的数据不再有用。这个页面不应该被读取或者写入页文件。但是该内存块可能会被之后使用,因此不应该被decommitted。这个值不能跟其他标志混合使用,既不能使用or操作将其跟其他标志值一起使用。
使用这个值不能担保操作范围清空,如果你要使其清空,你可以decommit内存然后从新提交它。
当你指定MEM_RESET,VirtualAlloc函数忽略fProtect。但是你必须还得设置一个有效的保护值,像PAGE_NOACCESS。
如果你使用MEM_RESET而且内存范围被映射到一个文件,那么VirtualAlloc返回一个错误。建议:只是当映射到一个页面文件的时候才使用该标志。
注意:Windows ME/98/95不支持该标志。
MEM_LARGE_PAGES:使用大页面支持来分配内存。这个大小必须最小大页面的倍数。可以通过GetLargePageMinimum函数来获得该值。
MEM_PHYSICAL: 分配物理内存用于读写访问。这个值单独用于AWE内存(Address Windowing Extensions)。
这个值必须跟MEM_RESERVE一同使用,并且不能用or操作跟其他值一起使用。
MEM_TOP_DOWN: 在最高的地址空间分配内存。
注意:Windows ME/98/95不支持该标志。
MEM_WRITE_WATCH:引起系统跟踪被写入分配空间的页面。如果你指定这个值,你必须也指定MEM_RESERVE。
当区域被分配或者写跟踪状态被重置,要想获得已经被写入的页面地址可以调用GetWriteWatch函数。重置写跟踪状态可以调用GetWriteWatch或者ResetWriteWatch函数。
flProtect
被分配页面区域的内存保护。如果页面被提交,你可以使用Or操作连同PAGE_GUARD或者PAGE_NOCACHE一起指定任何一个内存保护选项。
如果函数调用成功,返回分配页面区域的基地址。如果函数调用失败返回一个NULL值。你可以通过GetLastError获得额外错误信息。
VirtualAlloc函数不能保留一个已经被保留的页面,但是它可以提交一个准备提交的页面。也就是说可以提交一个页面范围,不管是否它是否准备被提交,而且函数总是能被成功执行的。
你可以使用VitualAlloc函数保留一个页面块,然后另外在调用该函数从保留块中单独提交页面。这允许进程保留它自己虚拟内存空间的一个区域直到确实需要使用的时候才去映射一个实际的物理内存空间。
VirtualFree函数可以decommit一个被提交的页面、释放一个页面的存储空间或者同时decommit和释放一个被提交的页面。也可以释放一个被保留的页面使其变成空闲页面。
如果lpAddress参数不为空,这个函数使用lpAddress和dwSize参数计算被分配的页区域的大小。页区域的全部状态必须同分配时指定的flAllocationType参数相兼容。
虚拟内存释放函数 VirtualFress
该函数用来释放、decommits或者释放和decommits一个进程调用分配的虚拟地址空间的页面区域。
函数原形:
BOOL VirtualFree(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD dwFreeType
);
参数:
lpAddress:指向将要被释放的页区域的基址。
如果dwFreeType参数是MEM_RELEASE,该参数必须是VirtualAlloc函数使页区域被保留时返回的基址。
dwSize :被释放的内存区域大小,单位是字节(byte)。
如果dwFreeType参数是MEM_RELEASE,这个参数必须置零。这个函数的将释放所有由VitrualAlloc函数保留的区域。
如果dwFreeType参数为MEM_DECOMMIT这个函数decommits所有内存页面。包括从lpAddress到lpAddress+dwSize大小的内存页面。如果lpAddress是VirtualAlloc函数返回的基址,并且dwSize是零,这个函数decommits所有由VirtualAlloc分配的区域。之后所有的区域被设置成保留状态。
dwFreeType:内存释放操作类型。
MEM_DECOMMIT:不提交那些指定的已经被提交的页面区域。在这个操作之后页面变成保留状态。
如果你尝试不提交一个没有提交的页面,这个函数不会返会失败。也就是说你可以提交一个页面区域而不管是否已经设置提交状态。
注意:不要同MEM_RELEASE标志一起使用。
MEM_RELEASE:释放指定的页面区域。在这个操作之后这些页面变成空闲状态。
如果你指定这个标志,你必须把dwSize置为0,并且lpAddress必须指向调用VirtualAlloc函数时返回的基址。
如果区域中的任何页面试正在提交的,这个函数首先decommits然后再释放它们。
从不同状态来释放页面,这个函数是不会调用失败的。
注意:不要同MEM_DECOMMIT一同使用。
如果这个函数成功调用,返回值为非0,如果失败返回0,不过你可以通过调用GetLastError函数来获得更多的错误信息。
VirtualFree函数可以decommit不同状态的页面区域。如果一个页面被decommit但是没有释放,状态标志会被转变成保留状态。你可以随后调用VirtualAlloc再次提交,或者释放。。如果尝试读写一个保留页面那么会引发一个访问违规异常。同样该函数也可以释放不同状态的页面区域。
注意,在内存被释放或者 decommited ,你不能再次访问该内存,但是那些内存越有的信息会保留,如果你尝试访问那么会引发一个反问违规异常。(待续)