總是有新入門的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.句柄結構
- #ifdef STRICT
- typedef void *HANDLE;
- #define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name
- #else
- typedef PVOID HANDLE;
- #define DECLARE_HANDLE(name) typedef HANDLE name
- #endif
- typedef HANDLE *PHANDLE;
- #if !defined(_MAC) || !defined(GDI_INTERNAL)
- DECLARE_HANDLE(HFONT);
- #endif
- DECLARE_HANDLE(HICON);
- #if !defined(_MAC) || !defined(WIN_INTERNAL)
- DECLARE_HANDLE(HMENU);
- #endif
- DECLARE_HANDLE(HMETAFILE);
- DECLARE_HANDLE(HINSTANCE);
- typedef HINSTANCE HMODULE; /* HMODULEs can be used in place of HINSTANCEs */
- #if !defined(_MAC) || !defined(GDI_INTERNAL)
- DECLARE_HANDLE(HPALETTE);
- DECLARE_HANDLE(HPEN);
- #endif
- DECLARE_HANDLE(HRGN);
- DECLARE_HANDLE(HRSRC);
- DECLARE_HANDLE(HSTR);
- DECLARE_HANDLE(HTASK);
- DECLARE_HANDLE(HWINSTA);
- DECLARE_HANDLE(HKL);
- typedef struct HMENU__
- {
- int unused;
- } *HMENU;
- struct
- {
- int pointer; //指針段
- int count; //內核計數段
- int attribute; //文件屬性段:SHARED等等
- int memAttribute; //內存屬性段:MOVABLE和FIXED等等
- ...
- };
- //GMEM_FIXED
- hGlobal = GlobalAlloc(GMEM_FIXED, (lstrlen(szBuffer)+1) * sizeof(TCHAR));
- pGlobal = GlobalLock(hGlobal);
- lstrcpy(pGlobal, szBuffer);
- _tprintf(TEXT("pGlobal和hGlobal%s\n"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));
- GlobalUnlock(hGlobal);
- _tprintf(TEXT("使用句柄當做指針訪問的數據爲:%s\n"), hGlobal);
- GlobalFree(hGlobal);
- pGlobal和hGlobal相等
- 使用句柄當做指針訪問的數據爲:Test text
- //GMEM_MOVABLE
- hGlobal = GlobalAlloc(GMEM_MOVEABLE, (lstrlen(szBuffer)+1) * sizeof(TCHAR));
- pGlobal = GlobalLock(hGlobal);
- lstrcpy(pGlobal, szBuffer);
- _tprintf(TEXT("pGlobal和hGlobal%s\n"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));
- _tprintf(TEXT("使用句柄當做指針訪問的數據爲:%s\n"), hGlobal);
- GlobalUnlock(hGlobal);
- GlobalFree(hGlobal);
- pGlobal和hGlobal不相等
- 使用句柄當做指針訪問的數據爲:?
那麼總結來說,就是下面一幅圖了