深入理解windows句柄

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

這裏我列舉詞條中的關於句柄的敘述不當之處至於如何不當先不管繼續往下看就會明白:

1.windows 之所以要設立句柄根本上源於內存管理機制的問題—虛擬地址簡而言之數據的地址需要變動變動以後就需要有人來記錄管理變動就好像戶籍管理一樣因此係統用句柄來記載數據地址的變更。

2.如果想更透徹一點地認識句柄我可以告訴大家句柄是一種指向指針指針


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

1.虛擬內存結構

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


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

爲了解決此類問題現代CPU引入了 MMUMemory Management Unit 內存管理單元。

MMU 的核心思想是利用虛擬地址替代物理地址即CPU尋址時使用虛址由 MMU 負責將虛址映射爲物理地址。MMU的引入解決了對物理內存的限制對程序來說就像自己在使用4G內存一樣。

內存分頁(Paging)是在使用MMU的基礎上提出的一種內存管理機制。它將虛擬地址和物理地址按固定大小4K分割成頁(page)和頁幀(page frame)並保證頁與頁幀的大小相同。這種機制從數據結構上保證了訪問內存的高效並使OS能支持非連續性的內存分配。在程序內存不夠用時還可以將不常用的物理內存頁轉移到其他存儲設備上比如磁盤這就是大家耳熟能詳的虛擬內存。

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

物理內存之間的交互關係

從這張圖中可以清晰地看到CPU與頁表物理內存之間的交互關係。

進一步優化引入TLBTranslation lookaside buffer頁表寄存器緩衝
由上一節可知頁表是被存儲在內存中的。我們知道CPU通過總線訪問內存肯定慢於直接訪問寄存器的。
爲了進一步優化性能現代CPU架構引入了TLB用來緩存一部分經常訪問的頁表內容。
如下圖

加入了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?在CODE上查看代碼片派生到我的代碼片

  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?在CODE上查看代碼片派生到我的代碼片

  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?在CODE上查看代碼片派生到我的代碼片

  1. typedef struct HMENU__  

  2. {  

  3.     int unused;  

  4. } *HMENU;  

到這裏你是不是覺得有一點眉目了呢對句柄是一種指向結構體的指針結合這裏的int unused定義很容易猜到結構體的第一個字段就是我們的邏輯地址(指針) 。那麼是不是僅僅如此呢當然不是由於指向結構體指針可以強制截斷只獲取第一個字段這裏的struct結構體絕對不止一個字段聯繫我們在Windows中的編程經驗對於線程HANDLE有計數那麼必須有計數段對於事件HEVENT等內核對象會要求指定屬性那麼必須有屬性段對於內存分配HANDLE有可移動和不可移動之說那麼必須有內存可移動屬性段等等。基於此我們可以大膽猜測Windows的句柄指向的結構類似如下

[cpp] view plain copy print?在CODE上查看代碼片派生到我的代碼片

  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?在CODE上查看代碼片派生到我的代碼片

  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?在CODE上查看代碼片派生到我的代碼片

  1. pGlobal和hGlobal相等  

  2. 使用句柄當做指針訪問的數據爲:Test text  

對比使用GMEM_MOVABLE程序爲

[cpp] view plain copy print?在CODE上查看代碼片派生到我的代碼片

  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?在CODE上查看代碼片派生到我的代碼片

  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


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