来自Windows核心编程 – 第十五章

本章主要讲述内容是应用程序对虚拟内存的操作。

MS Windows 提供了以下三种机制来对内存进行操控:

虚拟内存: 最适合用来管理大型对象数组 或 大型结构数组

内存映射文件: 最适合用来管理大型数据流(通常是文件),以及在同一机器上运行的多个进程之间共享数据。

堆: 最适合用来管理大量的小型对象。

在本章会讨论虚拟内存这种方式。

在第十七章会讨论 内存映射文件这种方式

在第十八章会对 堆 的操控进行讨论

Windows 提供了一些用来操控虚拟内存的函数,我们可以通过这些函数直接预订地址空间区域,给区域调拨(来自页交换文件的)物理存储器,以及根据自己的需要来设置页面的保护属性。

 

15.1 预定地址空间区域

我们可以调用 VirtualAlloc 函数来预订进程中的地址空间区域:

四个参数:

第一个参数 pvAddress 是内存地址,用来告诉系统我们想要预订地址空间中的哪一块。

可以给参数传递NULL,表示系统自动找一块闲置区域即可。

计算的单位是字节,所以如果我们想从第50M的地方分配区域,就要传递(20*1024*1024)给这个参数。

如果这个地址有一块足够大的闲置区域满足我们的需求,那么就会预定我们需要的区域,然后返回。如果没有闲置区域,或者这个区域不够大,系统则无法满足请求,会返回NULL。要注意的是,传递过去的参数地址必须在 用户模式分区 中,否则也会失败并返回FALSE.

要注意的是,系统都是按照分配粒度(Win下都是 64KB)的整数倍分配区域的,所以如果我们想在进程地址空间中起始地址为(300 * 65536 + 8192)的地方预定区域,那么系统就会把该地址向下取整到64KB的整数倍,即(300*65536),然后再从取整后的地址开始预定区域。

如果VirtualAlloc能够满足我们的要求,那么它会预定一块区域并返回区域的基地址。如果我们给函数指定了 pvAddress 参数,那么返回值就是 pvAddress 这个参数的值向下取整到 64KB的整数倍。

 

第二个参数 dwSize  用来指定我们想要预订的区域大小,以字节为单位。

由于系统始终都根据CPU页面大小的整数倍来预订区域,因此如果我们在页面大小为 4KB 的机器上预订 62 KB大小的区域的话,最终我们得到的区域大小会是 64KB .

 

第三个参数 fdwAllocationType 用来告诉系统我们是想预订区域 还是 调拨物理存储器。

因为 VirtualAlloc 函数也可以用来调拨物理存储器。

如果我们是想预订地址空间区域,就必须传递 MEM_RESERVE 作为fdwAllocationType 参数的值。

如果我们预定的区域准备用很长一段时间,那么我们希望系统从尽可能高的内存地址来预订区域。这样就可以防止在进程地址空间的中间预订区域,从而避免可能会引起的内存碎片。

如果想让系统从尽可能搞的内存地址来预订区域,那么我们应该给第一个参数 pvAddress 传递NULL 值,同时将 第三个参数 fdwAllocationType 参数的值 设置为 MEM_TOP_DOWN | MEM_RESERVE.

 

最后一个参数 fdwProtect 是设置区域的保护属性,区域的保护属性对调拨给该区域的物理存储器不起任何作用。

无论为区域指定什么保护属性,只要没有给它调拨物理存储器,就试图访问区域内任何内存地址都会引发违规访问。

要注意的是,不能用 PAGE_WRITECONY 、 也不能用PAGE_EXECUTE_WRITECOPY 属性。如果我们这样做,那么VirtualAlloc 将不会预订区域,而回返回NULL。

 

15.2 给区域调拨物理存储器

在预订了区域之后,我们还要给区域调拨物理存储器,这样才能访问其中的内存地址。

系统会页交换文件中来调拨物理存储器给区域

在调拨物理存储器时,起始地址始终都是页面大小的整数倍,整个大小也是页面大小的整数倍。

调拨物理存储器,我们需要再次调用 VirtualAlloc 函数。

这次我们要给第二个参数 fdwAllocationType 传入 MEM_COMMIT .

第四个参数保护属性,一般情况下我们会和预订的时候相同,也可以指定一个完全不同的保护属性。

同样的我们需要告诉 VirtualAlloc 要给XX地址开始调拨多少物理存储器。

通过传递 pvAddress 和 dwSize 来指定。

值得注意的是我们不需要一次性给整个区域都调拨物理存储器。

 

 

15.3 同时预定和调拨物理存储器

可以同时预定和调拨物理存储器。

给第三个参数传递 MEM_RESERVE | MEM_COMMIT 即可。

如果系统找不到足够大的地址空间 或 无法调拨物理存储器,那么VirtualAlloc 会返回NULL

同样的我们可以给第一个参数传递NULL,并给第三个参数传递

MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN

来获得高内存地址。

 

15.4 何时调拨物理存储器

需要的时候调拨。

比如:Excel表格…很多空间都是无用的。所以我们可以只预定空间,使用的时候在调拨物理存储器。

 

15.5 撤销调拨物理存储器及释放区域

要撤销调拨给区域的物理存储器,或者是释放地址空间中的一块区域,可以调用 VirtualFree 函数:

要注意的是,pvAddress 参数必须是区域的基地址。即VirtualAlloc函数的返回值。

给定一个地址,系统就可以知道位于该地址处的区域的大小,由于这个原因,我么可以给dwSize参数传0,实际上,我们也必须给dwSize参数传递 0 ,否则VirtualFree会失败。

第三个参数我们必须传递 MEM_RELEASE ,来告诉系统撤销调拨给该区域的所有物理存储器,并释放区域。

如果我们只想撤销调拨给区域的一部分物理储存区,又不像释放整个区域:

那么就将 我们要撤销的第一个页面地址 传递给 pvAddress 参数

将我们想要释放的物理存储器的大小给 dwSize参数

给 fdwFreeType参数 传递 MEM_DECOMMIT

要注意的是,和调拨一样,撤销也是基于页面粒度的。

 

15.6 改变保护属性

调用 VirtualProtect 函数可以用来改变一个内存页面的保护属性:

四个参数:

pvAddress 指向内存的基地址

dwSize 表示要改变保护属性的区域的大小,单位是字节

flNewProtect  是新的保护属性,不能是 *_WRITE_COPY

pflOldProtect 是一个 DWORD 的地址,VirtualProtect 会在其中返回原来的保护属性

要注意的是,保护属性是与物理存储页相关的,不能单独给页面设置保护属性。

 

15.7 重置物理存储器的内容

在前面章节(十三)我们提到了,页交换文件。

阐述了系统可能会需要释放一些内存用以载入新的页面,这时如果内存中的页面数据有修改,则需要将数据重新写回页面,然后再释放内存中间,然后再载入新页面。

但是我们提到过,这个操作比较慢,会影响性能。但是对于大多数应用程序来说,我们都希望把修改后的页面保存到页交换文件中。

也有部分应用程序只需要在一小段时间内使用存储器,可能之后也不需要保留存储器中的内容。为了提高性能,应用程序可以告诉系统不要在也交换文件中保存指定的存储页。这种情况下,如果系统决定讲这个内存页挪作他用,也不会将页面的内容保存到页交换文件中,这样就提高了性能。

为了重置存储器,我们还是调用 VirtualAlloc 函数,并在第三个参数传入 MEM_RESET标志。

此时VirtualAlloc函数内部执行如下:如果被引用到的页面在页交换文件中,系统会直接抛弃这些页面。下次应用程序再访问存储器时,会使用全新的、全部清零的内存页。如果重置的页面在内存中,那么系统会将它们标记为未修改过。这样一来系统就不会把他们写到页交换文件中了。

要注意的是,即使该内存页没有清零,我们也不应该再使用它了。

对函数地址取整要注意的是,因为清零是很危险的事情,所以如果我们像这样调用函数的话,是会直接失败的:

讲解下上面的代码:大致分为三步
1. 预定空间,调拨物理存储器,1KB

2. 赋值

3. 告诉系统最前面4字节不再使用,可以被重置。

但是在传递了 MEM_RESET的时候,函数会把基地址 向上 取整到整数倍,其目的是确保基地址之前的同一页还有其他重要数据的情况下不会被抛弃。同样的页面大小 向下 取整,确保后面的数据不会被重置。这样的话前面的代码就是重置 0 个页面了…自然就是错误了。

返回NULL、GetLastError 返回错误码 ERROR_INVALID_ADDRESS …

然后要记住的是 MEM_RESET 只能单独使用,不能和其他标志组合起来,否则会一直失败并返回NULL.

最后:虽然我们调用VirtualAlloc 函数要传递一个有效的保护属性值,但是但是并没有用到它。

 

15.8 地址窗口扩展 – 略…实在是没看懂

 

【Windows核心编程】在应用程序中使用虚拟内存
Tagged on:
0 0 vote
Article Rating
订阅
提醒
0 评论
Inline Feedbacks
View all comments