17.1 操作系统的内存管理
(1)内存管理基础
①虚拟内存函数:主要用于预留/提交/释放虚拟内存,在虚拟内存页上改变保护方式、锁定虚拟内存页,以及查询一个进程的虚拟内存等操作,是一组位于底层的函数。
②堆管理函数:相对比较高级一点。Win32中的堆分为两种,一种是进程的“默认堆”,默认堆只有一个,指的是进程可以使用的整个地址空间。一种是“私有堆”,可以随意创意多个私有堆。也可以随意的释放,私有堆全部位于默认堆中。
③标准内存管理函数:总是在默认堆中分配和释释内存,这组函数是常规意义上的内存管理函数。
④内存映射文件:相对比较独立,是为了文件操作的方便性而设立的,当对文件进行操作的时候,一般总是先打开文件,然后申请一块内存用做缓冲区,再将文件数据循环读入并处理,当文件长度大于缓冲区长度的时候需要多次读入,每次读入后处理缓冲区边界位置的数据往往是个麻烦的问题。而内存映射文件是将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容。
(2)虚拟内存与内存映射文件的比较
①两者的联系:和虚拟内存一样,内存映射文件可以用来保留一个进程地址区域,两者都是将一部分内容映射到内存,另一部分放在磁盘上。但不同的是虚拟内存的后备物理存储设备是页交文件,而内存映射文件可以是任何的磁盘文件(如exe和dll文件本身)。
②虚拟内存的实现基础是分页机制与局部性原理。局部性原理是应用虚拟内存提升性能的主要原因,也是虚拟内存有别于内存映射文件的本质。内存映射文件是使进程地址空间的某个区域建立与磁盘文件全部或部分内容的映射,该区域可以像用指针读写内存的方式一样的直接读写,而不必执行文件IO操作,也需要对文件进行缓冲处理。
17.2内存映射文件的简介
(1)内存映射文件的概念
①操作系统提供了一种机制,使应用程序能够通过内存指针,像访问内存一样对磁盘上的文件进行访问。
②通过内存映射文件函数可以将磁盘上文件的全部或部分映射到进程的虚拟地址空间的某个位置,一旦完成了映射,对文件内容的访问就如同在该地址区域内直接对内存访问一样的简单。这样向文件写入数据的操作就是直接对内存进行赋值,而从文件的某个特定位置读取数据也就是直接从内存中取数据。
③当内存映射文件提供了对文件某个特定位置的直接读写时,真正对磁盘文件的读写操作是交由系统底层处理的。而且在写操作时,数据也并非在每次操作时都即时写入磁盘,而是底层会通过缓冲处理来提高系统的整体性能。
(2)使用内存映射文件的好处
①系统对所有的数据传输都是通过4KB大小的数据页面来实现的,这意味着一些小文件的操作将被缓冲入一个大的页面中(4KB),也就是首次存取文件中的某段数据时,会引发一次磁盘操作并将数据所在的一个页面全部读入,到以后对附近数据进行操作时,所需的数据己经被前一次页面操作读入到内存,无需再进行一次磁盘操作,从而提高了系统的性能。
②当内存映射机制是以标准的内存地址形式来访问文件数据,操作系统底层会在后台按页面大小周期性地从磁盘读入数据,这个过程对应用程序是完全透明的。虽然用内存映射文件最终还是要将文件从磁盘读入内存,实质上并没有省略掉什么操作,整体性能可能并没有获得什么提高。但程序的结构发生了变化,缓冲区边界问题不复存在,而且对文件内容的更新操作是由操作系统自动完成的,操作系统会自动判断页面是否为脏页面,并仅将脏页面写入磁盘,比程序自己将全部数据写入文件的效率要高很多。
(3)内存映射文件的实现原理
①虚拟内存的原理:在Windows的页式虚拟存储管理中,地址空间中的每个页面给定时刻都可以是空闲、保留或提交三种状态之一。这些页面根据需要由操作系统交换进内存或换出内存。当内存中的某个页面不再需要时,操作系统将释放该页面以供其他程序使用;当该页面再次成为需求页面时,它将被从物理存储器重新读入内存(这里的物理存储器即可以是物理内存,也可以是磁盘上的页文件)
②内存映射文件的实现基于同样的原理,与实现虚拟内存一样,内存映射文件保留了一个地址空间的区域,并根据需要将物理存储器提交给该区域。(注意这里的物理存储器是磁盘文件)
③Windows不仅使用内存映射文件来访问磁盘数据,也使用它来加载和执行exe和dll,这样可以大大节省页文件空间和应用程序启用运行所需的时间。对于每个进程,系统将可执行的代码页提交到磁盘中的可执行文件中,而数据页(含静态数据段及动态分配的内存)被提交到虚拟内存中。(如下图所示)。对于不同进程间共享的数据页,只要将它们提交到虚拟内存的同样页面就可以了。这样,只要一个进程改变了数据页的内容,通过分页映射机制,其他进程的共享数据区的内容就会同时改变,因为它们实际上存储在同一个地方。
(4)内存映射文件的3个主要用途
①系统加载EXE或DLL文件
操作系统就是用它来加载exe和dll文件,运行exe。这样可以节省页文件空间和应用程序启动的时间。
②访问(大)数据文件
可以通过内存映射文件来访问磁盘上的数据文件。这样可以避免直接使用I/O操作和对文件内容进行缓存。同时利用内存映射文件可以处理超过进程用户区2GB大小的文件。
③进程间数据共享机制
内存映射文件是多个进程共享数据最高效的方式,它也是操作系统进程通信机制的底层实现方 法。RPC、COM、OLE、DDE、窗口消息、剪贴板、管道、Socket等都是使用内存映射文件实现的。
17.3 使用内存映射文件
17.3.1 一般内存映射文件操作的步骤
①使用CreateFile创建文件,获得文件句柄(hFile);//这步非必需的
②调用CreateFileMapping创建文件映射(可指定大小、安全属性、保护属性等)
③使用MapViewOfFile将指定文件偏移处的指定长度的区域映射到曀虚的地址空间中(并分配物理内存),该函数返回虚拟内存地址空间的指针,可以像普通内存那样操作(注意要求地址空间中的空闲的连续区域必须足够大)
④使用UnmapViewOfFile撤消文件映射区域的映射,释放虚拟地址空间。
⑤CloseHandle关闭文件和文件映射的句柄。
17.3.2 使用内存映射文件的详细步骤
(1)第1步:CreateFile创建或打开一个文件内核对象(只讨论前3个参数)
①pszFileName参数:要创建或打开的文件的名称(可包含路径,也可不包含路径)
②dwDesiredAccess参数:用来指定文件的访问权限(对于内存映射文件,必须以只读或读/写方式来打开)
值 |
含义 |
0 |
不可读写。只能用来获取文件的属性 |
GENERIC_READ |
读取文件 |
GENERIC_WRITE |
可写文件 |
GENERIC_READ|GENERIC_WRITE |
可读、可写 |
③dwShareMode:文件的共享模式
值 |
含义 |
0 |
其他任何试图打开文件的操作都会失败。 |
FILE_SHARE_READ |
其他任何通过GENERIC_WRITE来打开文件的操作都会失败。 |
FILE_SHARE_WRITE |
其他任何试图通过GENERIC_READ来打开文件的操作都会失败 |
FILE_SHARE_READ | FILE_SHARE_WRITE |
其他任何试图打开文件的操作都会成功 |
④返回值:成功时会返回一个文件内核对象的句柄,失败时返回INVALID_HANDLE_VALUE!
(2)第2步:创建文件映射内核对象——CreateFileMapping
①CreateFileMapping函数——文件映射对象(相当于虚拟内存,后备存储器可以磁盘文件或“页交换”文件!)
参数 |
含义 |
HANDLE hFile |
需要映射到进程地址空间的文件的句柄。如果指定为INVALID_HANDLE_VALUE时,表示创建一个无文件句柄的文件映射,这种主要用来共享内存。(注意CreateFile的失败时的返回值为INVALID_HANDLE_VALUE。如果不加以检查,可能会出现潜在的问题,即创建了一个共享内存) |
PSECURITY_ATTRIBUTES psa |
安全属性。一般为NULL,表示使用默认的安全属性) |
DWORD fdwProtect |
A、页面的保护属性 ①PAGE_READONLY:完成映射时,可以读取文件中的数据。但调用CreateFile时必须传入GENERIC_READ ②PAGE_READWRITE:完成映射时,可读取和写入文件。调用CreateFile时传GENERIC_READ|GENERIC_WRITE。 ③PAGE_WRITECOPY:完成映射时,可以读取或写入文件。写入操作时将导致系统为页面创建一份副本。在调用CreateFile时必须传入GENERIC_READ或GENERIC_READ|GENERIC_WRITE。 ④PAGE_EXECUTE_READ:完成映射时,可读取也可运行其中的代码。在调用CreateFile时必须传入GENERIC_READ和GENERIC_EXECUTE。 ⑤PAGE_EXECUTE_READWRITE:完成映射时,可读取、写入和执行其中的代码。调用CreateFile时必须传入GENERIC_READ、GENERIC_WRITE和GENERICF_EXECUTE B、将上面5种页面属性与段属性按位或结合,段属性有: ①SEC_NOCACHE:告诉系统不要对内存映射进行页面缓存。把数据写入文件时,会频繁地更新磁盘文件。这标志一般不用。 ②SEC_IMAGE:告诉系统要映射的是一个PE文件映像。当被映射到进程地址空间时。代码段给PAGE_EXECUTE_READ来映射。数据段用PAGE_READWRITE映射。即将映射PE文件并给页面设置相应的保护属性。 ③SEC_RESERVE和SEC_COMMIT:这两个是互斥的,不能用于创建内存映射数据文件。一般用于“稀疏调拨的内存映射文件”。 ④SEC_LARGE_PAGES:为内存映射文件使用大页面内存。但必须满 足以下条件:I:调用CreateFileMapping时指定为SEC_COMMIT。 II映射的大小必须大于GetLargePageMinimum函数的返回值。III必须用PAGE_READWRITE保护属性定义映射。IV用户必须启用内存中“锁定内存页”的用户权限。 |
DWORD dwMaximumSizeHigh |
①内存映射文件的最大大小,以字节为单位。用64位来表示。其中High为高32位,Low为低32位。对于小于4GB文件High始终为0. ②如果要用当前文件的大小来创建内存映射文件,就给这两个参数都传入0。 ③如果想给文件追加大小,这时最大大小应留有余地。如果当前磁盘上的文件大小为0字节,就不能传两个0,因为这相当于告诉系统要创建一个大小为0的文件映射。函数会调用失败,返回NULL |
DWORD dwMaximumSizeLow |
|
PCTSTR pszName |
以0为终止符的字符串,用来给文件映射对象指定一个名称。一般用于进程间共享文件映射内核对象的。 |
备注:①返回值为文件映射的对象句柄。无法创建时,返回NULL ②当CreateFile失败时返回的是INVALID_HANDLE_VALUE(-1),而CreateFileMapping失败时返回的是NULL,不要将这两个错误码搞混了 |
(3)第3步:将文件的数据映射到进程地址空间
①MapViewOfFile函数
参数 |
含义 |
hFileMappingObject |
文件映射对象的句柄,由CreateFileMapping或OpenFileMapping函数返回而得 |
DWORD dwDesiredAccess |
保护属性 ①FILE_MAP_WRITE:可读、可写。在调用CreateFileMapping时必须传为PAGE_READWRITE保护属性。 ②FILE_MAP_READ:可读。在调用CreateFileMapping时可以传入PAGE_READONLY或PAGE_READWRITE ③FILE_MAP_ALL_ACCESS:等于同FILE_MAP_WRITE|FILE_MAP_READ| FILE_MAP_COPY ④FILE_MAP_COPY:可读、可写。写入时会为该页面创建一份副本,在调用CreateFileMapping时必须传入PAGE_WRITECOPY。此时会从页文件中调拨物理存储器,但写入页面以后,页面属性会被改为PAGE_READWRITE。 ⑤FILE_MAP_EXECUTE:将文件中的数据作为代码来执行。在调用CreateFileMapping时可以传入PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ保护属性。 |
DWORD dwFileOffsetHigh |
映射时,可以只将文件的一部分映射到进程的地址空间。(这部分被称为视图View) 这个Offset指的是将文件中的哪个字节映射到视图中的第1个字节。(用64位来表示一个偏移,但这个偏移必须是系统分配粒度的整数倍。在Windows中,即为64KB的整数倍。) |
DWORD dwFileOffsetLow |
|
SIZE_T dwNumberOfBytesToMap |
要把数据文件中的多少字节映射到进程的地址空间。如果指定为0,系统会从偏移量开始到文件末尾的所有部分都映射到视图中 |
(4)第4步:从进程的地址空间撤销对文件数据的映射
①BOOL UnmapViewOfFile(PVOID pvBaseAddress)——撤销映射,释放区域
A、pvBaseAddress为区域的基地址,必须与MapViewOfFile返回值相同
B、UnmapViewOfFile的一个特征是,如果视图最初用FILE_MAP_COPY来映射,那么对文件数据的任何修改实际上是对保存在页交换文件中的文件数据副本的修改。如果在这种情况下调用UnmapViewOfFile,函数不需要对磁盘文件进行任何更新,但它会释放页交换文件中的页面,从而导致数据丢失,用了保留被修改过的数据,必须进行额外操作,如为同一个文件用PAGE_READWRITE标志创建另一个文件映射对象,然后在第1个视图中查找具有PAGE_READWRITE保护属性的页面,将修改过的数据写入文件。
②FlushViewOfFile:将缓存的页面写入磁盘
参数 |
含义 |
PVOID pvAddress |
内存映射文件的视图中第1个字节的地址,函数会把传入的地址向下取整到页面的整数倍。 |
SIZE_T dwNumberOfBytesToFlush |
要刷新的字节数,系统会向上取整,使总字节数为页面大小的整数倍。 |
备注:如果内存映射文件的物理存储器来自网络,那么FlushViewOfFile只保证从当前工作站写入文件数据,并无法保证远端的共享文件的服务器也会把数据写入到磁盘中。为了确保服务器也会把数据写入磁盘,应传FILE_FLAG_WRITE_THROUGH标志给CreateFile |
(5)关闭文件映射对象和文件对象
HANDLE hFile=CreateFile(…); HANDLE hFileMapping=CreateFileMapping(hFile,…); CloseHandle(hFile); //因MapViewOfFile函数的副作用,在此关闭,可消除潜在的资源泄漏。 PVOID pvFile=MapViewOfFile(hFileMapping,…); //该函数会增加对hFile和hFileMapping对象的引用计数(该函数的副作用)。 CloseHandle(hFileMapping); //使用内存映射文件 UnmapViewOfFile(pvFile);
【FileReverse程序】使用内存映射文件把文本文件(ANSI或Unicode)的内容颠倒过来
①IsTextUnicode函数用来判断文件是ANSI或Unicode格式
②_strrev是C运行库的函数,用来将以 字符结束的字符串反转过来。
③文本文件并不是以 结尾,所以需在文件未尾追加一个 .
④每行未尾的 也会被反转成 ,因此必须转回 的顺序。
⑤文件内容颠倒之后,必须将先前在文件未尾追加的 去掉。(只需调用SetEndOfFile)
//FileRev.cpp
/************************************************************************ Module: FileRev.cpp Notices: Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include "resource.h" #include <tchar.h> #include <string.h> //For _strrev ////////////////////////////////////////////////////////////////////////// #define FILENAME TEXT("FileRev.dat") ////////////////////////////////////////////////////////////////////////// BOOL FileReverse(PCTSTR pszPathname, PBOOL pbIsTextUnicode){ *pbIsTextUnicode = FALSE; //打开文件 HANDLE hFile = CreateFile(pszPathname, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE){ chMB("无法打开文件"); return FALSE; } //获取文件大小 DWORD dwFileSize = GetFileSize(hFile, NULL); //创建文件映射对象(该对象比原文件多1个字符,以便在文件结尾处放一个 . //因为不知道文件是Unicode还是Ansi,所以这里假定最坏的情况,增加一个WCHAR //型的 ,而不是CHAR型 HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize + sizeof(WCHAR), NULL); if (hFileMap == NULL){ chMB("无法打开文件!"); CloseHandle(hFile); return FALSE; } //创建文件视图 PVOID pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); if (pvFile == NULL){ chMB("无法创建文件视图!"); CloseHandle(hFileMap); CloseHandle(hFile); return FALSE; } //判断文件是ANSI或Unicode int iUnicodeTestFlags = -1; //测试所有项目 *pbIsTextUnicode = IsTextUnicode(pvFile, dwFileSize, &iUnicodeTestFlags); if (!*pbIsTextUnicode){ //ANSI,以下都是用ANSI版的函数来处理ANSI文件 //将 放入文件结尾处 PSTR pchANSI = (PSTR)pvFile; pchANSI[dwFileSize / sizeof(CHAR)] = 0; //反转文件的内容 _strrev(pchANSI); //将所有的“ ”转回“ ”。 pchANSI = strstr(pchANSI, " "); //查找第一个" " while (pchANSI != NULL) { *pchANSI++ = ' '; *pchANSI++ = ' '; pchANSI = strstr(pchANSI, " "); //查找下一个" " } } else{ //UNICODE //UNICODE,以下都是用ANSI版的函数来处理UNICODE文件 //将 放入文件结尾处 PWSTR pchUnicode = (PWSTR)pvFile; pchUnicode[dwFileSize / sizeof(WCHAR)] = 0; if ((iUnicodeTestFlags & IS_TEXT_UNICODE_SIGNATURE)!=0){ //如果第1个字符是Unicode字节序BOM //0xFEFF,则跳过该字符 pchUnicode++; } //反转文本的内容 _wcsrev(pchUnicode); //将" "转回" " pchUnicode = wcsstr(pchUnicode, L" "); //查找第1个" " while (pchUnicode != NULL){ *pchUnicode++ = L' '; *pchUnicode++ = L' '; pchUnicode = wcsstr(pchUnicode, L" "); //查找下一个" " } } //撤消文件映射 UnmapViewOfFile(pvFile); CloseHandle(hFileMap); //删除先前在文件结尾处的添加的 字符 SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); SetEndOfFile(hFile); CloseHandle(hFile); return TRUE; } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam){ chSETDLGICONS(hwnd, IDI_FILEREV); //禁用“反转”按钮 EnableWindow(GetDlgItem(hwnd, IDC_REVERSE), FALSE); return TRUE; } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify){ TCHAR szPathName[MAX_PATH]; switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_FILENAME: //编辑框内容 EnableWindow(GetDlgItem(hwnd, IDC_REVERSE), Edit_GetTextLength(hwndCtl) > 0); break; case IDC_REVERSE: GetDlgItemText(hwnd, IDC_FILENAME, szPathName, _countof(szPathName)); //复制一份文件(副本) if (!CopyFile(szPathName,FILENAME,FALSE)){ chMB("无法创建新文件!"); break; } BOOL bIsTextUnicode; if (FileReverse(FILENAME,&bIsTextUnicode)){ SetDlgItemText(hwnd, IDC_TEXTTYPE, bIsTextUnicode ? TEXT("Unicode") : TEXT("ANSI")); //打开记事本查看转换后的文件 STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; TCHAR sz[] = TEXT("Notepad ")FILENAME; if (CreateProcess(NULL,sz,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi)){ CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } } break; case IDC_FILESELECT: OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 }; ofn.hwndOwner = hwnd; ofn.lpstrFile = szPathName; ofn.lpstrFile[0] = 0; ofn.nMaxFile = _countof(szPathName); ofn.lpstrFileTitle = TEXT("选择要反转的文件"); ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST; GetOpenFileName(&ofn); SetDlgItemText(hwnd, IDC_FILENAME, ofn.lpstrFile); SetFocus(GetDlgItem(hwnd, IDC_REVERSE)); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nShowCmd) { DialogBox(hInstance, MAKEINTRESOURCE(IDD_FILEREV), NULL, Dlg_Proc); return 0; } ////////////////////////////////文件结束/////////////////////////////////
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 17_FileRev.rc 使用 // #define IDD_FILEREV 1 #define IDC_FILESELECT 101 #define IDI_FILEREV 102 #define IDC_FILENAME 1000 #define IDC_REVERSE 1001 #define IDC_TEXTTYPE 1002 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//FileRev.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "