作者:[email protected] 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月28日
每个进程都有自己的虚拟地址空间,对32位进程来说是4G,对64位来说是4G的平方16E。在Windows下虚拟地址空间分成4个区:空指针赋值区、用户模式分区、64K禁入分区、内核模式分区。
空指针赋值区
这段分区是从0x00000000~0x0000ffff,目的是帮助程序员捕获对空指针的赋值。如果不加以保护,就会造成访问到其他有用的数据区。
用户模式分区
一般情况是2G,有些程序可能需要更多的用户地址空间如sqlserver,windows提供了3G用户分区模式,执行BCDEdit.exe
64位机器上默认OS会保留多余的分区空间,让程序与32位程序一样只使用2G,如果需要整个分区需要打开/LARGEADDRESSAWARE来告诉编译器。
内核模式分区
内核与线程调度、内存管理、文件系统支持、网络支持、驱动设备的代码都在这个分区,这个分区的任何东西都为所有进程共有。如果应用程序试图去读写这段地址则会引发违规。
OS创建一个进程并赋予地址空间时,可用地址空间中大部分都是free或unallocated,必须调用VirtualAlloc
来分配其中的区域,这个操作叫做reserving.
应用程序reserving地址空间时,OS保证区域的起始地址是分配粒度的整数倍,大小是OS页面大小的整数倍。一般前者为64K后者为4K。VirtualFree
用来释放reserving的空间。
为了使用reserving的空间,还需要分配物理存储器,并将存储器映射到所预定的区域,这个操作叫做committing物理存储器。仍然是通过VirtualAlloc
完成。
不需要访问时需要释放物理存储器,这个操作叫做decommitting,还是通过调用VirtualFree
完成 。
线程访问的数据在内存中,CPU会把数据的虚拟内存地址映射到内存的物理地址,然后访问
线程访问的数据不在内存中,而是在paging file的某处,这次不成功的访问称为页面错误,CPU会通知OS,OS在内存中立即找到一个free的页面,如果找不到,OS必须先释放一个已分配的页面
运行一个程序时,OS实际上并不会为该进程的代码和数据执行上述一系列操作:reserving、committing、将代码数据拷贝到paging file中committing的物理存储器。而是先根据.exe文件计算出所需代码和数据的大小,然后reserving一块地址空间,将其committing到.exe文件本身(而不是paging file)。这种把一个程序位于硬盘的一个文件映像(.exe/.dll)用作地址空间区域对应的物理存储器的做法,叫做内存映射文件。
当载入一个.exe/.dll时,OS会自动reversing/committing到这个区域,但是OS也提供了一组API,能够在程序里指定reversing/committing.
GetSystemInfo
void WINAPI GetSystemInfo(
_Out_ LPSYSTEM_INFO lpSystemInfo
);
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
};
};
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO;
GlobalMemoryStatus
void WINAPI GlobalMemoryStatus(
_Out_ LPMEMORYSTATUS lpBuffer
);
typedef struct _MEMORYSTATUS {
DWORD dwLength;
DWORD dwMemoryLoad;
SIZE_T dwTotalPhys;
SIZE_T dwAvailPhys;
SIZE_T dwTotalPageFile;
SIZE_T dwAvailPageFile;
SIZE_T dwTotalVirtual;
SIZE_T dwAvailVirtual;
} MEMORYSTATUS, *LPMEMORYSTATUS;
VirtualQuery
SIZE_T WINAPI VirtualQuery(
_In_opt_ LPCVOID lpAddress,
_Out_ PMEMORY_BASIC_INFORMATION lpBuffer,
_In_ SIZE_T dwLength
);
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
Windows提供已下三种机制操作内存:
这里讨论第一种:虚拟内存。
LPVOID WINAPI VirtualAlloc(
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
lpAddress:告诉OS我们想要哪块地址空间,由于OS会记录free的地址空间,所以大部分时间传NULL,表示自动寻找。如果传参则需要保证位于用户模式分区中,否则分配失败返回NULL
dwSize:指定区域大小
fdwAllocationType:是reserving还是committing. MEM_RESERVE表示预定
fdwProtect:指定保护属性
Reserving之后还要Committing,这样OS才会在页交换文件中调拨物理存储器给区域。
lpAddress:要调拨给哪里使用
dwSize:要调拨多少物理存储器
fdwAllocationType:传入MEM_COMMIT
对于一个200*256的电子表格:
CELLDATA CellData[200][256];
考虑到用户只会在少数几个单元格存放信息,如果全部载入内存,利用率太低。如果使用链表,又增大了读取单元格内容的难度。此时虚拟内存就是一个合适的选择。步骤如下:
有几种方法可以确定是否需要给区域中某一部分committing物理存储器:
BOOL WINAPI VirtualFree(
_In_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD dwFreeType
);
与VirtualAlloc基本相同,不再解释。
与虚拟内存相似,内存映射文件允许开发者reserving/committing一块地址空间区域。不同点在于内存映射文件的物理存储器来自磁盘上已有文件,而不是OS的paging file. 一旦把文件映射到地址空间,就可以像已经被载入内存一样对其访问。
内存映射文件主要用于以下三种情况:
当一个线程调用CreateProcess
时OS执行以下操作:
然后OS会访问.exe文件中一个特定的段,这个段列出了一些dll文件,OS再调用LoadLibrary来载入dll,与上述倒数两步相似。
所有.exe/.dll文件都映射到进程的地址空间后,OS开始执行.exe文件的启动代码,并负责paging、buffering、caching. 例如当.exe一行代码跳转到一个未载入内存的指令地址,则CPU报page fault,并将该页代码从文件映像中载入到内存。
假设应用程序的第二个实例现在运行,OS只是把包含代码和数据的虚拟内存页面映射到第二个实例的地址空间中,如果其中一个实例修改了数据页面中的一些全局变量,那么应用程序所有实例的内存都会被修改,这中灾难是通过写时复制的方式避免的。
当应用程序试图写入内存映射文件时,OS会先截获此类尝试,接着为app试图写入的内存页面分配一块新的虚拟内存,再复制页面内容,最后让app写入到刚刚分配的内存块。
考虑一个问题:如何颠倒一个2G的文件。
使用内存映射数据文件的好处是,将何时载入内存、换入换出都交给了OS处理。下面看使用内存映射数据文件的具体步骤
HANDLE WINAPI CreateFile(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
之前讨论过这个API,这里注意一下
dwDesiredAccess
必须是:GENERIC_READ
或 GENERIC_READ | GENERIC_WRITE
HANDLE WINAPI CreateFileMapping(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCTSTR lpName
);
dwMaximumSizeHigh dwMaximumSizeLow
两个参数加在一起告诉OS内存映射文件的最大大小,最多为64E。
但是有一个问题,32位进程地址空间最大只有4G,其中还包括内核等不能用的地址。如何把16E的文件映射到4G地址空间去呢,后面会介绍。
如果文件比指定的大小要小,那么CreateFileMapping
会增大文件大小,目的是保证后来把文件用作内存映射文件时,物理存储器已经准备就绪。
LPVOID WINAPI MapViewOfFile(
_In_ HANDLE hFileMappingObject,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD dwFileOffsetLow,
_In_ SIZE_T dwNumberOfBytesToMap
);
dwMaximumSizeHigh dwMaximumSizeLow
两个参数加在一起告诉OS从哪里开始映射dwNumberOfBytesToMap
指定映射多大BOOL WINAPI UnmapViewOfFile(
_In_ LPCVOID lpBaseAddress
);
有多种机制能在进程间共享数据:RPC/COM/OLE/DDE/WM_COPYDATA/剪切板/mailslot/pipe/socket,但是最底层的机制就是内存映射文件,也就是说在同一台机器上进程间共享数据,上边提到的所有机制都会用到内存映射文件。
这种数据共享机制通过让多个进程映射同一个文件映射对象的视图来实现,即进程间共享相同的物理存储页面。因此当一个进程在文件映射对象的视图中写入数据时,其他进程会在视图中立即看到变化。
【Note】OS允许同一个数据文件为后背存储器来创建多个文件映射对象,但不保证这些文件映射对象的各个视图是一致的。OS只保证同一文件映射对象的多个视图间保持一致
进程初始化时OS会在进程的地址空间中创建一个大小为1MB(可调)的默认堆。我们可以通过调用GetProcessHeap
拿到进程默认堆的句柄。
以下几种情况需要创建额外的堆:
对组件进行保护
假如程序需要处理一个链表和一个二叉树,如果在同一个堆中,链表的错误操作可能会覆盖二叉树的节点,使开发者误认为是二叉树的代码有bug,如果创建两个独立的堆,会使这种可能性小很多。
更有效的内存管理
如果链表节点的大小是8,堆节点的大小是12,那么链表释放的节点无法被堆节点使用,造成内存碎片。如果两个独立的堆,可以重复分配空间。
使内存访问局部化
链表和二叉树在同一个堆,可能导致遍历链表时频繁换页。如果分别占用同一块页,会降低换页开销。
避免线程同步的开销
后文会介绍。
快速释放
可以直接释放整个堆而不必显示释放每个内存块。
HANDLE WINAPI HeapCreate(
_In_ DWORD flOptions,
_In_ SIZE_T dwInitialSize,
_In_ SIZE_T dwMaximumSize
);
默认堆堆的访问会依次进行,使多个线程可以从同一个堆中分配和释放内存块,不会存在堆数据被破坏的危险。如果开发者能够自己保证对堆访问时的线程安全,可以在flOptions中指定HEAPNOSERIALIZE
dwInitialSize表示一开始要committing给堆的字节数
dwMaximumSize表示堆的最大大小,如果为0表示无上限可增长
LPVOID WINAPI HeapAlloc(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ SIZE_T dwBytes
);
其中dwFlags与HeapCreate
相似
LPVOID WINAPI HeapReAlloc(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ LPVOID lpMem,
_In_ SIZE_T dwBytes
);
SIZE_T WINAPI HeapSize(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ LPCVOID lpMem
);
BOOL WINAPI HeapFree(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags,
_In_ LPVOID lpMem
);
BOOL WINAPI HeapDestroy(
_In_ HANDLE hHeap
);
class CSomeClass
{
public:
void* operator new (size_t size);
void operator delete (void* p);
private:
static HANDLE s_hHeap;
static UINT s_uNumAllocsInHeap;
};
HANDLE CSomeClass::s_hHeap = NULL;
UINT CSomeClass::s_uNumAllocsInHeap = 0;
void* CSomeClass::operator new (size_t size) {
if (s_hHeap == NULL) {
s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
}
if (s_hHeap == NULL) {
return NULL;
}
void* p = HeapAlloc(s_hHeap, 0, size);
if (p != NULL) {
s_uNumAllocsInHeap ++;
}
return p;
}
void CSomeClass::operator delete (void* p) {
if (HeapFree(s_hHeap, 0, p)) {
s_uNumAllocsInHeap--;
}
if (s_uNumAllocsInHeap == 0) {
if (HeapDestroy(s_hHeap)) {
s_hHeap = NULL;
}
}
}