Windows API
用于16位Windows的API(Windows1.0~Windows3.1)称作“Winl6”,用于32位Windows的API(Windows 9/NT/2000/XP/7/10)称作“Win32”。64位WindowsAPI的名称和功能基本没有变化,还是使用Win32的函数名,只不过是用64位代码实现的。
在Win32API函数字符集中,“A”表示ANSI,“W”表示Widechars(即Unicode)。前者就是通常使用的单字节方式;后者是宽字节方式,以便处理双字节字符。每个以字符串为参数的Win32函数在操作系统中都有这两种方式的版本。MessageBoxExA函数其实是一个替换翻译层,用于分配内存,并将ANSI字符串转换成Unicode字符串,系统最终调用Unicode版的MessageBoxExW函数执行。当MessageBoxExW函数返回时,它便释放内存缓存。在这个过程中,系统必须执行这些额外的转换操作,因此,ANSI版的应用程序需要更多的内存及更多的CPU资源,而Unicode版的程序在NT架构下的执行效率高了很多。
WOW64
WOW64(Windows-on-Windows 64-bit)是64位Windows操作系统的子系统,可以使大多数32位应用程序在不进行修改的情况下运行在64位操作系统上。64位的Windows,除了带有64位操作系统应有的系统文件,还带有32位操作系统应有的系统文件。Windows的64位系统文件都放在一个叫作“System32”的文件夹中,\Windows\System32文件夹中包含原生的64位映像文件。为了兼容32位操作系统,还增加了\Windows\SysWOW64文件夹,其中存储了32位的系统文件。64位应用程序会加载System32目录下64位的kernel32.dll、user32.dll和ntdll.dll。当32位应用程序加载时,WOW64建立32位ntdll.dll所要求的启动环境,将CPU模式切换至32位,并开始执行32位加载器,就如同该进程运行在原生的32位系统上一样。WOW64会对32位ntdll.dl的调用重定向ntdll.dll(64位),而不是发出原生的32位系统调用指令。WOW64转换到原生的64位模式,捕获与系统调用有关的参数,发出对应的原生64位系统调用。当原生的系统调用返回时,WOW64在返回32位模式之前将所有输出参数从64位转换成32位。WOW64既不支持16位应用程序的执行(32位Windows支持16位应用程序的执行),也不支持加载32位内核模式的设备驱动程序。WOW64进程只能加载32位的DLL,不能加载原生的64位DLL。类似的,原生的64位进程不能加载32位的DLL。
虚拟内存
在默认情况下,32位Windows操作系统的地址空间在4GB以内。Win32的平坦内存模式使每个进程都拥有自己的虚拟空间。对32位进程来说,这个地址空间是4GB,因为32位指针拥有00000000h~FFFFFFFFh的任何值。此时,程序的代码和数据都放在同一地址空间中,不必区分代码段和数据段。
虚拟内存(Virtual Memory)不是真正的内存,它通过映射(Map)的方法使可用虚拟地址(VirtualAddress)达到4GB,每个应用程序可以获得2GB的虚拟地址,剩下的2GB留给操作系统自用。在Windows NT中,应用程序甚至可以获得3GB的虚拟地址。
Windows是一个分时的多任务操作系统,CPU时间在被分成一个个时间片后分配给不同的程序。在一个时间片里,与这个程序的执行无关的内容不会映射到线性地址中。因此,每个程序都有自己的4GB寻址空间,互不干扰。在物理内存中,操作系统和系统DLL代码需要供每个应用程序调用,所以它们在任意时刻必须被映射。用户的EXE程序只在自己所属的时间片内被映射,用户DLL则有选择地被映射。
简单地说,虚拟内存的实现方法和过程如下。
①当一个应用程序启动时,操作系统就创建一个进程,并给该进程分配2GB的虚拟地址(不是内存,只是地址)。
②虚拟内存管理器将应用程序的代码映射到那个应用程序的虚拟地址中的某个位置,并把当前需要的代码读入物理地址(注意:虚拟地址与应用程序代码在物理内存中的位置是没有关系的)。
③如果使用DLL,DLL也会被映射到进程的虚拟地址空间中,在需要的时候才会被读入物理内存。
④其他项目(数据、堆栈等)的空间是从物理内存中分配的,并被映射到虚拟地址空间中。
⑤应用程序通过使用其虚拟地址空间中的地址开始执行。然后,虚拟内存管理器把每次内存访问映射到物理位置。
看不明白上面的步骤也不要紧,但要明白以下几点。
·应用程序不会直接访问物理地址。
虚拟内存管理器通过虚拟地址的访问请求来控制所有的物理地址访问。
· 每个应用程序都有独立的4GB寻址空间,不同应用程序的地址空间是彼此隔离的。
DLL程序没有“私有”空间,它们总是被映射到其他应用程序的地址空间中,作为其他应用程序的一部分运行。其原因是:如果DLL不与其他程序处于同一个地址空间,应用程序就无法调用它。
使用虚拟内存的好处是:简化了内存的管理,弥补了物理内存的不足,可以防止多任务环境下应用程序之间的冲突。
64位Windows操作系统提供了16TB的有效寻址空间,其中的一半可用于用户模式的应用程序。
进程
| 分区 | X86 32位Windows |
| 空指针赋值区 | 0x00000000 - 0x0000FFFF |
| 用户模式区 | 0x00010000 - 0X7FFEFFFF |
| 64KB禁入区 | 0x7FFF0000 - 0X7FFFFFFF |
| 内核 | 0x80000000 - 0xFFFFFFFF |
1. 任何进程都是别的进程创建的:CreateProcess
2. 进程的创建过程
1. 映射EXE文件
2 创建内核对象EPROCESS
3. 映射系统DLL(ntdll.dll)
4. 创建线程内核对象ETHREAD
5. 加载用户DLL
6. 系统启动线程
内核对象
进程,线程,文件,互斥体,事件等在内核都有一个对应的结构体,这些结构体由内核负责管理。我们管这样的对象叫做内核对象。进程(EPROCESS),线程(ETHREAD)
句柄表
windows定义了很多内核对象:进程对象、线程对象、互斥量对象、信号量对象、事件对象、文件对象等等。在调用相应的函数创建这些对象后,我们都可以通过HANDLE类型的句柄来引用它们。或许你在一些书上看到过说句柄相当于指针,它指向具体的对象。在某种程度上来说这是不错的,但是进一步深入探究时就会发现这样的说法很不准确。说到句柄就不能不提句柄表,句柄必须通过句柄表才能找到所引用的内核对象。
任意进程,只要每打开一个对象,就会获得一个句柄,这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。句柄本身是进程的句柄表中的一个结构体,用来描述一次打开操作。句柄值则可以简单看做句柄表中的索引,并不影响理解。HANDLE的值可以简单的看做一个整形索引值。
一 个 进 程 可 能 同 时 打 开 许 多 对 象 ,跟 许 多 对 象 建 立 连 接 , 还 可 以 跟 同 一 个 对 象 建 立 起 多 个 连 接 。 所 以 每 个 程 都 需 要 有 个 句 柄 表 用 来 记 录 , 维 持 这 些 连 接 。 所 以 , 句 柄 表 最 基 本 的 作
用 就 是 一 张 句 柄 与 目 标 对 象 之 间 的 对 照 表 , 而 句 柄 表 中 的 每 个 表 項 , 则 代 表 着 一 个 具 体 的 连 接 。 所 以 , 在 "进 程 控 制 控 制 *"EPROCESS#“中有 指 针 ObjectTable, 用 来指 向 本 进 程 向 柄 表。
每个进程都有自己的句柄表,互不影响,A进程的句柄不能到B句柄去用,因为B的句柄表中可能没有这个句柄,也可能有这个句柄但是和A指向的内核对象不同
句柄可以被继承,子进程可以继承父进程的句柄
全局句柄表
1) 所有的进程和线程无论无论是否打开,都在这个表中。
2) 每个进程和线程都有一个唯一的编号:PID和CID 这两个值其实就是全局句柄表中的索引。进程句柄和线程句柄只能在进程的句柄表中使用
线程
线程是附属在进程上的执行实体,是代码的执行流程
一个进程可以包含多个进程,但一个进程至少要包含一个线程
线程安全问题
每个线程都有自己的栈,而局部变量是存储在栈中的不与其它线程共享,但是全局变量确不同多个线程可以同时访问它
如何解决这个问题呢:
CRITICAL_SECTION 临界区
当一个线程执行了EnterCritialSection之后,cs里面的信息便被修改,以指明哪一个线程占用了它。在这个线程尚未执行LeaveCriticalSection之前,其它线程碰到EnterCritialSection语句的话,就会处于等待状态,相当于线程被挂起了。 这种情况下,就起到了保护共享资源的作用。这里是在同一个cs变量的情况下
不同进程访问同一个资源如何保证安全
互斥体
互斥体还可以判断进程是否已经启动,做单开,互斥体也可用于线程安全
CreateMutex(创建互斥体),WaitForSingleObject(获取互斥体),ReleaseMutex(释放互斥体)
防止多开
int main(int argc, char* argv[])
{
HANDLE m_hMutex = CreateMutex(NULL,TRUE,"cplusplus_me");
DWORD dwRet = GetLastError();
if (m_hMutex)
{
if (ERROR_ALREADY_EXISTS == dwRet)
{
printf("程序已经在运行中了,程序退出!\n");
CloseHandle(m_hMutex);
return 0;
}
}
else
{
printf("创建互斥量错误,程序退出!\n");
CloseHandle(m_hMutex);
return 0;
}
while(1)
{
printf("cplusplus_me\n");
}
CloseHandle(m_hMutex);
return 0;
}互斥体与临界区的区别
1. 临界区只能用于单个进程间的线程控制
2. 互斥体可以设定等待超时,临界区不能
3. 线程意外终止时,Mutex可以避免无限等待
4. Mutex效率没有临界区高
事件
可以用于线程间的通知,线程间互斥有序的执行代码
CreateEvent,SetEvent,ResetEvent
例如,控制多个线程同时从WaitForSingleObject的等待中唤醒
例如,多个线程同时执行任务,先完成的需等待最后完成的,然后一起执行后面的
例如,一个线程生产,一个线程消费,控制生产一个之后,需等消费线程消费掉之后,才能继续消费另一个,用2个事件,生产线程生产出来后SetEvent消费事件,自己进入等待
DWORD WINAPI ThreadRun(LPVOID lpParam)
{
//如果互斥体有信号则把它变为无信号函数返回
//如果互斥体无信号则根据参数二等待互斥体变成有信号然后把互斥体变为无信号,函数返回
//这里进来会一直等待直到有事件通知后变为有信号
WaitForSingleObject(g_hEvent, INFINITE);
printf("run %d\n", lpParam);
return 0;
}
int main()
{
//创建通知事件,为无信号状态,这样WaitForSingleObject就会等待
g_hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("text"));
HANDLE szHandleThread[2] = { 0 };
szHandleThread[0] = CreateThread(NULL, 0, ThreadRun, (LPVOID)1, 0, 0);
szHandleThread[1] = CreateThread(NULL, 0, ThreadRun, (LPVOID)2, 0, 0);
Sleep(3000);
SetEvent(g_hEvent);
//等待线程结束
WaitForSingleObject(szHandleThread[0], INFINITE);
WaitForSingleObject(szHandleThread[1], INFINITE);
CloseHandle(g_hEvent);
getchar();
return 0;
}
GUI图形设备接口
GUI的接口在user32.dll中,GDI的接口在gdi32.dll中
1. 设备对象(HWND) 这个是全局是不属于某个进程,有全局的表保存HWND和内核的窗口对应
2. DC(设备上下文, Device Contexts)
3. 图形对象
图形对象
GDI中的绘图工具(GDI对象):
Pen:用于绘制线条
Brush:用于填充颜色
Front:用于决定字符样式
Bitmap:保存位图格式图像
Palette:绘图时可使用的颜色集
一般的画图步骤
//1. 设备对象
HWND hWnd = 0; //桌面句柄
//2. 获取设备上下文
HDC hdc = GetDC(hWnd);
//3. 创建画笔
HPEN hpen = CreatePen(PS_SOLID, 2, RGB(255, 0, 5));
//4. 关联
SelectObject(hdc, hpen);
//画线
LineTo(hdc, 100, 100);
//释放资源
DeleteObject(hpen);
ReleaseDC(hWnd, hdc);
Windows程序的入口
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //模块句柄 模块的起始地址
_In_opt_ HINSTANCE hPrevInstance, 永远为空
_In_ LPWSTR lpCmdLine, 启动参数
_In_ int nCmdShow)
消息
消息的产生过程
鼠标,键盘点击等操作->操作系统将消息放到对应线程(创建窗口的线程)的消息队列中->线程调用GetMessage取出消息->线程调用DispatchMessage分发消息->由内核发起调用,执行窗口过程函数
子窗口控件
1. Windows提供了几个预定义的窗口类以方便我们的使用,我们一般就把他们叫作子窗口控件,简称控件
2. 控件会自己处理消息,并在自己状态发生改变时通知父窗口
3. 预定义的控件有:
按钮,复选框,编辑框,静态文本框,滚动条等
虚拟内存与物理内存
操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:
因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的
因为内存时随机分配的,所以程序运行的地址也是不正确的。
于是针对上面会出现的各种问题,虚拟内存就出来了。
在之前一篇文章中进程分配资源介绍过一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。
进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
进程开始要访问一个地址,它可能会经历下面的过程
每次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。

再来总结一下虚拟内存是怎么工作的
当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)
利用虚拟内存机制的优点
既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系
当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存
私有内存和共享内存
私有内存的意思就是这块内存申请只在本进程的物理页当中. 共享内存就是这个物理页 A B两个进程都可以使用.
私有内存申请API
VirtualAlloc / virtualAllocEx
LPVOID VirtualAlloc(
LPVOID lpAddress, 你要申请的地址.可以指定地址.但是物理页我们不知道那个地址是否存在.所以一般为0
SIZE_T dwSize, 申请内存的大小.一般为一个页.虽然MSDN说了一Byte(字节) 为主. 但是我们知道物理内存是一个页.所以申请一个页即可.
DWORD flAllocationType, 申请内存的类型. 是这样的.我们申请内存在物理页. 可以有两种类型.一种类型就是物理内存直接申请.一种就是物理内存占位置.并不申请.一般用第一种.
DWORD flProtect 内存的状态.我们申请的内存状态是可读的 还是可写的. 还是可读写的.virtualAllocEx 是远程内存申请.就是说我们可以通过指定的进程. 给这个指定进程申请内存.
new malloc的区别. 请注意.真正申请内存的其实是API. 而new malloc 是申请堆内存. 意思就是说. new malloc其实就是在已申请的内存上面划分出来了一块虚拟内存给你使用.
不管你使用没使用. 而且new关键字本质也就是malloc 只不过可以进行构造. 而 malloc的底层是通过 HeapAlloc申请的. 并没有进0环(内核)
共享内存申请
共享内存其实就是物理页可以共享使用了. A进程申请物理页往这个物理页填写内容. B进程就可以读取了.
我们具体的API
1. 申请物理页内存API
HANDLE CreateFileMapping( HANDLE hFile, 文件映射.申请的物理页可以跟文件相映射.如果不需要文件只申请物理页则不需要.
LPSECURITY_ATTRIBUTES lpAttributes, SD安全属性,每个内核对象都需要的安全属性
DWORD flProtect, 权限.你申请的这个物理页是可读的可写的还是可读写.
DWORD dwMaximumSizeHigh, 申请内存的高32位. windows为了支持64我操作系统.所以给了高低32位来保存地址. 如果是32位地址.则不需要.填0即可.
DWORD dwMaximumSizeLow, 低32位,你要申请的物理页内存的大小
LPCTSTR lpName 进程共享物理页的名字.如果希望这个物理页B进程可以使用则需要给一个名字.
返回值: 返回物理页句柄索引.有创建物理页 也有打开物理页 主要是B进程使用.
HANDLE OpenFileMapping( DWORD dwDesiredAccess, // access mode
BOOL bInheritHandle, // inherit flag
LPCTSTR lpName // object name);线性地址(虚拟地址) 关联物理页
上面申请了物理页.那么我们还需要将这个物理页映射到线性地址.需要的API如下.
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 物理页句柄
DWORD dwDesiredAccess, // 线性地址访问权限,注意跟物理页最好一直.或者比物理页更严格.
DWORD dwFileOffsetHigh, // 映射线性地址的偏移位置 高32位
DWORD dwFileOffsetLow, // 低32位
SIZE_T dwNumberOfBytesToMap // 内存映射结束位置;取消关联
BOOL UnmapViewOfFile( LPCVOID lpBaseAddress // starting address); 虚拟地址关闭物理页映射则使用CloseHandle. PS: 知识 引用计数-1. 为0了则会关闭.
文件系统
基本概念
1、磁盘分区(Partitions)
磁盘是装到计算机上的存储设备,比如常见的硬盘。磁盘分区是为了便于管理和使用物理硬盘,而在一个物理硬盘上划分可以各自独立工作的一些逻辑磁盘。比如一块80GB的硬盘可以划分为4个20GB的分区来使用,对操作系统来说这4个20GB的分区是4块独立的逻辑磁盘。
2、卷(Volumes)
卷,也称为逻辑驱动器,是NTFS、FAT32等文件系统组织结构的最高层。卷是存储设备(如硬盘)上由文件系统管理的一块区域,是在逻辑上相互隔离的存储单元。一个磁盘分区至少含有一个卷,卷也可以存在于多个磁盘分区上,仅存在于一个磁盘分区上的卷称为“简单卷”,仅存在于多个磁盘分区上的卷称为“多分区卷”或“跨区卷”。在最常见的情况下,一个分区只包含一个卷,一个卷也只存在于一个分区上,所以两者容易混淆。卷存在卷标,程序可以通过卷标访问卷。
在Windows系统中,文件路径的长度被限制260个字符,这260个字符包括卷标、路径、主文件名和扩展名以及分隔符,因此,文件名的长度为260减去文件所在目标的路径长度。在Windows NT系统中,字符使用的是Unicode宽字符。英文和中文都占用两个字节。
特殊字符则不能在文件名的任何位置出现。特殊字符一共有9个,分别为:“/”、“∣”“\”、“*”、“?”、"<"、“>”、“””、“:”。特殊字符在系统或者命令行下代表一些特殊意义。
HANDLE handle; //声明一个文件句柄变量,用于保存打开的文件句柄
DWORD Num;
handle= ::CreateFile("new.tmp",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_ALWAYS,
FILE_FLAG_DELETE_ON_CLOSE,NULL); //在当前目录下创建一个new.tmp文件
if(INVALID_HANDLE_VALUE!= handle ) //判断创建文件是否成功
{
::SetFilePointer(handle,0,0,FILE_BEGIN); //创建成功后,移动当前文件指针到文件开头处
char Buffer[] = "这是个刚创建的文件"; //声明一个要写的文件数据
::WriteFile(handle,Buffer,sizeof(Buffer),&Num,NULL); //写文件
ZeroMemory(Buffer,sizeof(Buffer)); //清空Buffer变量
::SetFilePointer(handle,0,0,FILE_BEGIN); 写完数据后,当指文件指针为移动,下面我们要读出刚才写的数据,所以我们要把文件指针移到文件开始处
::ReadFile(handle,Buffer,sizeof(Buffer),&Num,NULL); //读出刚才写的文件数据
MessageBox(Buffer); //把读出来的数据弹出来。
::CloseHandle(handle); //关闭文件。
} 内存映射文件
与虚拟内存相似,内存映射文件允许开发人员预定一块地址空间区域并给区域调拨物理存储器。
不同之处在于,内存映射文件的物理存储器来自磁盘上已有的文件,而不是来自系统的页交换文件。
一旦把文件映射到地址空间,我们就可以对它进行访问,就好像整个文件都已经被载入内存一样。
内存映射文件主要用于以下三种情况。
1、系统使用内存映射文件来载入并运行.exe和动态链接库(DLL)文件。这大量节省了页交换文件的
空间以及应用程序启动的时间。这里他们是写拷贝的写的时候会先拷贝到别的地方,然后修改别的地方实现修改,这就是为什么我们改了系统的dll但是对别的程序却没有影响
2、开发人员可以用内存映射文件来访问磁盘上的数据文件。这使得我们可以避免直接对文件进行
I/O操作和对文件内容进行缓存。
3、通过使用内存映射文件,我们可以在同一台机器的不同进程之间共享数据。Windows的确提供了
其他的一些方法来在进程间传送数据,但这些方法都是通过内存映射文件来实现的。因此,如果在
同一台机器的不同进程之间共享数据,内存映射文件是最高效的方法。
使用内存映射文件的步骤:
1、创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射文件的那个磁盘文件。
CreateFile();
2、创建一个文件映射内核对象,来告诉系统文件的大小以及我们打算如何访问文件。
CreateFileMapping();
3、告诉系统把文件映射对象的部分或全部映射到进程的地址空间中。
MapViewOfFile();
用完内存映射文件之后,必须执行以下三步进行清理。
1、告诉系统从进程地址空间中取消对文件映射内核对象的映射。
UnmapViewOfFile();
2、关闭文件映射内核对象。
CloseHandle();
3、关闭文件内核对象。
CloseHandle();