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

本文详细介绍了如何在Windows应用程序中使用虚拟内存,包括预定地址空间区域、调拨物理存储器、改变保护属性、撤销调拨以及地址窗口扩展等关键操作,帮助开发者更好地理解和管理内存资源。
摘要由CSDN通过智能技术生成

第十五章 在应用程序中使用虚拟内存

本章内容
15.1 预定地址空间区域
15.2 给区域调拨物理存储器
15.3 同时预定和调拨物理存储器
15.4 何时调拨物理存储器
15.5 撤销调拨物理存储以及释放区域
15.6 改变保护属性
15.7 重置物理存储器的内容
15.8 地址窗口扩展

Microsoft Windows 提供一下三种机制来对内存进行操控。
虚拟内存  最适合用来管理大型对象数组或大型数据结构
内存映射文件 最适合用来管理大型数据流(通常是文件),以及在同一机器上运行的多个进程之间的共享数据。
最适合用来管理大量的小型对象

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

15.1 预定地址空间区域

我们可以调用 VirtualAlloc函数来预定进程中的地址空间区域:
WINBASEAPI
_Ret_maybenull_ _Post_writable_byte_size_(dwSize)
LPVOID
WINAPI
VirtualAlloc(
    _In_opt_ LPVOID lpAddress,
    _In_ SIZE_T dwSize,
    _In_ DWORD flAllocationType,
    _In_ DWORD flProtect
    );

lpAddress 告知系统想要预定的地址空间中的哪一块。由于系统会记录所有闲置地址区间,通常只需要传递NULL就可以了。
通常分配方向是随机的,但是可以指定一些标志对分配方向进行一些控制( MEM_TOP_DOWN

对于大多数程序员来说,能够让系统在指定的内存地址预定区域是个不同寻常的概念,传统的概念应该是让系统帮我们寻找一块足够大的内存并分配之,然后返回这块内存的地址。但是由于现在每个进程都有自己的地址空间,因此可以要求操作系统在我们希望的内存区域预定区域。

例如想要在虚拟地址起始50MB的地方分配,传递52428800(50x1024x1024)给lpAddress 如果这个内存地址有足够大的闲置区域,则系统会把该区域预定下来。如果没有闲置的区域,或者区域不够大。则 VirtualAlloc返回NULL。另外若传递的lpAddress不在可供用户模式选择的地址空间也会直接返回NULL

因为系统是按照分配粒度来分配的。所以实际返回的地址是以lpAddress为准向下取整的(64KB分配粒度)基地址。

dwSize是我们想要预定区域的大小,以字节为单位。大小必须是页面的整数(4KB, 8KB 或16KB) 如果预定62KB 最终得到的区域大小会是64KB

flAllocationType 告知所分配的区域是否需要调拨物理存储器(这种区分是必要的,因为 VirtualAlloc也可以用于调拨物理存储器)
要预定地址空间必须传 MEM_RESERVE

如果想预定尽可能高的地址(因为要使用很长一段时间,防止引起内存碎片)传NULL给lpAddress 同时传 MEM_TOP_DOWN | MEM_RESERVE 给flAllocationType


flProtect给区域指定保护属性。区域的保护属性对于调拨给该区域的物理存储器不起任何作用。无论区域指定什么保护属性,只要还没给它调拨物理内存,试图访问区域内的任何内存地址都会引发访问违规。



看一上例子,使用 VirtualAlloc预定了一块区域但是还没有调拨物理内存,试图写入整型数据直接导致异常。

在预定区域并制定保护属性时,应该考虑在调拨物理内存时最常用的保护属性。例如: PAGE_READWRITE保护属性来调拨物理存储器,则使用 PAGE_READWRITE来预定区域。当区域和物理存储器的保护属性一致时,系统内部处理的效率会更高。
可以使用以下保护属性。
PAGE_NOACCESS, PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE, PAGE_EXECUTE_READ,或 PAGE_EXECUTE_READWRITE.
不能使用 PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY(否则 VirtualAlloc不会预定区域而直接返回NULL)
同时也不能使用 PAGE_GUARD, PAGE_NOCACHEPAGE_WRITECOMBINE(也会返回NULL)
这些都只能用来调拨物理存储器。



15.2 给区域调拨物理存储器


预定了区域以后,还需要给区域调拨物理存储器,这样才能访问其中的内存地址。系统会从页交换文件中调拨物理存储器给区域。并且物理存储器的起始地址始终都是页面大小的整数倍,整个大小也是页面大小的整数倍。

使用 VirtualAlloc 并传入 MEM_COMMIT给其第二个参数flAllocationType(预定区域的值为 MEM_RESERVE
指定保护属性通常和预定区域相同。(当然也可以指定不同的保护属性)

通过参数lpAddress(起始地址)和dwSize(存储器的大小)告知系统需要调拨多少物理内存

例如一下例子:应用程序已经在5242880处预定了一个512KB的区域。从改区域的2KB地址开始调拨6KB的物理存储器。
char * p = (char*)VirtualAlloc((PVOID)(5242880 + (2 * 1024)), 
		6 * 1024, MEM_COMMIT, PAGE_READWRITE);
根据页对齐的特性,最终系统会调拨8KB的物理存储器。 5242880到5251071(5242880 + 8KB - 1)之间。这些页面都具有PAGE_READWRITE保护属性。
同一区域中的不同页面可以具有不同的保护属性。


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

有时候需要同时预定区域并调拨物理内存,代码如下:


PVOID pvMem = VirtualAlloc(NULL, 99 * 1024,
		MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

根据页对齐原则,系统实际会调拨100KB (在x86机器上, IA-64会调拨104KB)

以上代码将可以正常执行。


windows系统还提供了大页面支持。使用一下函数:
WINBASEAPI
SIZE_T
WINAPI
GetLargePageMinimum(
    VOID
    );

如果cpu不支持大页面分配, GetLargePageMinimum会返回0. 如果要分配的页面大于该函数的返回值,就可以使用Windows大页面支持。
只要在分配VirtualAlloc并将MEM_LARGE_PAGE和fdwAllocationType按位或运算即可。
还需要满足一下条件:
1) 要分配的内存大小必须是 GetLargePageMinimum函数返回的整数倍。
2)fdwAllocationType 必须传递 MEM_RESERVE | MEM_COMMIT 再或上 MEM_LARGE_PAGE (也就是必须同时预定+调拨物理内存)
3)fdwProtect必须传递PAGE_READWRIE


Windows认为MEM_LARGE_PAGE标志得到的内存是不可换页的(unpagable),也就是必须驻留在内存中。这种方式分配的内存能得到更好的性能。
使用MEM_LARGE_PAGE调用VirtualAlloc需要调用方具有内存中锁定页面(Lock Pages In Memory)的用户权限,否则函数会失败。通常应用程序是不具有此权限了。
在计算机-》控制面板-》管理工具-》本地安全策略中打开此权限。设置如下:






例如以上将admin这个用户添加了此权限。
然后注销再登陆计算机。同时应用程序也必须以提升运行权限的方式来运行。
之后Virtual Alloc会返回所预定区域的虚拟地址。这个地址随后被保存在pvMem变量中。
如果系统无法找到足够大的空间,则VirtualAlloc会返回NULL

这种方式调拨物理内存给 VirtualAlloc函数的pvAddress指定一个地址仍然是可行的
也可以传递NULL,并把fdwAllocationType或上MEM_TOP_DOWN让系统来寻找合适的区域。

15.4 何时调拨物理存储器。

作者举了一个例子:
假设一个电子表格程序,支持200行,256列。 每个单元格需要维护一个CELLDATA的结构体数据大小是128字节。
如果一开始就申明好数据
CELLDATA CellData[200][256];

那么这个二维数据需要6553600个字节(200x256x128)

这样程序一开始就需要分配大量内存,而用户可能只会使用其中的少数几个单元格。内存使用率很低。
通常电子表格都是使用其他数据结构来实现的,比如链表。这样当某个电子表格确实存放了数据,才需要创建与之对应的CELLDATA结构。
但是同时存在一个问题,如果要访问第五行,第十列的单元格内容。这种遍历方法会很慢(事实上,可以采用二维索引表来记录对应单元格的数据地址,采用链表作为数据存储结构。这样能提升访问效率)
虚拟内存也可以提供一种折中方案。享受数组方法所带来的快速而便捷的访问,又能节省存储器

为了使用虚拟内存,需要执行以下步骤。
1)预定一块足够大的区域来容纳CELLDATA结构的整个数组,只预定根本不会消耗物理存储器。
2)当用户在某个单元格中输入数据时,首先确定CELLDATA结构在区域中的内存地址。由于这时还没有给该地址映射物理存储器,试图访问会引发内存错误。
3)给第二步中的内存地址调拨足够的物理存储器。
4)设置CELLDATA结构成员

有几个问题:
如果个某个单元格调拨过物理存储器,由于分配粒度和页面对齐等原因,实际上会给相邻的区域也调拨物理存储器。那么如何判断相邻的区域是否已经调拨了物理存储器呢?
1)总是尝试调拨物理存储器,这样如果该区域已经被调拨,系统并不会额外再给其调拨物理存储器。
2)使用 VirtualQuery来判断是否已经给CELLDATA结构调拨物理存储器。(其实这种方法会增大开销,因为VirtualQuery函数执行开销不低)
3)记录哪些页面已经调拨而哪些页面未调拨。这样可以使程序运行更快,同时避免了频繁调拨VirtualAlloc。
4)使用结构化异常处理(structured exception handling)SEH是操作系统的一项特性,可以在系统发生某种情况通知应用程序。给应用程序设置一个结构化异常处理程序,当程序试图访问未调拨物理存储器的内存时,系统会通知我们的应用程序。接着应用程序可以调拨物理存储器,并告知系统重新执行那条引发异常的指令。

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

要撤销调拨给区域的物理存储器,或者释放地址空间中的一整块区域,调用以下函数
WINBASEAPI
BOOL
WINAPI
VirtualFree(
    _Pre_notnull_ _W
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值