WINDOWS核心编程--虚拟内存

原创 2007年10月11日 15:25:00

WINDOWS核心编程--虚拟内存

windows内存管理

每个进程都被赋予它自己的虚拟地址空间。对于32位进程来说,这个地址空间是4GB,因为32位指针可以拥有从0x00000000至0xFFFFFFFF之间的任何一个值。这是虚拟地址空间,不是物理地址空间。该地址空间只是内存地址的一个范围。在你能够成功地访问数据而不会出现违规访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。

Windows提供了3种进行内存管理的方法,它们是:
1.虚拟内存,最适合用来管理大型对象或结构数组。
2.内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
3.内存堆栈,最适合用来管理大量的小对象。

先看虚拟内存,它涉及到的分页机制是其它内存分配方式的基础。

原理:

操作系统能够使得磁盘空间看上去就像内存一样。磁盘上的文件通常称为页文件,它包含了可供所有进程使用的虚拟内存。操作系统负责内存和页文件之间的转换,从应用程序的角度来看,页文件透明地增加了应用程序能够使用的RAM(即内存)的数量。如果计算机拥有64MB的RAM,同时在硬盘上有一个100MB的页文件,那么运行的应用程序就认为计算机总共拥有164M B的RAM。

操作系统与CPU相协调,共同将RAM的各个部分保存到页文件中,当运行的应用程序需要时,再将页文件的各个部分重新加载到 RAM。。如果没有页文件,那么系统就认为只有较少的RAM可供应用程序使用。物理存储器被视为存储在磁盘驱动器(通常是硬盘驱动器)上的页文件中的数据。这样,当一个应用程序通过调用VirtualAlloc函数,将物理存储器提交给地址空间的一个区域时,地址空间实际上是从硬盘上的一个文件中进行分配的。系统的页文件的大小是确定有多少物理存储器可供应用程序使用时应该考虑的最重要的因素,RAM的容量则影响非常小。

当你的进程中的一个线程试图访问进程的地址空间中的一个数据块时,将会发生两种情况之一。在第一种情况中,线程试图访问的数据是在RAM中。在这种情况下,CPU将数据的虚拟内存地址映射到内存的物理地址中,然后执行需要的访问。在第二种情况中,线程试图访问的数据不在RAM中,而是存放在页文件中的某个地方。这时,试图访问就称为页面失效,CPU将把试图进行的访问通知操作系统。这时操作系统就寻找RAM中的一个内存空页。如果找不到空页,系统必须释放一个空页。如果一个页面尚未被修改,系统就可以释放该页面。但是,如果系统需要释放一个已经修改的页面,那么它必须首先将该页面从RAM拷贝到页交换文件中,然后系统进入该页文件,找出需要访问的数据块,并将数据加载到空闲的内存页面。然后,操作系统更新它的用于指明数据的虚拟内存地址现在已经映射到RAM中的相应的物理存储器地址中的表。这时CPU重新运行生成初始页面失效的指令,但是这次CPU能够将虚拟内存地址映射到一个物理RAM地址,并访问该数据块。

关键步骤:

当进程被创建并被赋予它的地址空间时,该可用地址空间的主体是空闲的,即未分配的。若要使用该地址空间的各个部分,必须先通过调用VirtualAlloc函数来分配它里边的各个区域。对一个地址空间的区域进行分配的操作称为保留(reserving)。
PVOID VirtualAlloc(PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect)
fdwAllocationType告诉系统你想保留一个区域还是提交物理存储器(MEM_RESERVE表示保留)

若要使用已保留的地址空间区域,必须分配物理存储器,然后将该物理存储器映射到已保留的地址空间区域。这个过程称为提交物理存储器。物理存储器总是以页面的形式来提交的。若要将物理存储器提交给一个已保留的地址空间区域,也要调用VirtualAlloc函数,不过这次为fdwAllocationType参数传递的是MEM_COMMIT标志,而不是MEM_RESERVE标志。

使用完成后释放物理存储器和系统页面资源时则使用VirtualFree:
BOOL VirtualFree(LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);
fdwFreeType参数传递MEM_RELEASE,即释放某区域。释放对应的是之前的保留(reserving)操作,即应用程序不再访问物理存储器,而且不可能只释放保留区域中的一块,一旦调用释放即为释放对应的全部保留区域.
fdwFreeType参数传递MEM_DECOMMIT,为回收该区域的物理存储器.对应的是之前的提交物理存储器的操作.这样当回收物理存储器后,即释放物理内存,可以供其它进程使用.回收物理存储器以页面为单位,回收后若对其进行访问会引发访问违规。

例子:

假设想实现一个电子表格应用程序,这个电子表格为200行x256列。每一个单元格用一个CELLDATA结构来描述单元格的内容,大小为128k。若是直接以CELLDATA CellData[200][256]数组来表示是最直接的了,但是这个二维矩阵将需要6553600(200x256x128)个字节的物理存储器。对于电子表格来说,如果直接用页文件来分配物理存储器,那么这是个不小的数目了,尤其是考虑到大多数用户只是将信息放入少数的单元格中,而大部分单元格却空闲不用,因此显得有些浪费。内存的利用率非常低。

虚拟内存为我们提供了一种兼顾预先声明二维矩阵和实现链接表的两全其美的方法。运用虚拟内存,既可以使用已声明的矩阵技术进行快速而方便的访问,又可以利用链接表技术大大节省内存的使用量。

使用方式:
1.保留一个足够大的地址空间区域,用来存放 CELLDATA结构的整个数组。保留一个根本不使用任何物理存储器的区域。
2.当用户将数据输入一个单元格时,找出 CELLDATA结构应该进入的保留区域中的内存地址。当然,这时尚未有任何物理存储器被映射到该地址,因此,访问该地址的内存的任何企图都会引发访问违规。
3.就CELLDATA结构来说,将足够的物理存储器提交给第二步中找到的内存地址。
4.开始访问单元格数据。

虚拟内存技术非常出色,因为只有在用户将数据输入电子表格的单元格时,才会提交物理存储器。由于电子表格中的大多数单元格是空的,因此大部分保留区域没有提交给它的物理存储器。

第一个问题是:如何确定是否要将物理存储器提交给区域的一个部分?这里有四种方法:

1.始终设法进行物理存储器的提交。每次调用VirtualAlloc函数的时候,不要查看物理存储器是否已经映射到地址空间区域的一个部分,而是让你的程序设法进行内存的提交。系统首先查看内存是否已经被提交,如果已经提交,那么就不要提交更多的物理存储器。这种方法最容易操作,但是它的缺点是每次改变CELLDATA结构时要多进行一次函数的调用,这会使程序运行得比较慢。

2.(使用VirtualQuery函数)确定物理存储器是否已经提交给包含CELLDATA结构的地址空间。如果已经提交了,那么就不要进行任何别的操作。如果尚未提交,则可以调用VirtualAlloc函数以便提交内存。这种方法实际上比第一种方法差,它既会增加代码的长度,又会降低程序运行的速度(因为增加了对VirtualAlloc函数的调用)。

3.保留一个关于哪些页面已经提交和哪些页面尚未提交的记录。这样做可以使你的应用程序运行得更快,因为不必调用VirtualAlloc函数,你的代码能够比系统更快地确定内存是否已经被提交。它的缺点是,必须不断跟踪页面提交的信息,这可能非常简单,也可能非常困难,要根据你的情况而定。

4.使用结构化异常处理(SEH)方法,这是最好的方法。SEH是一个操作系统特性,它使系统能够在发生某种情况时将此情况通知你的应用程序。实际上可以创建一个带有异常处理程序的应用程序,然后,每当试图访问未提交的内存时,系统就将这个问题通知应用程序。然后你的应用程序便进行内存的提交,并告诉系统重新运行导致异常条件的指令。这时对内存的访问就能成功地进行了,程序将继续运行,仿佛从未发生过问题一样。这种方法是优点最多的方法,因为需要做的工作最少.

注意:系统总是按页面的分配粒度来提交物理存储器的。因此,当试图为单个CELLDATA结构提交物理存储器时(像上面的第二步那样),系统实际上提交的是内存的一个完整的页面。这并不像它听起来那样十分浪费:为单个CELLDATA结构提交物理存储器的结果是,也要为附近的其他CELLDATA结构提交内存。如果这时用户将数据输入邻近的单元格(这是经常出现的情况),就不需要提交更多的物理存储器。


第二个问题是:何时回收内存?
在实践中,知道何时回收内存是非常困难的。让我们再以电子表格为例。如果你的应用程序是在x86计算机上运行,每个内存页面是4KB ,它可以存放32个(4096/128)CELLDATA结构。如果用户删除了单元格CellData[0][1]的内容,那么只要单元格CellData[0][0]至CellData[0][31]也不被使用,就可以回收它的内存页面。那么怎么能够知道这个情况呢?可以用下面3种方法来解决这个问题。

最容易的方法是设计一个CELLDATA结构,它的大小只有一个页面。这时,由于始终都是每个页面使用一个结构,因此当不再需要该结构中的数据时,就可以回收该页面的物理存储器。即使你的数据结构是x86 CPU上的8KB或12KB页面的倍数(通常这是非常大的数据结构),回收内存仍然是非常容易的。当然,如果要使用这种方法,必须定义你的数据结构,使之符合你针对的CPU的页面大小而不是我们通常编写程序所用的结构。

更为实用的方法是保留一个正在使用的结构的记录。为了节省内存,可以使用一个位图。这样,如果有一个100个结构的数组,你也可以维护一个100位的数组。开始时,所有的位均设置为0,表示这些结构都没有使用。当使用这些结构时,可以将对应的位设置为1。然后,每当不需要某个结构,并将它的位重新改为0时,你可以检查属于同一个内存页面的相邻结构的位。如果没有相邻的结构正在使用,就可以回收该页面。

最后一个方法是实现一个无用单元收集函数。这个方案依赖于这样一种情况,即当物理存储器初次提交时,系统将一个页面中的所有字节设置为0。若要使用该方案,首先必须在你的结构中设置一个BOOL(也许称为fInUse)。然后,每次你将一个结构放入已提交的内存中,必须确保该fInUse被置于TRUE。当你的应用程序运行时,必须定期调用无用单元收集函数。该函数应该遍历所有潜在的数据结构。对于每个数据结构,该函数首先要确定是否已经为该结构提交内存。如果已经提交,该函数将检查fInUse成员,以确定它是否是0。如果该值是0,则表示该结构没有被使用。如果该值是T R U E,则表示该结构正在使用。当无用单元函数检查了属于既定页面的所有结构后,如果所有结构都没有被使用,它将调用VirtualFree函数,回收该内存。


例子代码:
说明:一个数组,每个元素大小2k。保留区域-〉提交内存。VirtualQuery判断当前区域的状态(是否保留或提交).结构的第一个字节说明是否被使用,便于回收内存.

typedef struct {
   BOOL fInUse;
   BYTE bOtherData[2048 - sizeof(BOOL)];
} SOMEDATA, *PSOMEDATA;

//保留操作
PSOMEDATA g_pSomeData = (PSOMEDATA) VirtualAlloc(NULL, 200* sizeof(SOMEDATA), MEM_RESERVE, PAGE_READWRITE);

//提交物理内存给某个区域
int uIndex=15;
VirtualAlloc(&g_pSomeData[uIndex], sizeof(SOMEDATA), MEM_COMMIT, PAGE_READWRITE);
g_pSomeData[uIndex].fInUse = TRUE;

//回收页面。查看页面内的结构的第一字节,若是均没有使用则回收
//参数分别是:数组基址,数组大小,元素结构大小
VOID GarbageCollect(PVOID pvBase, DWORD dwNum, DWORD dwStructSize) {

   static DWORD s_uPageSize = 0;

   if (s_uPageSize == 0) {
      // 得到当前的系统的页面大小
      SYSTEM_INFO si;
      GetSystemInfo(&si);
      s_uPageSize = si.dwPageSize;
   }

   UINT uMaxPages = dwNum * dwStructSize / g_uPageSize;
   for (UINT uPage = 0; uPage < uMaxPages; uPage++) {
      BOOL fAnyAllocsInThisPage = FALSE;
      UINT uIndex     = uPage  * g_uPageSize / dwStructSize;
      UINT uIndexLast = uIndex + g_uPageSize / dwStructSize;

      for (; uIndex < uIndexLast; uIndex++) {
         MEMORY_BASIC_INFORMATION mbi;
         VirtualQuery(&g_pSomeData[uIndex], &mbi, sizeof(mbi));
         fAnyAllocsInThisPage = ((mbi.State == MEM_COMMIT) &&   //该区域是否已提交和该区域第一字节是否为true
            * (PBOOL) ((PBYTE) pvBase + dwStructSize * uIndex));

         // Stop checking this page, we know we can't decommit it.
         if (fAnyAllocsInThisPage) break;
      }

      if (!fAnyAllocsInThisPage) {
         // No allocated structures in this page; decommit it.
         VirtualFree(&g_pSomeData[uIndexLast - 1], dwStructSize, MEM_DECOMMIT);  //回收内存
      }
   }

《Windows核心编程》读书笔记十五 在应用程序中使用虚拟内存

第十五章 在应用程序中使用虚拟内存 本章内容 15.1 预定地址空间区域 15.2 给区域调拨物理存储器 15.3 同时预定和调拨物理存储器 15.4 何时调拨物理存储器 15.5 撤销调拨物理存储以...
  • sesiria
  • sesiria
  • 2017年11月15日 15:23
  • 93

windows核心编程-虚拟内存

Microsoft Windows 提供了三种机制来对内存进行操作。 1、堆-----------最适合用来管理大量的小型对象。 2、虚拟内存-----最适合用来管理大型对象数组或大型结构数组。 ...
  • windows_nt
  • windows_nt
  • 2013年07月09日 23:06
  • 1254

《Windows核心编程》——四 进程

前言     一般将进程定义为一个正在运行的程序的一个实例,它由两部分组成:     ①一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地方     ②一个地址空间,其中...
  • andylauhuang2012
  • andylauhuang2012
  • 2015年01月05日 17:11
  • 434

《Windows核心编程》读书笔记

这篇笔记是我在读《Windows核心编程》第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理...
  • u010984552
  • u010984552
  • 2016年07月05日 19:11
  • 1262

《Windows核心编程》之“Windows挂钩”(一)

《Windows核心编程》一书中介绍了一种针对带窗口的Windows应用程序的“DLL注入”的方法——Windows Hook(窗口挂钩)。本系列文章将探讨这种技术的原理和分享我的实验心得。...
  • Sagittarius_Warrior
  • Sagittarius_Warrior
  • 2016年08月11日 15:27
  • 802

Windows核心编程 第五章 作业(上)

Windows核心编程 第五章 作业(上)
  • u013761036
  • u013761036
  • 2016年09月04日 20:31
  • 368

《windows核心编程系列》十八谈谈windows钩子

windows应用程序是基于消息驱动的。各种应用程序对各种消息作出响应从而实现各种功能。       windows钩子是windows消息处理机制的一个监视点,通过安装钩子可以达到监视指定窗口某种类...
  • fanhenghui
  • fanhenghui
  • 2017年01月06日 14:54
  • 700

Windows核心编程笔记(二十) 窗口与消息

线程的消息队列 (1)Windows用户对象(User Object)   ①类型:图标、光标、窗口类、菜单、加速键表等   ②当一个线程创建某个对象时,则该对象归这个线程的进程所有...
  • wangpengk7788
  • wangpengk7788
  • 2017年02月13日 19:50
  • 244

Windows核心编程 第四章 进程(下)

Windows核心编程 第四章 进程(下)
  • u013761036
  • u013761036
  • 2016年08月28日 18:33
  • 451

windows 核心编程之在应用程序中使用虚拟内存

Microsoft Windows 提供了以下三种机制来对内存进行操控: 虚拟内存 最适合用来管理大型对象数组或大型结构数组 内存映射文件 最适合用来管理大型数据流(通常是文件),以及在同一台机...
  • zhuimengfuyun
  • zhuimengfuyun
  • 2014年06月05日 17:30
  • 1066
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:WINDOWS核心编程--虚拟内存
举报原因:
原因补充:

(最多只允许输入30个字)