Windows API笔记(一)内核对象
Windows API笔记(二)进程和进程间通信、进程边界
Windows API笔记(三)线程和线程同步、线程局部存储
Windows API笔记(四)win32内存结构
Windows API笔记(五)虚拟内存
Windows API笔记(六)内存映射文件
Windows API笔记(七)堆
Windows API笔记(八)文件系统
Windows API笔记(九)窗口消息
Windows API笔记(十)动态链接库
Windows API笔记(十一)设备I/O
在Win32中,每个进程的虚地址空间是4GB。32位指针的值能从0x00000000到0xFFFFFFFF。这使得指针能有4,294,967,296种值,这覆盖了进程的4GB空间。
32位CPU,32位操作系统,32位应用程序的定义是什么?
所有的Win32进程都有自己的私人地址空间。当进程种的线程在运行时,线程只能访问属于本进程的内存。对运行的线程来说,属于其他所有进程的内存是隐藏的和不可访问的。
进程拥有的4GB空间是虚地址空间,而不是物理内存。该地址空间只不过是一段内存地址。物理内存需要被分配或映射到地址空间的分区。如何将物理内存映射到虚地址空间的分区?
范围 | 大小 | 作用 | 说明 |
---|---|---|---|
0x00000000 - 0x0000FFFF | 64KB | 用于NULL指针分配,不可访问 | NULL指针区域,师徒读写这一分区中内存地址将会引起访问冲突 |
0x00010000 - 0x7FFEFFFF | 2GB - 64K - 64KB | 属于Win 32进程私有,非保留,可使用 | 进程的私人地址空间,DLL被装入这一分区,内存映射文件映射到该分区 |
0x7FFF0000 - 0x7FFFFFFF | 64KB | 用于环指针分配,不可访问 | 该分区禁止使用 |
0x80000000 - 0xFFFFFFFF | 2GB | 用于操作系统,不可访问 | 装入Windows NT执行体、内核和设备驱动 |
要使用虚地址空间中的一块保留区域,就必须分配物理存储,然后将其映射到保留区域,这一过程叫做提交物理存储。物理存储总是以页为单位提交的。要把物理存储提交给保留区域,还要调用VirtualAlloc函数。
通过调用VirtualAlloc来向地址空间提交物理存储时,空间实际上是分配在虚地址空间的一块连续内存,在物理存储上不一定连续。
物理存储被认为是计算机上的RAM的多少。在Win32中,Microsoft提供了对硬盘交换文件形式的虚拟内存的支持。但只有在CPU直接支持交换文件的情况下操作系统才能使用它们。从应用程序的角度来说,交换文件透明地增加了应用程序所能使用地RAM(或存储)地数量。如果计算机上有2GB的RAM,在硬盘上还有2GB的交换文件的话,运行的应用程序会认为计算机共有4GB的RAM。
并不是实际上有4GB的RAM。相反,操作系统同CPU一起合作,把RAM的部分内容保存在交换文件中,而在运行程序需要的时候又把交换文件中的内容装回到RAM中。如果不适用交换文件,系统只是认为应用程序能使用的RAM较少一些。
物理存储 = RAM + 页面文件/虚拟内存 + 不包含在页面文件中的物理存储
当进程中的线程试图访问进程的地址空间中的一块数据时,必然会发生两件事情中的一件,如图:
当操作系统需要把内存和页面中的数据来回交换的时候,硬盘就会越多地咔咔作响,系统也会越慢。因为操作系统把时间都花在把页面换入和换出,而不是运行程序上了。
注意:“虚地址/虚拟内存地址”和“虚拟内存”没有任何关系,“虚拟内存地址”指应用程序虚地址空间的内存地址;而“虚拟内存”是指页面文件。
当启动一个应用程序时,系统会打开该应用的exe文件来判断它的代码和数据的大小。然后系统保留了地址空间中的一块区域,使该区域的物理存储就是exe文件本身。系统并不从页面中分配空间,而是使用exe文件的实际内容(或映象)作为程序的地址空间的保留区域。这当然会使加载应用程序非常快,而且减少了页面文件的大小。
当程序的文件映象(即exe或dll文件)被用作地址空间的一块区域的物理存储时,它被称为内存映射文件。当装入exe或dll时,系统自动的保留了地址空间的一块区域,把文件的映象映射到这一区域。
由VirtualAlloc函数分配的独立的物理存储页面可用被赋予不同的保护属性。下表给出了Win32的保护属性:
保护属性 | 描述 |
---|---|
PAGE_NOACCESS | 不可访问,试图读、写或执行该区域中的内存的操作都会引起访问错误 |
PAGE_READONLY | 只读,试图写或执行该区域中的内存的操作都会引起访问错误 |
PAGE_READWRITE | 可读可写,试图执行该区域中的内存的操作会引起访问错误 |
PAGE_EXECUTE | 可执行 |
PAGE_EXECUTE_READ | 可执行,可读 |
PAGE_EXECUTE_READWRITE | 可执行,可读可写 |
PAGE_WRITECOPY | 不可执行;试图写会使系统给进程一份物理存储的自己的私有拷贝 |
PAGE_EXECUTE_WRITECOPY | 任何操作都不会引起访问错误;试图写会使系统给进程一份物理存储的自己的私有拷贝 |
注意:在使用VirtualAlloc函数来保留地址空间或提交物理存储时,不能传递PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY。这两个属性是操作系统在映射exe或dll文件时使用的。
Win32允许多个进程共享同一块数据的机制。当所有的进程都把数据视为只读或只执行而不向其中写时,这样做通常没什么问题的。但是如果不同进程中的线程都要写同一块数据时,就将会是一片混乱。
为了避免这种混乱,操作系统赋予共享的数据写拷贝的保护。当进程中的线程试图写共享的数据时,系统会介入,进行下列操作:
这几步完成后,进程就能访问它自己的私有数据了。
除了已经讨论过的保护属性,还要两个保护属性标志:PAGE_NOCACHE和PAGE_GUARD,可以将这两个标志同除PAGE_NOACCESS之外的任一保护属性或(OR)起来。
我们自己写一个程序,然后使用VMMAP程序查看程序的结构,来总结Win32内存结构。
代码如下:
/*Win32 内存结构*/
#include
#include
int main()
{
// 保留区域,512k
LPVOID mem_ptr = VirtualAlloc(NULL, 1024 * 512, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE);
if (NULL == mem_ptr)
{
printf("reserve failed\n");
return -1;
}
printf("reserve address : 0x%X\n", mem_ptr);
// 提交物理存储,系统提交了4k的存储,最小是4k
LPVOID mem_cmt = VirtualAlloc(mem_ptr, 10, MEM_COMMIT, PAGE_READWRITE);
if (NULL == mem_cmt)
{
printf("reserve failed\n");
}
else
{
printf("commit address : 0x%X\n", mem_cmt);
// 初始化内存区域,虽然提交的是len+1的大小,但是最小单位是4K,保留和提交的大小必须是4k的整数倍
memset(mem_cmt, 0, 1024 * 4); // 改成 1024 * 4 + 1 将会出错
}
char *ptr = (char *)mem_ptr;
ptr += 1024 * 5;
printf("startPtr address : 0x%X\n", ptr);
// 提交物理存储
LPVOID mem_cmt2 = VirtualAlloc(ptr, 10, MEM_COMMIT, PAGE_READWRITE);
if (NULL == mem_cmt2)
{
printf("reserve 2 failed\n");
}
else
{
printf("commit 2 address : 0x%X\n", mem_cmt2);
}
// 与上一提交的起始地址间隔10k,导致与上一地址块间隔一页的大小(4k)
ptr += 1024 * 10;
// 提交物理存储
LPVOID mem_cmt3 = VirtualAlloc(ptr, 10, MEM_COMMIT, PAGE_READWRITE);
if (NULL == mem_cmt3)
{
printf("reserve 3 failed\n");
}
else
{
printf("commit 3 address : 0x%X\n", mem_cmt3);
}
// 保留一块小区域,看起始位置是不是分配边界,应该是4k
LPVOID mini_vptr = VirtualAlloc(NULL, 12, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE);
if (NULL == mini_vptr)
{
printf("reserve mini_vptr failed\n");
}
else
{
printf("reserve mini_vptr address : 0x%X\n", mini_vptr);
// 相差几个64k,地址是否是64k的整数倍
printf("mem_ptr - mini_vptr = %d * 64KB , mini_vptr %% 64K = %d\n", ((ULONG)mem_ptr - (ULONG)mini_vptr) / (64 * 1024), (ULONG)mini_vptr % (64 * 1024));
}
system("pause");
// 释放保留区域
VirtualFree(mem_ptr, 0, MEM_RELEASE);
if (NULL != mini_vptr)
VirtualFree(mini_vptr, 0, MEM_RELEASE);
return 0;
}
程序运行结果:
使用VMMAP查看内存结构:
主要看两个保留地址:0x7FD60000和0x7FD50000。
地址 | 数据类型 | 保留或提交的内存大小 | 占用块 | 保护类型 |
---|---|---|---|---|
0x7FD50000 | private data | 4k | 1 | Reserved |
0x7FD51000 | unusable | 60k | ||
0x7FD60000 | private data | 512k | 1 | Read/Write |
0x7FD60000 | private data | 512k | 1 | Read/Write |
vmmap内存查看工具
术语 | 意义 | 备注 |
---|---|---|
虚拟地址空间 | 进程私有的地址空间,虚拟地址空间是一段内存地址,并不是实际的物理存储空间 | Win32中每个进程的虚拟地址空间是4GB,在64位操作系统中更大,虚拟地址需要将物理存储映射到虚拟地址空间才能正确访问而不引起冲突 |
物理内存,物理存储 | 实际的存储空间 | RAM + 页面文件(虚拟内存)+ 不包含在页面文件中的物理存储 |
区域 | 一块连续的虚拟地址空间 | 调用VirtualAlloc函数来分配/保留区域 |
保留(reserving) | 分配一块区域的行为被称为保留(reserving) | |
释放(release) | 不需要访问一块区域时,就该将其释放(release) | 调用VirtualFree函数来完成 |
分配单元 | 因CPU平台不同而不同,x86 是64KB | |
分配单元边界 | 保留区域时的起始地址 | 分配单元(64KB)的整数倍 |
页 | 页是系统用来管理内存的单位 | 页大小因CPU不同而不同,Win32的x86使用4KB,当保留或提交一块区域时,系统会确保该区域的大小是系统的页大小的整数倍 |
提交(commit) | 提交物理存储,为一块保留区域分配物理存储,然后将其映射到保留区域 | 物理存储总是以页为单位提交的 |
页面文件 | 虚拟内存文件,在硬盘上的交换文件 | 页面文件大小是决定应用程序能使用的物理存储的多少的最重要的因素,在RAM较小时页面文件很关键 |
Windows 内存结构:
参考文档: