dll占的究竟是谁的空间?――浅谈Windows内存机制

作者:简简单单 2008-04-26

近来工作比较空闲,所以就上111com.net看看帖子什么的,两个多月前,我在VC/MFC板块中发了这么一个帖子:dll占的究竟是谁的空间?详细参考:

 

http://topic.111com.net/u/20080123/16/310330cd-e262-4534-b8c8-9bff892c7f21.html

 

关于这个帖子,我后面作了个总结,意思是说:dll占用的空间不属于某一个调用它的进程,dll是属于系统的,我得出这个标准答案的时候自己算是比较满意了,于是结了贴,事情并没这样结束,我昨天闲来无事,翻看以前的帖子,发现我结贴后还有一位朋友在帖子后留了言,我很感兴趣,正好也看到他在线,于是通过111com.net的web在线交流跟他(或者?)聊了挺久,之后又上网查了不少资料,然后结合自己实际看到的情况,对这个问题又有了新的认识,于是写这篇文章,当然我不敢说现在就是标准答案了,做blog的一个目的就是,share自己的知识,以便和众朋友交流。有什么不对,我再改进。

 

不过得在此先声明一下,这篇文章也不是final version,因为我对很多东西的认识也只停留在表面层次,所谓知其然不知其所以,因此我还打算在以后有了进一步研究之后重写这篇文章。我不喜欢太理论化的东西,诸如《XXXX原理》,整本书连一行代码都看不到,完全没有操作性可言,我希望我写的东西足够通俗易懂并且可以很简单地去实践,试验。

 

先说个概念的事情,就是我们经常说的一个词,价格,这一点都不陌生,很熟悉的词,我们去买东西的时候都喜欢讨价还价,最后以某个价格成交,比如一个冷饮一块五毛钱,一条鱼十块钱,再复杂一点的情况是买数码产品,比如一台数码相机,你问价,奸商会反问你:你要的是带的还是不带的?带不带可能有一两百的价差,这时候价格就被赋予不同的含义。如果买更大件些的东西,价格就不仅仅是一个数字,比如买房,就比较复杂了,你说的价格究竟是上家的到手价,还是是合同价,还是成交价,抑或是你要支付的金额?如果需要房贷,那办理贷款的手续费,甚至以后要偿还的利息算不算在房价内?因为这也算是你为了这个房子的付出啊,所以这个时候价格就比较复杂了,实际情况可能比这更复杂一些,得多费些工夫才能完全理解其意义。内存的占用,也是这样的道理,如果是单片机,我想没什么好说的,是多少就多少,但虚拟内存这个概念一出现,情况就变得复杂了,再加上”“这些概念,再加上DLL,这个程序会占用多少内存?这个问题就不简单了。

 

我们最直截了当地了解进程所占用内存的方法是通过Windows自带的任务管理器(Task Monitor),有两列是最值得我们关心的,一列是内存使用(Mem Usage),一列是虚拟内存大小(VM Size),在从事Windows开发以前,我一直认为Mem Usage是进程所占用的物理内存,而VM Size是程序所占用的虚拟内存(物理内存不够就把硬盘模拟为内存,然后把该存放在物理内存中的数据存到硬盘上去),所以占用的总的内存大小应该是两者的和,这种理解明显这是不对的。不过也不能完全怪我,其实Mem Usage这个名称本身就有其误导性,准确的解释:Mem Usage指的是该进程的Working Set Size,什么是Working Set Size?Working Set Size就是一个进程能直接不发生缺页错误(Page Faults)地访问的物理内存的大小。也许你会想:嗯?搞什么文字游戏?这不就是占用的物理内存大小么?我说那未必,不信就看下面这个图。

 

图中一共打开了11个App1.exe程序,如果Mem Usage指的是各个进程所占用的物理内存的话,那11个App1.exe的实例一共占据的物理内存就是就是574112K,约561M,可我的电脑的物理内存只有512M啊,这怎么可能?再看这11个App1.exe的VM Size,344K,明显Mem Usage可以大于VM Size,当然,从图中也能看到有VM Size大于Mem Usage的情况。这究竟是为什么?

 

对于这两个值,我目前的理解是这样的,Mem Usage就如前面所说,指的是一个进程能直接不发生Page Faults地访问的物理内存的大小,(本文为了方便起见,之后还是把Mem Usage称为物理内存占用,但读者必须清楚它的实际意义)VM Size是进程本身已经commit的虚拟内存。

 

关于Mem Usage:上述的11个App1.exe看起来肯定共同拥有了一些物理内存,它们可以共同访问这些物理内存而不发生Page Faults,如何来实现这个“共同拥有”?聪明的你一定想到了,用dll,我给出关键代码段,然后读者你自己去试试看。

 

这是dll中的代码:

 

#pragma comment(linker,"/section:.SharedDataName,rws")

#pragma data_seg (".SharedDataName")

__declspec(dllexport) char szDataSegTest[50*1024*1024]="";

/* 注意:后面的“=""”不能去除,否则就跟非共享段的没什么差别,至于为什么,得去问Microsoft */

#pragma data_seg()

 

这是App1中的代码

 

extern char __declspec(dllimport) szDataSegTest[];

int main(int argc, char* argv[])

{

    for(int i=0; i<50*1024*1024; i+=4096)

    {

       sprintf(&szDataSegTest[i], "%u", time(NULL));

    }

    getchar();

    return 0;

}

 

实验1:运行多个App1的实例,观察Task Monitor

 

通过DLL的“共享段”来共享数据,这是Microsoft提供的一种不错的功能,这不是C++的功能,算是Microsoft的扩展功能,我是觉得这个功能不错,在预先知道数据长度的情况下轻松实现进程间数据共享。也许你要问,main函数中的那个for循环是什么意思?问得好,现在你不妨再来做个实验。

 

实验2:把那段for循环注释掉,再运行程序,观察Task Monitor

 

你会发现这时候的Mem Usage才几百K,完全没有50M那么大,为什么?原因是这样,如果这段内存你没用到,Windows是不会预先去映射的,当你的程序需要访问这段内存,就发生缺页,然后Windows才会去映射,所以通过这个for去使用了这段内存,你才能看到这50MMem Usage。那现在问题又来了,是不是我只需要:

 

szDataSegTest[0] = '''';

 

这么一个操作,就可以看得到这50MMem Usage呢?答案是否定的,实验当然你可以马上做一下,因为Windows映射内存的单位是页,发生缺页的时候,就产生Page FaultsWindows才会映射一个页,那一个页究竟多大?也许你已经看到了,就是代码中的4096,当然并不是所有的Windows系统的页大小(Page Size)都是4096,但我们目前能接触的应该是4096,获取Page Size的方法是GetSystemInfo,具体参考MSDN。顺便提一下这个Page Size操作系统写死的,不能随便改变的。由于这个共享的机制,同时运行11App1.exe并不会真的占用561M内存。

 

另外,Task Monitor中也是能看Page Faults的数量的,上面这个App1运行一次所发生的Page Faults大概是50*1024*1024/4096=12800次,事实上肯定会比这个数字大一些,因为程序Load到内存的时候或多或少都会发生缺页的。

 

OK,我们进入下一步实验。

 

实验3:程序还是上面的程序,取消刚才的注释,现在,运行了这个程序后,把它最小化,然后再观察Task Monitor

 

在我的机器上,Mem Usage50M变成了156K,我估计在你的机器上也差不多,50M的物理内存占用突然不见了,为什么?其实这个程序不是个特例,只不过我用一个50M的数组来把这种现象夸张放大了,你观察下别的程序也会这样的,最小化之后Mem Usage小了不少,然后再把程序窗口还原,也许Mem Usage能恢复变大,但通常没有原来的大了。这其实并不是程序本身作了什么特殊的处理,这完全是Windows的功能。Windows的设计者考虑到这么种现象,就是我们用户在最小化一个程序之后,往往是暂时不想再使用这个程序,所以有必要把这个程序占用的一些物理内存释放出来,以便供别的程序使用,所以这个时候Windows就把最小化程序的Working Set Size弄小了,其实用户也可以“手动地”把Working Set Size弄小,通过这个APISetProcessWorkingSetSize,具体参考MSDN,但手动调整这个是完全没有必要的,Windows足够智能去处理这些事情了,所以什么所谓“内存整理工具”都是画蛇添足。

 

现在,我们来改一下程序,DLL部分不变,改App1

 

int main(int argc, char* argv[])

{

    for(int i=0; i<50*1024*1024; i+=4096)

    {

       sprintf(&szDataSegTest[i], "%u", time(NULL));

    }

    getchar();

    for(i=0; i<50*1024*1024; i+=4096)

    {

       sprintf(&szDataSegTest[i], "%u", time(NULL));

    }

    getchar();

    return 0;

}

 

实验4:运行程序,最小化,观察Task Monitor中的Mem UsagePage Faults,然后恢复程序窗口,按一下回车键,让程序继续运行,再观察Task Monitor,然后再把程序最小化,再观察一次Task Monitor

 

我想你已经发现了,Page Faults几乎翻倍。“错误”本身不是什么好东西,页面错误亦然,但Windows在这里为什么竟敢纵容那么多的Page Faults?我想那是因为这种所谓“错误”其实并不怎么严重的缘故,为什么这么说?我们可以再做个实验。

 

实验5:运行两个App1,把其中一个最小化,观察Task Monitor

 

你会发现最小化的那个的Mem Usage变成了156K,而另一个还是50M。它们共享的是同样的的一段物理内存,这段物理内存明显还在使用中,而最小化的那个要继续访问这段物理内存的话就得发生Page Faults,这就有点不太合情理了,内存既然还在使用,为什么Windows要取消最小化的App1对这段物理内存的映射关系?也许这只是个字面游戏,虽然这里发生了大量的Page Faults,可程序速度几乎不受影响,你可以在两个for循环的前后加个GetTickCount来测试一下耗时。代码类似这样:

 

unsigned int dwBegin = GetTickCount();

for(int i=0; i<50*1024*1024; i+=4096)

{

    sprintf(&szDataSegTest[i], "%u", time(NULL));

}

unsigned int dwEnd = GetTickCount();

printf("elapsed[%d]n", dwEnd-dwBegin);

 

实验6:运行App1,按回车看结果,关闭;再运行App1,最小化,恢复,按回车看结果。

 

两者差别很小,几乎不会影响什么性能,但我这里说的只是这种形式的Page Faults不影响性能,并不是说所有类型的Page Faults不影响性能,为什么?这样说下去可能话就长了,加上我对Windows内部的运作还不是很了解,所以只能大概地说:这种类型的Page Faults导致的额外开销只是重新建立App1对共享段内存的映射关系,而无需将页面文件的数据Load到内存中,所以对性能影响不大。也许敏捷的你马上要问:“你刚才把App1最小化了,那它使用的物理内存不是被系统收回了么?为什么恢复它只是‘重新建立映射关系’而已?”很好,能问这个问题真是了不起,关于这个,我们马上再做个实验。(读者:怎么这么多实验?作者:本人没什么水平,只能用实验来说明问题,呵呵。)

 

现在打开Task Monitor,但现在看的是“性能”这个标签页,而不是“进程”这个标签页,这里我们能看到CPU,物理内存,页面文件的总体使用情况:

 

也许你注意到了,我的电脑有两个CPU,准确说一个双核CPUWindows把它当成两个了,现在已经进入了双核时代,还有我的物理内存,514088K,不足512M,嗯?到底怎么回事?不能怪我啊,公司的电脑都没有独立显卡的,只能把一部分内存用作显存了,嗯,要求不能太高。(老板:嗯?你小子居然在上班时候写blog!)

 

实验7:先观察一下Task Monitor,注意页面文件(PF)使用情况和物理内存使用情况,运行App1,看Task Monitor,然后把App1最小化,继续观察Task Monitor,恢复App1,按回车键,让它继续跑,再最小化,再观察Task Monitor,最后把App1关掉,再观察Task Monitor

 

这是一个非常有趣的现象,在程序运行的时候,PF使用突然增加50M,物理内存可用量降低50M,把程序最小化之后,按道理说物理内存很大一部分就被系统收回(只运行一个App1的实例情况下),前面你也看到了,程序最小化后的Mem Usage瞬间剧减至156K,所以我们应该看到的物理内存可用量会剧增50M,可这里不会,你会发现,物理内存的可用量会以大约每秒500K左右的速度,增加50M,这个过程不是瞬间完成的,也就是说Windows对物理内存的回收不是一下子完成的,为什么?我想最主要的原因是性能问题,一次性将巨大的物理内存的数据写入到页面文件中去以腾出物理内存空间的这种做法是低效的,假如物理内存中的数据有200M,(现在物理内存动辄2G的硬件配置,这不算什么啦)你可以尝试一次性在磁盘上写入200M的内容看看需要花费多少时间,虽然Windows有高速缓冲机制,但对于巨大的数据,这种缓冲仍然是不足的,如果用户最小化程序窗口之后,紧接着将程序窗口恢复并运行(这种操作还是比较多见的),那Windows又得从页面文件中调出这200M的数据,效率就可想而知,所以Windows对内存的回收是一点点的,偷偷摸摸地进行的,这样确保了我们的系统看起来很流畅。如果运行了两个App1的实例,只是最小化其中一个,那这种内存回收的现象将观察不到。好,回到这个实验当中,到了最后,你把App1关掉,但我还要求你继续观察Task Monitor,为什么呢?我还是相信你的观察力的,你会看到:App1所占据的物理内存会被马上释放,而页面文件使用量并没有马上降低50M,并且,你看不到页面文件使用量的明显变化,甚至观察了几分钟都没有什么动静,冷静冷静,你要有足够的耐心,为了探求真理,等个10来分钟,如何?还是没动静?好吧,别等了,我承认这个情况是比较复杂的,这依赖于Windows对页面文件的管理算法,这个页面文件使用是会释放的,但我也说不清什么时候会,你如果还愿意再等,那么现在停止手头的工作,出去跑两圈,回来看看也许就释放掉这50M了。什么?你问我Windows

相关文章

精彩推荐