深入瞭解Windows控制代碼到底是什麼

來源:互聯網
上載者:User

標籤:file   sni   思想   理解   分割   相對   應用   mtk   hicon   

原文連結:http://blog.csdn.net/wenzhou1219/article/details/17659485【侵刪】

目錄(?)[-]

  1. 虛擬記憶體結構
  2. 控制代碼結構
 

總是有新入門的Windows程式員問我Windows的控制代碼到底是什麼,我說你把它看做一種類似指標的標識就行了,但是顯然這一答案不能讓他們滿意,然後我說去問問度娘吧,他們說不行網上的說法太多還難以理解。今天比較閑,我上網查了查,光是百度百科詞條“控制代碼”中就有好幾種說法,很多敘述還是錯誤的,天知道這些誤人子弟的人是想幹什麼。

這裡我列舉詞條中的關於控制代碼的敘述不當之處,至於如何不當先不管,繼續往下看就會明白:

1.windows 之所以要設立控制代碼,根本上源於記憶體管理機制的問題—虛擬位址,簡而言之資料的地址需要變動,變動以後就需要有人來記錄管理變動,(就好像戶籍管理一樣),因此系統用控制代碼來記載資料地址的變更。

2.如果想更透徹一點地認識控制代碼,我可以告訴大家,控制代碼是一種指向指標的指標。

 

通常我們說控制代碼是WINDOWS用來標識被應用程式所建立或使用的對象的唯一整數。這句話是沒有問題的,但是想把這句話對應到具體的記憶體結構上就做不到了。下面我們來詳細探討一下Windows中的控制代碼到底是什麼。

1.虛擬記憶體結構

要理解這個問題,首先不能避開Windows的虛擬記憶體結構。對於這個問題已有前人寫了比較好的解釋,這裡我為了保證部落格連貫性,直接貼上需要的部分(原文是講解Java JVM虛擬機器的效能提升的文章,在其中涉及到了虛擬記憶體的內容,解釋的非常好,這裡我截取這部分略加修改,這裡是文章連結)

我們知道,CPU是通過定址來訪問記憶體的。32位CPU的定址寬度是 0~0xFFFFFFFF ,計算後得到的大小是4G,也就是說可支援的實體記憶體最大是4G。但在實踐過程中,碰到了這樣的問題,程式需要使用4G記憶體,而可用實體記憶體小於4G,導致程式不得不降低記憶體佔用。

為瞭解決此類問題,現代CPU引入了 MMU(Memory Management Unit 記憶體管理單元)。

MMU 的核心思想是利用虛擬位址替代物理地址,即CPU定址時使用虛址,由 MMU 負責將虛址映射為物理地址。MMU的引入,解決了對實體記憶體的限制,對程式來說,就像自己在使用4G記憶體一樣。

 

記憶體分頁(Paging)是在使用MMU的基礎上,提出的一種記憶體管理機制。它將虛擬位址和物理地址按固定大小(4K)分割成頁(page)和頁幀(page frame),並保證頁與頁幀的大小相同。這種機制,從資料結構上,保證了訪問記憶體的高效,並使OS能支援非連續性的記憶體配置。在程式記憶體不夠用時,還可以將不常用的實體記憶體頁轉移到其他存放裝置上,比如磁碟,這就是大家耳熟能詳的虛擬記憶體。

在上文中提到,虛擬位址與物理地址需要通過映射,才能使CPU正常工作。
而映射就需要儲存映射表。在現代CPU架構中,映射關係通常被儲存在實體記憶體上一個被稱之為頁表(page table)的地方。
如:

從這張圖中,可以清晰地看到CPU與頁表,實體記憶體之間的互動關係。

進一步最佳化,引入TLB(Translation lookaside buffer,頁表寄存器緩衝)
由上一節可知,頁表是被儲存在記憶體中的。我們知道CPU通過匯流排訪問記憶體,肯定慢於直接存取寄存器的。
為了進一步最佳化效能,現代CPU架構引入了TLB,用來緩衝一部分經常訪問的頁表內容。
如:

對比 9.6 那張圖,在中間加入了TLB。

為什麼要支援大記憶體分頁?
TLB是有限的,這點毫無疑問。當超出TLB的儲存極限時,就會發生 TLB miss,之後,OS就會命令CPU去訪問記憶體上的頁表。如果頻繁的出現TLB miss,程式的效能會下降地很快。

為了讓TLB可以儲存更多的頁地址映射關係,我們的做法是調大記憶體分頁大小。

如果一個頁4M,對比一個頁4K,前者可以讓TLB多儲存1000個頁地址映射關係,效能的提升是比較可觀的。

簡而言之,虛擬記憶體將記憶體邏輯地址和物理地址之間建立了一個對應表,要讀寫邏輯地址對應的實體記憶體內容,必須查詢相關頁表(當然現在有還有段式、段頁式記憶體對應方式,但是從原理上來說都是一樣的)找到邏輯地址對應的物理地址做相關操作。我們常見的對程式員開放的記憶體配置介面如malloc等分配的得到的都是邏輯地址,C指標指向的也是邏輯地址。

這種虛擬記憶體的好處是很多的,這裡以連續記憶體配置和可移動記憶體為例來講一講。

首先說一說連續記憶體配置,我們在程式中經常需要分配一塊連續的記憶體結構,如數組,他們可以使用指標迴圈讀取,但是實體記憶體多次分配釋放後實際上是破碎的,如

圖中白色為可用實體記憶體,黑色為被其他程式佔有的記憶體,現在要分配一個12大小的連續記憶體,那麼顯然實體記憶體中是沒有這麼大的連續記憶體的,這時候通過頁表對應的方式可以看到我們很容易得到邏輯地址上連續的12大小的記憶體。

再說一說可移動記憶體,我們使用GlobalAlloc等函數時,經常會指定GMEM_MOVABLE和GMEM_FIXED參數,很對人對這兩個參數很頭疼,搞不明白什麼意思。

實際上這裡的MOVABLE和FIXED都是針對的邏輯地址來說的。GMEM_MOVABLE是說允許作業系統(或者應用程式)實施對記憶體堆(邏輯地址)的管理,在必要時,作業系統可以移動記憶體塊擷取更大的塊,或者合并一些閒置記憶體塊,也稱“記憶體回收”,它可以提高記憶體的利用率,這裡的地址都是指邏輯地址。同樣以分配12大小連續的記憶體,在某種狀態時,記憶體結構如下

顯然這時候是無法分配12連續大小的記憶體,但是如果這裡的邏輯地址都指明為GMEM_MOVABLE的話,作業系統這時候會對邏輯地址做管理,得到如下結果

這時候就實現了邏輯地址的MOVE,相對比實現實體記憶體的移動,這樣的代價當然要小得多撒,但是聰明的小夥伴們是不是要問,這樣在邏輯地址中移動了記憶體,那麼實際訪問資料不都亂套了嗎,還能找到自己分配的實際實體記憶體資料嗎,等等,不要心急,這就是等下要講的控制代碼做的事情了。

GMEM_FIXED是說允許在實體記憶體中移動記憶體塊,但是必須保證邏輯地址是不變的,在早期16位Windows作業系統不支援在實體記憶體中移動記憶體,所以禁止使用GMEM_FIXED,現在的你估計體會不到了。

事實上用GlobalAlloc分配記憶體時指定GMEM_FIXED參數返回的控制代碼就是指向記憶體配置的記憶體塊的指標,不理解???接著看下面的控制代碼結構,你就明白了。

2.控制代碼結構在上面講解虛擬記憶體結構的過程中,我們就引出了幾個問題:MOVABLE的記憶體訪問為什麼不會亂,FIXED的記憶體為什麼說就是指向分配記憶體塊的指標。事實上我們儘管Windows沒有給出源碼,但是從一些標頭檔、MSDN和Windows早期記憶體配置函數中我們還是可以一窺端倪。在Winnt.h標頭檔中做了通用控制代碼的定義 [cpp] view plain copy  print?
  1. #ifdef STRICT  
  2. typedef void *HANDLE;  
  3. #define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name  
  4. #else  
  5. typedef PVOID HANDLE;  
  6. #define DECLARE_HANDLE(name) typedef HANDLE name  
  7. #endif  
  8. typedef HANDLE *PHANDLE;  
在Windef.h做了特殊控制代碼的定義 [cpp] view plain copy  print?
  1. #if !defined(_MAC) || !defined(GDI_INTERNAL)  
  2. DECLARE_HANDLE(HFONT);  
  3. #endif  
  4. DECLARE_HANDLE(HICON);  
  5. #if !defined(_MAC) || !defined(WIN_INTERNAL)  
  6. DECLARE_HANDLE(HMENU);  
  7. #endif  
  8. DECLARE_HANDLE(HMETAFILE);  
  9. DECLARE_HANDLE(HINSTANCE);  
  10. typedef HINSTANCE HMODULE;      /* HMODULEs can be used in place of HINSTANCEs */  
  11. #if !defined(_MAC) || !defined(GDI_INTERNAL)  
  12. DECLARE_HANDLE(HPALETTE);  
  13. DECLARE_HANDLE(HPEN);  
  14. #endif  
  15. DECLARE_HANDLE(HRGN);  
  16. DECLARE_HANDLE(HRSRC);  
  17. DECLARE_HANDLE(HSTR);  
  18. DECLARE_HANDLE(HTASK);  
  19. DECLARE_HANDLE(HWINSTA);  
  20. DECLARE_HANDLE(HKL);  
這裡微軟把通用控制代碼HANDLE定義為void指標,顯然啦,他是不想讓人知道控制代碼的真實類型,但是和他以往的做法一樣,微軟空有一個好的想法結果沒有實現。馬上,如果定義了強制類型檢查STRICT,他又定義了特殊類型控制代碼宏DECLARE_HANDLE,這裡用到了##,這是比較偏僻的用法,翻譯過來,對於諸如DECLARE_HANDLE(HMENU)定義其實就是 [cpp] view plain copy  print?
  1. typedef struct HMENU__  
  2. {  
  3.     int unused;  
  4. } *HMENU;  
到這裡,你是不是覺得有一點眉目了呢,對,控制代碼是一種指向結構體的指標,結合這裡的int unused定義很容易猜到結構體的第一個欄位就是我們的邏輯地址(指標) 。那麼,是不是僅僅如此呢,當然不是!!!由於指向結構體指標可以強制截斷只擷取第一個欄位,這裡的struct結構體絕對不止一個欄位,聯絡我們在Windows中的編程經驗,對於線程HANDLE有計數那麼必須有計數段,對於事件HEVENT等核心對象會要求指定屬性那麼必須有屬性段,對於記憶體配置HANDLE有可移動和不可移動之說那麼必須有記憶體可移動屬性段,等等。基於此我們可以大膽猜測Windows的控制代碼指向的結構類似如下 [cpp] view plain copy  print?
  1. struct    
  2. {  
  3.     int pointer;        //指標段  
  4.     int count;          //核心計數段  
  5.     int attribute;      //檔案屬性段:SHARED等等  
  6.     int memAttribute;   //記憶體屬性段:MOVABLE和FIXED等等  
  7.     ...  
  8. };  
事實上,Windows記憶體管理器管理的其實都是控制代碼,通過控制代碼來管理指標,Windows的系統整理記憶體時檢測記憶體屬性段,如果是可以移動的就能夠移動邏輯地址,移動完後將新的地址更新到對應控制代碼的指標段中,當要使用MOVABLE地址時的時候必須Lock住,這時候計數加1,記憶體管理器檢測到計數>0便不會移動邏輯地址,這時候才能獲得固定的邏輯地址來操作實體記憶體,使用完後Unlock記憶體管理器又可以移動邏輯地址了,到此MOVABLE的記憶體訪問為什麼不會亂這個問題就解決了。下面再說一說,FIXED的記憶體為什麼說就是指向分配記憶體塊的指標。我們看上面的通用控制代碼定義,可以發現HANDLE的控制代碼定義一直是void指標,其他的特殊控制代碼在嚴格類型檢查的時候定義為結構體指標,為什麼不把二者定義為一樣的呢。查看MSDN關於GlobalAlloc的敘述對於GMEM_FIXED類型"Allocates fixed memory. The return value is a pointer.",這裡返回的是一個指標,為了驗證這個說法,我寫了一小段程式 [cpp] view plain copy  print?
  1. //GMEM_FIXED  
  2. hGlobal = GlobalAlloc(GMEM_FIXED, (lstrlen(szBuffer)+1) * sizeof(TCHAR));  
  3. pGlobal = GlobalLock(hGlobal);  
  4. lstrcpy(pGlobal, szBuffer);  
  5. _tprintf(TEXT("pGlobal和hGlobal%s\n"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));  
  6. GlobalUnlock(hGlobal);  
  7.   
  8. _tprintf(TEXT("使用控制代碼當做指標訪問的資料為:%s\n"), hGlobal);  
  9.   
  10. GlobalFree(hGlobal);  
運行結果為 [plain] view plain copy  print?
  1. pGlobal和hGlobal相等  
  2. 使用控制代碼當做指標訪問的資料為:Test text  
對比使用GMEM_MOVABLE程式為 [cpp] view plain copy  print?
  1. //GMEM_MOVABLE  
  2. hGlobal = GlobalAlloc(GMEM_MOVEABLE, (lstrlen(szBuffer)+1) * sizeof(TCHAR));  
  3. pGlobal = GlobalLock(hGlobal);  
  4. lstrcpy(pGlobal, szBuffer);  
  5. _tprintf(TEXT("pGlobal和hGlobal%s\n"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));  
  6. _tprintf(TEXT("使用控制代碼當做指標訪問的資料為:%s\n"), hGlobal);  
  7. GlobalUnlock(hGlobal);  
  8.   
  9. GlobalFree(hGlobal);  
運行結果為 [cpp] view plain copy  print?
  1. pGlobal和hGlobal不相等  
  2. 使用控制代碼當做指標訪問的資料為:?  
顯然,使用GMEM_FIXED和使用GMEM_MOVABLE得到的資料類型不是一樣的,我們有理由相信Windows在調用GlobalAlloc使用GEM_FIXED的時候返回的就是資料指標,使用Windows在調用GMEM_MOVABLE的時候返回的是指向結構體的控制代碼,這樣操作的原因相信是為了使用更加方便。那麼這裡我們就要修正一下前面的說法了:通用控制代碼HANDLE有時候是邏輯指標,大多數時候是結構體指標,特殊控制代碼如HMENU等是結構體指標。這樣第二個問題也解決了。


那麼總結來說,就是下面一幅圖了 下面,我們再回頭看一看博文開頭說的敘述不當之處,說他們不當是因為不是完全錯誤:第一點,確實控制代碼有管理記憶體位址變動之用,但是並不只是這個作用,核心對象存取層級、檔案是否開啟都是和他相關的;第二點,指向指標的指標,看得出來作者也是認真思考了的,但是他忽略了控制代碼包含的其他功能和管理記憶體位址的作用。 那麼到這裡對於控制代碼你應該非常理解了,在此基礎我們在Windows編程上是不是可以有一些啟發:1.通用控制代碼HANDLE和特殊控制代碼一般情況下是可以相互轉換的,但是有時候會出錯2.如果不考慮跨平台移植的話,應該多採用Windows SDK提供的記憶體管理函數,這樣可以獲得更好的記憶體管理3.C語言的記憶體配置函數的實現都是依靠使用GMEM_FIXED調用Windows SDK的記憶體配置函數的

完整測試原始碼下載連結注意可能在新的VS2005等系列編譯器中看不到本文說的一些內容,因為在VC6時候有些代碼還不是那麼完善,所以給了我們機會去挖掘潛在的內容。至於微軟苦心積慮不讓我們看到控制代碼的真實定義那是必然的,試想一下主要的記憶體對象結構都被摸清楚了,那麼駭客們還不反了天了。原創,轉載請註明來自http://blog.csdn.net/wenzhou1219

深入瞭解Windows控制代碼到底是什麼

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.