今天早上,我偶然发现了一个我没想到会发生的令人惊讶的页面错误。 是的,我可能不应该担心,但它仍然令我感到奇怪,因为根据我的理解,它们不应该发生。 而且,如果他们没有,我会更好。
该应用程序(在WinXP Pro 32位下)使用VirtualAlloc(MEM_RESERVE)
)保留更大的地址空间(1GB),稍后使用VirtualAlloc(MEM_RESERVE)
分配适当大的内存块VirtualAlloc(MEM_COMMIT)
。 这是在工作人员提前完成的,意图是尽可能less地拖动主线。 显然,除非内存区域当前被locking,否则不能确保没有页面错误发生,但其中的一些肯定是可以忍受的(并且是不可避免的)。 令人惊讶的是每一个页面都有错误。 总是。
因此,假定系统只是在分配它们之后懒洋洋地创build页面,这在某种程度上也是有意义的(尽pipe文档提供了不同的东西)。 够公平的,我的不好。
因此,显而易见的解决方法是VirtualLock
/ VirtualUnlock
,它强制系统创build这些页面,因为它们必须在VirtualLock
返回后存在。 令人惊讶的是, 仍然每一个页面错误 。
于是我写了一个小testing程序,按顺序完成了上面所有的步骤,每个程序间rest5秒钟,以排除其他代码中的错误。 结果是:
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 *。 我的期望是,由于每一页都被locking了一次,所以至less在这之后它必须物理存在。 当超过配额时,它当然仍然可以被移入和移出WS(只要有足够的RAM可用,就改变一个参考)。 然而,执行时间,工作集和物理内存度量都不支持这一点。 相反,就像它看起来一样,每个被访问的页面都是在发生错误时创build的,即使它以前被locking了。 当然,我可以在工作线程中手动触摸每个页面,但是也必须有一个更清洁的方法呢?
我对虚拟VirtualLock
应该做什么或者我对虚拟内存没有正确理解? 任何关于如何以“干净,合法,工作”的方式告诉操作系统我想要记忆的想法,我真的想要它?
更新:
为了回应Harry Johnston的build议,我尝试了在千兆字节的内存上实际调用VirtualLock
问题。 为了成功,您必须首先设置进程的工作集大小,因为默认配额是200k / 1M,这意味着VirtualLock
不可能locking大于200k的区域(或者更确切地说,它不能locking超过200k的区域,这是减去已经被locking的I / O或其他原因)。
设置最小工作集大小为1GB,最大为2GB后, VirtualAlloc(MEM_COMMIT)
被调用时,所有的页面错误都会发生。 Process Explorer中的“虚拟大小”即时跳转1GB。 到目前为止,它看起来真的很好。
然而,看起来更接近,“物理”保持原样,实际的记忆实际上只在你触摸它的那一刻才被使用。
VirtualLock
仍然是一个无操作(故障明智),但提高最小工作集大小的一种更接近目标。
但是,篡改WS大小有两个问题。 首先,一般来说,在一个进程中通常不会有最小工作集的千兆字节,因为操作系统会尽力保持locking的内存量。 这在我的情况下是可以接受的(实际上或多或less只是我所要求的)。
更大的问题是, SetProcessWorkingSetSize
需要PROCESS_SET_QUOTA
访问权限,作为“pipe理员”是没有问题的,但是当你作为受限用户运行程序时(出于很好的原因),它会失败,并且触发“允许可能有害的程序?” 警惕一些着名的俄罗斯杀毒软件( 没有很好的理由,但唉,你不能把它closures)。
技术上VirtualLock是一个提示,所以OS被允许忽略它。 它受NtLockVirtualMemory系统调用的支持,它在Reactos / Wine上是作为一个no-op来实现的,但是Windows真正支持系统调用(MiLockVadRange)。
VirtualLock不保证成功。 调用此函数需要SE_LOCK_MEMORY_PRIVILEGE才能工作,地址必须满足安全性和配额限制。 此外,在VirtualUnlock之后,内核不再需要将页面保存在内存中,因此之后的页面错误是一个有效的操作。
正如Raymond Chen指出的那样,当你解锁内存时,它可以正式释放页面。 这意味着下一页上的下一个VirtualLock可能会再次获得相同的页面,所以当您触摸原始页面时,仍然会出现页面错误。
VirtualLock
仍然是一个无操作(故障)
我试图重现这一点,但它工作正如人们所期望的。 运行此帖子底部显示的示例代码:
MEM_COMMIT
VirtualAlloc
2500 MB的RAM(2页故障) VirtualLock
( 大约641,250页错误 ) 这一切都按预期工作。 2500 MB的RAM是640,000页。 数字加起来。 另外,就OS范围的RAM计数器而言, VirtualAlloc
提交费用上升,而VirtualLock
物理内存使用率上升。
所以VirtualLock
绝对不是我的Win7 x64机器上没有的操作。 如果我不这样做,那么页面如预期般出错,转移到我开始写入RAM的地方。 他们仍然总计超过64万。 另外,第一次写内存需要更长的时间。
相反,就像它看起来一样,每个被访问的页面都是在发生错误时创建的,即使它以前被锁定了。
这没有错 。 不能保证访问锁定然后解锁页面不会出错。 你锁定它,它被映射到物理RAM。 您可以解锁它,并且可以自由地立即取消映射,从而使故障成为可能。 你可能希望它保持映射,但没有保证…
在我的系统上有几千兆字节的物理内存,它的工作方式就像你期望的那样:即使我使用一个即时的VirtualUnlock
跟踪我的VirtualLock
,并将最小工作集大小设置回小,出现页面错误。
这是我做的。 我运行了测试程序(下面),不用立即解锁内存并恢复合理的最小工作集大小的代码,然后在每种情况下强制使用物理内存。 在强制低RAM之前,这两个程序都没有任何页面错误。 在强制低RAM之后,保持内存锁定的程序保持其巨大的工作集并且没有进一步的页面错误。 解锁内存的程序却开始出现页面错误。
如果你首先挂起这个过程,这是最容易观察的,因为否则,即使内存没有被锁定(显然是一个理想的事情),常量内存写操作将其全部保留在工作集中。 但是,暂停过程,强制低RAM,并观看工作集缩小只为解锁RAM的程序。 恢复过程,并目睹页面错误雪崩。
换句话说,至少在Win7 x64中,一切都按照您预期的那样工作,使用下面提供的代码。
但是,篡改WS大小有两个问题。 首先,你通常并不意味着在一个进程中有一个最小工作集的千兆字节
那么…如果你想VirtualLock
,你已经篡改它。 SetProcessWorkingSetSize
所做的唯一的事情是允许你篡改它。 它本身不会降低性能; 它是VirtualLock
– 但是只有当系统在物理内存上运行的时候低。
这是完整的程序:
#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
之后进入休眠状态。 根据Raymond Chen在2007年发表的一篇文章 ,操作系统可以自由地将所有内容从物理内存中分离出来,直到线程再次唤醒。 另外请注意, MSDN声称 ,不管是否所有的线程都在睡觉,这个内存不会被分页。 在我的系统中,当唯一的线程正在休眠时,它们肯定保留在物理RAM中。 我怀疑雷蒙德的建议是在2007年应用的,但在Win7中不再是这样。
我没有足够的评价,所以我不得不添加这个答案。
请注意,此代码会使该线程在VirtualLock之后进入休眠状态。 根据Raymond Chen在2007年发表的一篇文章,操作系统可以自由地在这个时候把所有的内容全部用物理内存分页,直到线程再次唤醒,我怀疑Raymond的建议是在2007年应用的,但是不再是真实的Win7的。
什么romkyns说已经在2014年被Raymond Chen确认 。也就是说,当你用VirtualLock锁定内存时,即使你的所有线程都被阻塞,它仍然会被锁定。 他还表示,页面保持锁定状态,可能只是一个实现细节,而不是契约。
这可能不是这样,因为根据msdn,这是契约性的
进程已锁定的页面保留在物理内存中,直到进程解除锁定或终止。 这些页面被保证在被锁定时不被写入页面文件。