这章讲进程内存与操作系统物理内存之间的关系,操作系统如何管理。关键字:虚拟地址空间,reserve commit free三步。
Windows Via C/C++ 读书笔记 9
Windows Memory Architecture
每个进程都有自己的虚地址空间,32bit系统的地址空间是4GB(0x00000000~0xFFFFFFFF),64bit系统是16EB。各个进程的实际地址是操作系统分配的地址加上虚拟空间地址的映射,因为各个进程的实际地址空间被分配在不同的区域,所以各个进程间的地址空间是隔离的,不能互相访问。
Partition |
x86 32-Bit Windows |
x86 32-Bit Windows with 3 GB User-Mode |
x64 64-Bit Windows |
IA-64 64-Bit Windows |
NULL-Pointer Assignment |
0x00000000 |
0x00000000 |
0x00000000'00000000 |
0x00000000'00000000 |
0x0000FFFF |
0x0000FFFF |
0x00000000'0000FFFF |
0x00000000'0000FFFF |
|
User-Mode |
0x00010000 |
0x00010000 |
0x00000000'00010000 |
0x00000000'00010000 |
0x7FFEFFFF |
0xBFFEFFFF |
0x000007FF'FFFEFFFF |
0x000006FB'FFFEFFFF |
|
64-KB Off-Limits |
0x7FFF0000 |
0xBFFF0000 |
0x000007FF'FFFF0000 |
0x000006FB'FFFF0000 |
0x7FFFFFFF |
0xBFFFFFFF |
0x000007FF'FFFFFFFF |
0x000006FB'FFFFFFFF |
|
Kernel-Mode |
0x80000000 |
0xC0000000 |
0x00000800'00000000 |
0x000006FC'00000000 |
0xFFFFFFFF |
0xFFFFFFFF |
0xFFFFFFFF'FFFFFFFF |
|
这个区域被认为是空指针区域,即任何试图访问这块区域的操作都会产生"access violation "错误。这个机制是用来帮助程序员尽早发现错误。比如某个指针指向NULL(0x0000),当试图访问这个指针指向的内存时,操作系统能够发现这个错误,抛出一个"access violation "错误。
这是用户使用的内存地址(相对地址)。因为不同进程被隔离在了不同的内存区域,因此一个进程不会因为另一个进程访问到它的内存,造成崩溃。
通过设置boot configuration data (BCD),可以修改User-Mode的大小。
内核模式区域是由操作系统使用,用来 线程管理、内存管理、文件系统、网络支持、驱动程序加载等。这块区域会被所有的进程共享,任何试图访问这块内存的代码都会导致访问非法错误,系统会弹出一个错误对话框,并结束进程。
所有进程可以使用的内存大小为物理内存和磁盘虚拟内存的总和,它们的管理单位是页面(Page)。Page有3个状态:free,reserved,committed。操作系统负责物理内存和磁盘虚拟内存的交换。当操作系统需要物理内存时,把最近最少使用内存映射到文件,然后使用这块内存。
进程操作内存的步骤为3步:
1. 先从进程自己的虚地址空间申请(Alloca)一块内存(n个页面大小),其它申请操作不能再使用这段地址。这步操作成为Reserving。这块内存的Pages状态由free变为reserved。
2. 请求操作系统分配一块物理内存与已经reserve的内存映射。对内存进行读写操作。这一步成为commit,内存状态由reserved变为committed。
3. 释放内存。内存状态变为free。
调用函数:
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
这步操作实际上操作系统还没有给进程分配一块实际的物理内存或者磁盘虚拟内存,通过写程序debug(后面提供这个MSDN的例子程序),这个指针有地址,但是没有内存。如果程序这个时候试图访问这块内存,是会导致异常的。
注:后面讲的例子程序就是利用了这个特点,当有访问异常的时候,说明还没有给这个地址分配实际的物理内存,由异常处理函数来做Commit操作,给该地址分配一个Page。因为进程不需要给所有reserved的内存都commit一块物理内存,只给有需要的内存分配,这么做可以节省实际的物理内存。
调用函数与Reserve相同。
这一步请求操作系统为进程的地址空间分配一块实际的物理内存,供进程读写。
调用函数:
BOOL VirtualFree(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD dwFreeType
);
释放内存。
// A short program to demonstrate dynamic memory allocation using
// a structured exception handler.
#include
#include
#include
#define PAGELIMIT 80 // ask for this many pages
LPTSTR lpNxtPage; // address of the next page to ask for
DWORD dwPages = 0; // count of pages gotten so far
DWORD dwPageSize; // the page size on this computer
INT PageFaultExceptionFilter(DWORD dwCode)
{
LPVOID lpvResult;
// If the exception is not a page fault, exit.
if (dwCode != EXCEPTION_ACCESS_VIOLATION)
{
printf("Exception code = %d/n", dwCode);
return EXCEPTION_EXECUTE_HANDLER;
}
printf("Exception is a page fault/n");
// If the reserved pages are used up, exit.
if (dwPages >= PAGELIMIT)
{
printf("Exception: out of pages/n");
return EXCEPTION_EXECUTE_HANDLER;
}
// Otherwise, commit another page.
lpvResult = VirtualAlloc(
(LPVOID) lpNxtPage, // next page to commit
dwPageSize, // page size, in bytes
MEM_COMMIT, // allocate a committed page
PAGE_READWRITE); // read/write access
if (lpvResult == NULL )
{
printf("VirtualAlloc failed/n");
return EXCEPTION_EXECUTE_HANDLER;
} else {
printf ("Allocating another page./n");
}
// Increment the page count, and advance lpNxtPage to the next page.
dwPages++;
lpNxtPage += dwPageSize;
// Continue execution where the page fault occurred.
return EXCEPTION_CONTINUE_EXECUTION;
}
VOID ErrorExit(LPTSTR lpMsg)
{
printf ("Error! %s with error code of %ld/n",
lpMsg, GetLastError ());
exit (0);
}
VOID main(VOID)
{
LPVOID lpvBase; // base address of the test memory
LPTSTR lpPtr; // generic character pointer
BOOL bSuccess; // flag
DWORD i; // generic counter
SYSTEM_INFO sSysInfo; // useful information about the system
GetSystemInfo(&sSysInfo); // initialize the structure
printf ("This computer has page size %d./n", sSysInfo.dwPageSize);
dwPageSize = sSysInfo.dwPageSize;
// Reserve pages in the process's virtual address space.
lpvBase = VirtualAlloc(
NULL, // system selects address
PAGELIMIT*dwPageSize, // size of allocation
MEM_RESERVE, // allocate reserved pages
PAGE_NOACCESS); // protection = no access
if (lpvBase == NULL )
ErrorExit(TEXT("VirtualAlloc reserve failed"));
lpPtr = lpNxtPage = (LPTSTR) lpvBase;
// Use structured exception handling when accessing the pages.
// If a page fault occurs, the exception filter is executed to
// commit another page from the reserved block of pages.
for (i=0; i < PAGELIMIT*dwPageSize; i++)
{
__try
{
// Write to memory.
lpPtr[i] = 'a';
}
// If there's a page fault, commit another page and try again.
__except ( PageFaultExceptionFilter( GetExceptionCode() ) )
{
// This code is executed only if the filter function is
// unsuccessful in committing the next page.
ExitProcess( GetLastError() );
}
}
// Release the block of pages when you are finished using them.
bSuccess = VirtualFree(
lpvBase, // base address of block
0, // bytes of committed pages
MEM_RELEASE); // decommit the pages
printf ("Release %s./n", bSuccess ? "succeeded" : "failed" );
}
例子里面关键看它的三个步骤,对应我前面讲的内存管理三步:
1 Reserve
它先reserve了块内存,大小为PAGELIMIT*dwPageSize。页面大小从操作系统获取,我的系统是4960字节。
lpvBase = VirtualAlloc(
NULL, // system selects address
PAGELIMIT*dwPageSize, // size of allocation
MEM_RESERVE, // allocate reserved pages
PAGE_NOACCESS); // protection = no access
2.Commit
它的Commit操作放在了异常处理函数PageFaultExceptionFilter里面。当程序运行到给lpPtr赋值(写操作)的时候,如果这个时候还没有给这个地址做commit,那么程序会出一个异常,然后就进入PageFaultExceptionFilter里面。在这个函数里面调用VirtualAlloc函数分配一块实际的物理内存。程序继续while循环,这个时候赋值就会成功。
3.Free
这个例子建议单步调试看运行效果,就能理解这章的内容了。我刚开始看也是云里雾里,结合msdn,才把它看明白。