意外的页面处理(同样,VirtualLock =没有操作?)

时间:2011-10-24 10:30:09

标签: windows virtual-memory

今天早上我偶然发现了一些令人惊讶的页面错误,我没想到它们。是的,我可能不应该担心,但它仍然让我感到奇怪,因为在我的理解中它们不应该发生。并且,如果他们不这样做,我会更好。

应用程序(在WinXP Pro 32位下)使用VirtualAlloc(MEM_RESERVE)保留更大的部分(1GB)地址空间,然后使用VirtualAlloc(MEM_COMMIT)分配中等大块(20-50MB)的内存。这是在工人提前完成的,目的是尽可能少地停止主线程。显然,除非内存区域当前被锁定,否则您无法确保不会发生页面错误,但其中一些肯定是可以容忍的(并且是不可避免的)。令人惊讶的是每一页错误。总是

因此,假设系统在分配页面之后只是懒惰地创建页面,这在某种程度上也是有意义的(尽管文档提出了不同的东西)。很公平,我的不好 因此,显而易见的解决方法是VirtualLock / VirtualUnlock,它强制系统创建这些页面,因为在VirtualLock返回后必须存在。令人惊讶的是,仍然每一页都有错误

所以我写了一个小测试程序,按顺序执行上述所有步骤,在每个步骤之间休息5秒,以排除其他代码中的错误。结果是:

  • MEM_RESERVE 1GB --->成功,零CPU,零时间,没有任何反应
  • MEM_COMMIT 1 GB --->成功,零CPU,零时间,工作集增加2MB,512页错误(每页用户空间分配8个字节的元数据)
  • for(... += 128kB) { VirtualLock(128kB); VirtualUnlock(128kB); } --->成功,零CPU,零时间,没有任何反应
  • for(... += 4096) *addr = 0; ---> 262144页错误,约0.25秒(内核时间约95%)。 Process Explorer
  • 中的“工作集”和“物理”增加1GB
  • VirtualFree --->零CPU,零时间,“工作集”和“物理”立即变为* poof *。

我的期望是,由于每个页面都被锁定一次,因此它必须至少在之后存在。当超出配额时,它当然可以移入和移出WS(只要有足够的RAM可用,只需更改一个引用)。然而,执行时间,工作集和物理内存指标似乎都不支持这一点。相反,正如它所看到的那样,每个单个访问页面都是在故障时创建的,即使它之前已被锁定。当然我可以在工作线程中手动触摸每一页,但是必须有更清洁的方法吗?

我是否对VirtualLock 应该做什么做出了错误的假设,或者我对虚拟内存的理解不正确?有关如何以“干净,合法,有效”的方式告诉操作系统我想要记忆的任何想法,我会想要真正的吗?

更新
在回应Harry Johnston的建议时,我尝试了一种有问题的方法,实际上在一千兆字节的内存上调用VirtualLock。为了成功,您必须首先相应地设置进程的工作集大小,因为默认配额是200k / 1M,这意味着VirtualLock不能锁定大于200k的区域(或者说,它不能锁定超过200k的区域200k alltogether ,这是减去I / O锁定的内容或其他原因。

将最小工作集大小设置为1GB且最大为2GB后,所有页面错误都会在调用VirtualAlloc(MEM_COMMIT)时发生。 Process Explorer中的“虚拟大小”立即跳起1GB。到目前为止,它看起来非常非常好 然而,仔细观察,“物理”仍然保持原样,实际记忆实际上只在您触摸它的那一刻使用。

VirtualLock仍然是无操作(故障),但提高的最小工作集大小更接近目标。

然而,篡改WS大小有两个问题。首先,你通常不打算在一个进程中拥有一个千兆字节的最小工作集,因为操作系统会努力保持锁定的内存量。这在我的情况下是可以接受的(它实际上或多或少只是我要求的) 更大的问题是SetProcessWorkingSetSize需要PROCESS_SET_QUOTA访问权限,这不是“管理员”的问题,但是当您以受限用户身份运行程序时(由于一个充分的理由)它会失败,并且它会触发“允许可能有害的程序?”一些着名的俄罗斯防病毒软件的警报(没有很好的理由,但唉,你不能把它关掉)。

3 个答案:

答案 0 :(得分:3)

从技术上讲,VirtualLock是一个提示,因此操作系统可以忽略它。它由NtLockVirtualMemory系统调用支持,它在Reactos / Wine上实现为无操作,但是Windows会使用实际工作(MiLockVadRange)支持系统调用。

VirtualLock无法保证成功。调用此函数需要SE_LOCK_MEMORY_PRIVILEGE才能工作,并且地址必须满足安全性和配额限制。此外,在VirtualUnlock之后,内核不再需要将页面保留在内存中,因此页面错误是一个有效的操作。

正如雷蒙德·陈指出的那样,当你解锁内存时,它可以正式释放页面。这意味着下一页上的下一个VirtualLock可能会再次获得同一页面,因此当您触摸原始页面时,您仍会遇到页面错误。

答案 1 :(得分:3)

  

VirtualLock仍为无操作(故障)

我试图重现这一点,但它可以像人们期望的那样工作。运行此帖子底部显示的示例代码:

  • 启动应用程序(523页错误)
  • 调整工作集大小(21页错误)
  • VirtualAlloc MEM_COMMIT 2500 MB内存(2页错误)
  • VirtualLock所有这些(约641,250页错误
  • 在无限循环中执行对所有RAM的写入(零页面错误)

这一切都与预期的一样。 2500 MB的RAM是640,000页。数字加起来。此外,就操作系统范围内的RAM计数器而言,提交费用在VirtualAlloc上升,而物理内存使用量则增加到VirtualLock

所以VirtualLock绝对是而不是我的Win7 x64机器上的无操作。如果我不这样做,页面错误,如预期的那样,转移到我开始写入RAM的位置。他们的总数仍然超过640,000。此外,第一次写入内存需要更长的时间。


  

相反,正如它所看到的那样,每个被访问的页面都是在出错时创建的,即使它之前被锁定过。

这不是错误的。无法保证访问锁定然后解锁的页面不会出错。你锁定它,它被映射到物理RAM。你可以解锁它,并且可以立即取消映射,从而使故障成为可能。你可能希望它会保持映射,但不能保证......

对于它的价值,在我的系统上有几GB的物理RAM免费,它按照你希望的方式工作:即使我立即跟踪我的VirtualLock VirtualUnlock并设置最小工作集大小回到小的状态,不会发生进一步的页面错误。

这就是我的所作所为。我使用和不使用代码来运行测试程序(下面),该代码立即解锁内存并恢复合理的最小工作集大小,然后强制物理RAM在每个场景中用完。在强制低RAM之前,程序都没有任何页面错误。在强制低RAM之后,保持内存锁定的程序保留了其庞大的工作集并且没有进一步的页面错误。然而,解锁内存的程序开始出现页面错误。

如果你先挂起进程,这是最容易观察到的,因为否则常量内存写入会将所有内容保存在工作集中,即使内存未锁定(显然是理想的事情)。但暂停进程,强制低RAM,并观察工作集仅缩小已解锁RAM的程序。恢复该过程,并目睹大量页面错误。

换句话说,至少在Win7 x64中,一切都按预期工作,使用下面提供的代码。


  

然而,篡改WS大小有两个问题。首先,你通常不打算在一个过程中拥有一个千兆字节的最小工作集

嗯......如果你想VirtualLock,你已经在篡改它了。 SetProcessWorkingSetSize唯一能做的就是允许你篡改它。它本身不会降低性能;它的作用是VirtualLock - 但前提是系统实际上在物理RAM上运行不足。


这是完整的程序:

#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <iostream>

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    SIZE_T chunkSize = 2500LL * 1024LL * 1024LL; // 2,626,568,192 = 640,000 pages
    int sleep = 5000;

    Sleep(sleep);

    cout << "Setting working set size... ";
    if (!SetProcessWorkingSetSize(GetCurrentProcess(), chunkSize + 5001001L, chunkSize * 2))
        return -1;
    cout << "done" << endl;

    Sleep(sleep);

    cout << "VirtualAlloc... ";
    UINT8* data = (UINT8*) VirtualAlloc(NULL, chunkSize, MEM_COMMIT, PAGE_READWRITE);
    if (data == NULL)
        return -2;
    cout << "done" << endl;

    Sleep(sleep);

    cout << "VirtualLock... ";
    if (VirtualLock(data, chunkSize) == 0)
        return -3;
    //if (VirtualUnlock(data, chunkSize) == 0) // enable or disable to experiment with unlocks
    //    return -3;
    //if (!SetProcessWorkingSetSize(GetCurrentProcess(), 5001001L, chunkSize * 2))
    //    return -1;
    cout << "done" << endl;

    Sleep(sleep);

    cout << "Writes to the memory... ";
    while (true)
    {
        int* end = (int*) (data + chunkSize);
        for (int* d = (int*) data; d < end; d++)
            *d = (int) d;
        cout << "done ";
    }

    return 0;
}

请注意,此代码会在VirtualLock之后使线程进入休眠状态。根据{{​​3}},操作系统可以自由地将其全部从物理RAM中分页,直到线程再次唤醒。另请注意2007 post by Raymond Chen,表示无论是否所有线程都处于休眠状态,该内存都不会被分页。在我的系统上,它们肯定保留在物理RAM中,而唯一的线程正在休眠。我怀疑雷蒙德的建议是在2007年应用的,但在Win7中已不再适用。

答案 2 :(得分:0)

我没有足够的声誉来发表评论,所以我必须将其作为答案添加。

  

请注意,此代码会在VirtualLock之后将线程置于休眠状态。根据Raymond Chen 2007年的一篇文章,操作系统可以自由地将它全部用物理RAM分页,直到线程再次醒来[...]我怀疑雷蒙德的建议在2007年应用,但不再适用于Win7的。

romkyns在2014年说有been confirmed by Raymond Chen。也就是说,当您使用VirtualLock锁定内存时,即使所有线程都被阻止,它也会保持锁定状态。他还说页面保持锁定的事实可能只是一个实现细节,而不是契约。

情况可能并非如此,因为根据msdn,它是契约性的

  

进程已锁定的页面保留在物理内存中,直到进程解锁或终止。保证这些页面在锁定时不会写入页面文件。