http://zjc.wuse.edu.cn/

轉載自:http://blog.csdn.net/guogangj/archive/2008/04/08/2261031.aspx

 

近來工作比較空閒,所以就上csdn.net看看帖子什麼的,兩個多月前,我在VC/MFC板塊中發了這麼一個帖子:dll佔的究竟是誰的空間?詳細參考:

 

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

 

關於這個帖子,我後面作了個總結,意思是說:dll佔用的空間不屬於某一個調用它的進程,dll是屬於系統的,我得出這個標準答案的時候自己算是比較滿意了,於是結了貼,事情並沒這樣結束,我昨天閒來無事,翻看以前的帖子,發現我結貼後還有一位朋友在帖子後留了言,我很感興趣,正好也看到他在線,於是通過csdn.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] = '/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關於頁面文件管理的的算法?我最怕算法這些東西了,我大學時候數學都是混着過去的。

 

這裏再插入一個相對單獨一些的論題,就是:DLL會不會在引用它的進程全部結束時候馬上被移除出內存?我們可以寫個App2來試試看,DLL代碼保持不變:

 

extern char __declspec(dllimport) szDataSegTest[];

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

{

    printf("[%s]", szDataSegTest);

    sprintf(szDataSegTest, "%u", time(NULL));

    return 0;

}

 

這個代碼的意思是,App2全部退出後,如果DLL還繼續有效駐留內存的話,再運行一次App2,應該能看到前面一個App2給這個DLL的共享段賦的那個值。

 

實驗:運行App2,再運行App2,觀察兩個App2的運行情況;運行App2,按回車結束App2的運行,緊接着馬上再運行一個App2,觀察兩個App2的運行情況。

 

實驗結果證明了引用DLL的程序全部結束後,DLL將立即被移出內存,不再有效,也許你認爲這個證據還不夠充分。好吧,我這裏提供個別的門路去檢驗:“超級兔子任務管理器”就提供了查看駐留內存的所有模塊的這個功能,大家可以嘗試用類似的工具檢驗一下。

 

寫了這麼多,大家應該多Mem UsageVM SizePage File這些東西有概念了吧。我們準備轉入下個主題了。

 

×××××××× 不 怎 麼 華 麗 的 分 割 線 ××××××××

 

在前面,我主要提了3個概念,一個Mem Usage,一個VM Size,一個Page File,對於Mem Usage,大家都很清楚了吧;Page File,估計也沒什麼問題了。這兩個寶一個是物理內存,一個是一直被廣大用戶誤會的虛擬內存(磁盤上的頁面文件,在Linux中,叫swap分區)。那麼VM Size號稱虛擬內存,究竟大小怎麼來定呢?我前面提到,VM Size是進程本身已經commit的虛擬內存,那什麼叫commit?OK,先別急,我們來改一下前面用來做實驗的那個DLL的代碼,以事實來說明問題。

 

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

#pragma data_seg (".SharedDataName")

#pragma data_seg()

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

 

其實就是把szDataSegTest的定義移出共享段,App1代碼不變。

 

實驗8:運行5App1的實例。(注意:這個實驗有一定“危險性”,確保你的電腦的物理內存不低於256M

 

運行結果大致如下圖所示:

 

比同前面齊刷刷的52192K,這個結果多少有些“不和諧”,468484684898964684847216,這是我其中一次的運行情況,你的情況跟我的不太可能相同,你如果完成了這個實驗一次,再做一次,會發現跟上次的結果也不太可能相同,但它們的VM Size卻完全一樣51544。在做這個實驗的時候,如果你電腦的內存跟我的一樣,只有512M,那你會明顯感覺有點卡,硬盤嘎吱嘎吱地響,如果內存只有256M,我想那卡得會更明顯了,硬盤也更累了,這就是內存不足,Windows不斷地寫頁面文件的結果,如果你在觀察頁面文件的使用量,你會發現頁面文件使用激增,完全不是剛纔使用共享段的那樣,增加了一次50M就完。而這次的Mem Usage也明顯比上次的少了,你敢不敢多運行幾個實例?好,我相信你有膽量,大不了就多聽聽硬盤交響曲。運行了之後你把它們所有的Mem Usage加起來,看看有沒有可能突破561M?不管你運行了多少,都不可能,運行更多隻會讓系統縮減其它進程的Working Set Size。由於這次訪問的DLL的內容不屬於共享段,所以每次對這個大數組進行寫操作,都得基於一個Copy-On-Write的規則,也就是在App1自己的進程空間裏分配一塊內存,複製大數組中相應的內容,將要寫向大數組的東西寫到自己這塊內存裏,當然這個分配同前面的一樣,也是基於頁的分配規則。由於這是進程在自己的空間裏分配的,所以我們清楚地看到VM Size是一點都不含糊的50M,而不是像之前的例子那樣反映出來的344K,(之前由於是共享段,只需要映射過去,不需要獨另分配)VM Size和它的名字並不怎麼相符,它不是一個虛無飄渺的數字,它代表了實際上進程commit的內存,所以它是必須有頁面文件或者物理內存作爲支持的,通常物理內存有限的情況下,就是增加頁面文件來支持這個內存分配。另外你會看到這次結束每個App1都能導致頁面文件使用量的顯著減少,如圖:

 

大家可以順便做做最小化後觀察內存變化的實驗,看看跟前面的有什麼不同,原理是一樣的。這裏略過。

 

前面反反覆覆提到一個詞——commit,這是什麼意思呢?比較貼近的意思是“提交”,“確認”,使用過數據庫都知道,commit之後數據的更改纔會真正生效,版本管理軟件svn也是一樣,用commit來提交更改確認。而內存分配翻譯爲“提交”卻不太通順,其實這個詞來自虛擬內存分配函數VirtualAlloc。(我想的,不知道正確否)關於VirtualAlloc,大家可以看看MSDN,它的第三個參數是分配類型,分配類型一共有兩種,分別是MEM_RESERVEMEM_COMMITMEM_RESERVE是“預留”,在進程空間裏預留出一塊區域,但不對它進行實際的分配,這塊區域可以不對應任何實際的物理內存或者頁面文件,而MEM_COMMIT則是真實的分配,這麼說大家都應該知道commit的意思了吧。當然你也可以reserve一塊相當大的內存出來,看看VM Size有沒有變化,實驗結果會告訴你:只reserve的話是不會增加VM Size的。C++中這樣的語句很多:

 

char *p = new char[100];

 

這其實就是commit了一塊sizeof(char[100])大小的內存,執行了這個語句你會看到VM Size中會多出了100個字節……厄,太少了,可能你看不到。

 

到此爲止,你認爲觀察一個程序所佔用的內存,是看Mem Usage準確一些呢,還是看VM Size準確一些呢?我看還是看VM Size更準確一些。但這個“更準確”也不代表絕對,這只是相對而言。對於共享段,你說這麼多個App1的實例爲什麼都不承認這個段屬於它們的?但如果所有的App1的實例都結束運行之後,這個DLL就將被移出內存,因此回答這篇文章的標題問題的話,應該這樣說吧:DLL是共同屬於引用它的進程的,這比“DLL是屬於系統的”來得更準確些。

 

寫到這裏,我也有點累了,我很多問題都沒有完全搞懂,比如Windows的內存回收機制,VM Size的具體計算方法等。

 

在此得感謝csdn.net的朋友cnzdgs,希望他/她看到這篇文章的時候能留言,交流。還有另一篇很不錯的文章,給了我很多啓示,大家可以通過下面這個鏈接看到:

 

http://blog.joycode.com/qqchen/archive/2004/03/17/16434.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章