win32api之内存知识梳理(六)
虚拟内存和物理内存
什么物理内存
物理内存指的是计算机主板上的随机存储器(RAM),它是用来存储计算机当前正在运行的程序和数据的。物理内存的大小是由计算机主板上内存插槽的数量和每个插槽上内存条的大小决定的
物理内存通常是以页式(Page-based)方式进行管理。在这种管理方式下,物理内存被分割成固定大小的页(通常为4KB),每个页都有一个唯一的物理地址
物理内存页可以被操作系统映射到进程的虚拟地址空间中,这个过程被称为内存映射。通过内存映射,操作系统可以让多个进程共享同一块物理内存,或者将物理内存映射到磁盘上的虚拟内存中。
什么虚拟内存
虚拟内存是计算机硬盘上划分出的一部分空间,它被操作系统用来扩展物理内存的容量,使得计算机可以运行更多的程序。
当物理内存不足以容纳当前正在运行的程序和数据时,操作系统会将一部分暂时不使用的数据从物理内存中移动到虚拟内存中,从而释放出物理内存,让正在运行的程序能够继续执行。
虚拟内存的大小可以根据需要动态调整,但是它的访问速度比物理内存要慢得多
虚拟内存地址的划分
虽然一个进程分配的4GB的内存地址, 但是其实真正能用的空间只有低地址的2GB空间, 在低地址的2GB空间中有2个占64KB的空间无法使用, 分别是空指针赋值区和64KB禁入区
分区 | 地址范围 |
---|---|
空指针赋值区 | 0x00000000~0x0000FFFF |
用户模式区 | 0x00010000~0x7FFEFFFF |
64KB禁入区 | 0x7FFF0000~0X7FFFFFFF |
内核 | 0x80000000~0xFFFFFFFF |
私有内存
什么是私有内存
私有内存指的是进程在运行过程中使用的内存空间,只有该进程能够访问和操作这部分内存,其他进程无法直接访问和修改该进程的私有内存空间,这为进程提供了一定的安全性和隔离性。通常使用VirtualAlloc
函数申请的内存属于私有内存
涉及函数
VirtualAlloc
VirtualAlloc函数是Windows API中的一个内存分配函数,用于在进程的虚拟地址空间中分配内存,此函数分配的内存可以使用VirtualAlloc函数来释放。函数执行成功则返回分配内存的首地址,失败则返回NULL。
此函数语法格式如下:
LPVOID VirtualAlloc(
LPVOID lpAddress, // 指定分配内存的起始地址
SIZE_T dwSize, // 指定需要分配的内存大小,通常是以物理页(4kb)为单位
DWORD flAllocationType, // 指定内存分配类型,如MEM_COMMIT、MEM_RESERVE、MEM_RESET等
DWORD flProtect // 指定内存保护方式,如PAGE_READWRITE、PAGE_EXECUTE_READ等
);
其中,lpAddress参数可以指定所需内存区域的首地址,如果为NULL则由系统自动分配
至于常用的内存的分配类型有以下两种:
MEM_COMMIT
:表示将分配的内存标记为可用,并分配物理内存MEM_RESERVE
:保留一段地址空间而不分配物理内存
VirtualAllocEx
与VirtualAlloc函数不同的是,VirtualAllocEx函数可以在指定的进程的虚拟地址空间中分配内存区域,返回所分配内存的起始地址
其语法格式如下:
LPVOID VirtualAllocEx(
HANDLE hProcess, // 目标进程句柄
LPVOID lpAddress, // 分配内存的首选地址,可以设为NULL,系统会自动分配
SIZE_T dwSize, // 要分配的内存大小
DWORD flAllocationType,// 分配类型,如MEM_COMMIT(提交分配), MEM_RESERVE(保留分配)
DWORD flProtect // 访问权限,如PAGE_EXECUTE_READWRITE(可执行、可读、可写)
);
WriteProcessMemory
WriteProcessMemory函数用于向指定进程的内存中写入数据,其语法格式如下:
BOOL WriteProcessMemory(
HANDLE hProcess, //指定要写入数据的进程的句柄,该句柄必须拥有PROCESS_VM_WRITE权限
LPVOID lpBaseAddress, //指定要写入数据在内存中的起始地址
LPCVOID lpBuffer, //指向包含要写入的数据的缓冲区指针
SIZE_T nSize, //指定要写入的数据的字节数
SIZE_T *lpNumberOfBytesWritten //可选的指针,用于接收实际写入的字节数
);
VirtualFree
VirtualFree函数用于释放一块由VirtualAlloc函数保留的虚拟内存区域,其语法格式如下:
BOOL VirtualFree(
LPVOID lpAddress, //指定要释放的虚拟内存区域的起始地址
SIZE_T dwSize, //要释放的区域大小
DWORD dwFreeType //如何释放内存,有两种方式:MEM_DECOMMIT、MEM_RELEASE
);
至于dwFreeType参数,有两个可选值:
MEM_DECOMMIT
:释放内存时,指定的物理页被释放,但是该内存区域所对应的虚拟地址空间仍然存在,而且在未来重新使用时,可以重新提交这些物理页,无需重新分配新的物理内存,而是使用之前的物理内存MEM_RELEASE
:释放内存时,整个内存区域的物理内存和虚拟空间地址都会被释放,并且所有物理页都会被系统删除。要注意的是,当使用MEM_RELEASE
参数时, 第二个参数dwSize
应该为0,否则会报错
使用实例
include <iostream>
include <windows.h>
int main()
{
//申请一块可进行读写操作的内存
LPVOID pMemory = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
//释放内存
VirtualFree(pMemory, 0x1000, MEM_COMMIT);
}
共享内存
什么是共享内存
共享内存是一种用于实现进程间通信的机制,它允许多个进程访问同一块物理内存,从而达到共享数据的目的。通常使用CreateFileMapping函数申请的内存属于共享内存
涉及函数
CreateFileMapping
CreateFileMapping函数用于创建一个文件映射对象同时提供一块可用来映射的物理内存,并将此文件映射至物理内存中,其返回值是一个文件映射对象的句柄,若函数调用失败则返回NULL
其语法格式如下:
HANDLE CreateFileMappingW(
HANDLE hFile, //需映射的文件句柄,若值为INVALID_HANDLE_VALUE,则仅提供一块物理内存
LPSECURITY_ATTRIBUTES lpAttributes, //对象的安全属性,一般置NULL
DWORD flProtect, //映射区域的保护属性
DWORD dwMaximumSizeHigh, //文件映射对象的高32位大小
DWORD dwMaximumSizeLow, //文件映射对象的低32位大小
LPCWSTR lpName //映射对象的名称,该参数可以是NULL,但是若使用名称创建映射对象,则多个进程可以通过名称来共享一个对象
);
参数中的dwMaximumSizeHigh和dwMaximumSizeLow用来指定创建的文件映射对象的大小。这两个参数联合起来构成一个64位的值,用来表示文件映射对象的最大大小。如果这两个参数都为0,那么表示文件映射对象的最大大小为文件的当前大小
使用CreateFileMapping函数创建的内存可以被多个进程共享。当多个进程需要访问同一块内存时,可以使用CreateFileMapping函数创建一个文件映射对象,然后通过MapViewOfFile函数映射到每个进程的地址空间中,这样每个进程就可以访问同一块内存了
OpenFileMapping
OpenFileMapping函数用于打开一个已经存在的文件映射对象。函数成功时将返回文件映射对象的句柄,否则返回NULL,其格式如下所示:
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, //指定内存映射对象的访问方式,如FILE_MAP_READ、FILE_MAP_WRITE、FILE_MAP_COPY
BOOL bInheritHandle, //指定是否可以被子进程继承,一般使用默认值FALSE即可
LPCTSTR lpName //指定内存映射文件对象的名称。在不同进程间共享内存时需要使用该参数指定相同的名称
);
MapViewOfFile
MapViewOfFile函数用于将创建的文件映射至进程的地址空间中,使得进程可以直接访问该文件,函数返回值为指向映射视图的指针,可以通过该指针直接访问文件的内容
其语法格式如下:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //文件映射对象的句柄
DWORD dwDesiredAccess, //映射视图的访问类型
//映射视图在文件中的偏移量
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap //映射的字节数
);
dwDesiredAccess
参数用于指定对共享内存区域的访问权限,其常见值如下:
FILE_MAP_READ
:只读访问。FILE_MAP_WRITE
:写访问。FILE_MAP_ALL_ACCESS
:可读可写访问。FILE_MAP_COPY
:创建映射副本,不会对原文件进行修改。
在使用 CreateFileMapping
创建内存映射文件对象之后,可以使用 MapViewOfFile
函数将该文件对象映射到当前进程的地址空间中,这个映射后的区域就称为映射视图
映射视图的访问属性相对映射区域的访问属性而言,只能是更加严格。例如映射视图的属性设置为可读写,那么映射区域的属性只能设置成可读、可写、可读写;若映射视图的属性设置为可写,那么映射区域的属性只能设置成可写,而不能设置成其他的
简单来说,映射区域是为了映射而创建的内存区域, 而映射视图是该内存区域在进程地址空间的投影
UnmapViewOfFile
UnmapViewOfFile函数用于解除文件映射对象与进程地址空间之间的映射,其语法格式如下:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress //映射视图的起始地址
);
使用实例
下面分别是两个进程的代码,进程A的代码用于创建共享内存并向其写入数据,进程B的代码用于向共享内存读取数据
//进程A
include <iostream>
include <windows.h>
define MapFileName L"Memory"
int main()
{
//创建文件映射对象
HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 0x1000, MapFileName);
//将文件映射对象映射至当前进程的地址空间中
LPTSTR hMapView = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_WRITE, 0, 0, 0x1000);
//向共享内存写入数据
*(PDWORD)hMapView = 0x123456;
printf("向内存地址%x写入的数据是:%x",hMapView, *(PDWORD)hMapView);
getchar();
//解除文件映射对象与进程地址空间之间的映射
UnmapViewOfFile(hMapFile);
//关闭文件映射对象的句柄
CloseHandle(hMapFile);
}
//进程B
include <iostream>
include <windows.h>
define MapFileName "Memory"
int main()
{
//打开文件映射对象
HANDLE hMapFile = OpenFileMapping(FILE_MAP_READ, FALSE, MapFileName);
//将文件映射对象映射至当前进程的地址空间中
LPCSTR hMapView = (LPCSTR)MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0x1000);
//读取共享内存的数据
printf("在内存地址%x读取的数据是:%x", hMapView, *(PDWORD)hMapView);
getchar();
//解除文件映射对象与进程地址空间之间的映射
UnmapViewOfFile(hMapFile);
//关闭文件映射对象的句柄
CloseHandle(hMapFile);
}
其执行结果如下所示,可以发现两个内存地址是不一样的,虽然两个进程使用的是同一个共享内存区域, 但是它们将这个区域映射到各自进程的虚拟地址空间的位置是不同的,因此个进程访问共享内存时使用的地址也是不同的,即使它们访问的是同一个物理内存区域
内存映射文件
什么是内存映射文件
内存映射文件是一种将磁盘文件映射至应用程序进程虚拟内存中的技术,使用内存映射文件,应用程序可以将磁盘上的文件视为一个地址空间,向访问内存一样直接访问文件内容
如下图所示,通过CreateFileMapping函数将磁盘文件映射至物理内存中,然后再通过MapViewOfFile函数将这块物理内存映射至进程地址空间
内存映射之文件共享
下面分别有两个进程的代码,进程A的代码用于向共享文件写入数据,进程B的代码用于向共享文件读取数据。这里有一点要注意,使用内存映射文件写入数据时,数据不是立马就能写入,而是会先写到缓存里,释放资源后数据才会写入,若需要立即写入,可使用FlushViewOfFile
函数强制更新缓存
//进程A
include <iostream>
include <windows.h>
define MapFileName "Memory"
int main()
{
//打开文件
HANDLE hFile = CreateFile("E:\\test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//创建文件映射对象
HANDLE hMapFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, MapFileName);
//将文件映射对象映射至当前进程的地址空间中
LPVOID lpMapView = MapViewOfFile(hMapFile, FILE_MAP_WRITE, 0, 0, 0);
//向文件写入数据
PDWORD pBuf = (PDWORD)lpMapView;
*pBuf = 0x44434241;
pBuf += 0x1;
*pBuf = 0x48474645;
getchar();
//解除文件映射对象与进程地址空间之间的映射
UnmapViewOfFile(lpMapView);
//关闭文件映射对象的句柄
CloseHandle(hMapFile);
return 0;
}
//进程B
include <iostream>
include <windows.h>
define MapFileName "Memory"
int main()
{
//打开文件映射对象
HANDLE hMapFile = OpenFileMapping(FILE_MAP_READ, NULL, MapFileName);
//将文件映射对象映射至当前进程的地址空间中
LPVOID hMapView = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0);
//读取文件内容
printf("向内存地址%x写入的数据是:%x", hMapView, *(PDWORD)hMapView);
getchar();
//解除文件映射对象与进程地址空间之间的映射
UnmapViewOfFile(hMapFile);
//关闭文件映射对象的句柄
CloseHandle(hMapFile);
}
执行结果如下所示,先执行进程A的代码写入数据,再执行进程B的代码读取数据
以下是写入前和写入后的文件对比
写拷贝
写拷贝(Copy-on-write)是一种内存管理技术,常用于优化进程间数据传输和共享内存。当一个进程需要访问共享内存中的某个区域时,操作系统将该区域映射到该进程的虚拟地址空间中。如果进程仅仅是读取这个区域的内容,那么它将直接读取共享内存中的数据。但是,如果进程试图修改共享内存中的数据,那么操作系统会为该进程复制一份共享内存的数据,使得该进程可以在独立的副本中进行修改,而不会影响到其他进程
在MapViewOfFile函数中可以通过设置访问类型为FILE_MAP_COPY
来实现写拷贝技术