句柄是什么?Windows結構體里面句柄的作用
句柄是什么?在Windows中,句柄的存在就像指針的標識一樣,但這樣的答案顯示不是你們需要的。閑暇之余,筆者摘錄以下Windows句柄的解釋。用戶可以端著咖啡再看Windows結構體里面句柄的作用表述。
這里我列舉詞條中的關于句柄的敘述不當之處,至于如何不當先不管,繼續往下看就會明白:
句柄是什么?
Windows 之所以要設立句柄,根本上源于內存管理機制的問題—虛擬地址,簡而言之數據的地址需要變動,變動以后就需要有人來記錄管理變動,(就好像戶籍管理一樣),因此系統用句柄來記載數據地址的變更。
如果想更透徹一點地認識句柄,我可以告訴大家,句柄是一種指向指針的指針。
通常我們說句柄是Windows用來標識被應用程序所建立或使用的對象的唯一整數。這句話是沒有問題的,但是想把這句話對應到具體的內存結構上就做不到了。下面我們來詳細探討一下Windows中的句柄到底是什么。
一、虛擬內存結構
我們知道,CPU是通過尋址來訪問內存的。32位CPU的尋址寬度是 0~0xFFFFFFFF ,計算后得到的大小是4G,也就是說可支持的物理內存最大是4G。但在實踐過程中,碰到了這樣的問題,程序需要使用4G內存,而可用物理內存小于4G,導致程序不得不降低內存占用。
為了解決此類問題,現代CPU引入了 MMU(Memory Management Unit 內存管理單元)。
MMU 的核心思想是利用虛擬地址替代物理地址,即CPU尋址時使用虛址,由 MMU 負責將虛址映射為物理地址。MMU的引入,解決了對物理內存的限制,對程序來說,就像自己在使用4G內存一樣。
內存分頁(Paging)是在使用MMU的基礎上,提出的一種內存管理機制。它將虛擬地址和物理地址按固定大?。?K)分割成頁(page)和頁幀(page frame),并保證頁與頁幀的大小相同。這種機制,從數據結構上,保證了訪問內存的高效,并使OS能支持非連續性的內存分配。在程序內存不夠用時,還可以將不常用的物理內存頁轉移到其他存儲設備上,比如磁盤,這就是大家耳熟能詳的虛擬內存。
1、虛擬地址與物理地址需要通過映射,才能使CPU正常工作。
而映射就需要存儲映射表。在現代CPU架構中,映射關系通常被存儲在物理內存上一個被稱之為頁表(page table)的地方。
如下圖:
從這張圖中,可以清晰地看到CPU與頁表,物理內存之間的交互關系。
進一步優化,引入TLB(Translation lookaside buffer,頁表寄存器緩沖)。
由上一節可知,頁表是被存儲在內存中的。我們知道CPU通過總線訪問內存,肯定慢于直接訪問寄存器的。
為了進一步優化性能,現代CPU架構引入了TLB,用來緩存一部分經常訪問的頁表內容。
如下圖:
在中間加入了TLB。
2、為什么要支持大內存分頁?
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參數返回的句柄就是指向內存分配的內存塊的指針,不理解???接著看下面的句柄結構,你就明白了。
二、句柄結構
在上面講解虛擬內存結構的過程中,我們就引出了幾個問題:MOVABLE的內存訪問為什么不會亂,FIXED的內存為什么說就是指向分配內存塊的指針。
事實上我們盡管Windows沒有給出源碼,但是從一些頭文件、MSDN和Windows早期內存分配函數中我們還是可以一窺端倪。
在Winnt.h頭文件中做了通用句柄的定義:
01#ifdef STRICT02typedef void *HANDLE;03#define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name04#else05typedef PVOID HANDLE;06#define DECLARE_HANDLE(name) typedef HANDLE name07#endif08typedef HANDLE *PHANDLE;復制代碼#ifdef STRICTtypedef void *HANDLE;#define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name#elsetypedef PVOID HANDLE;#define DECLARE_HANDLE(name) typedef HANDLE name#endiftypedef HANDLE *PHANDLE;在Windef.h做了特殊句柄的定義:
01#if !defined(_MAC) || !defined(GDI_INTERNAL)02DECLARE_HANDLE(HFONT);03#endif04DECLARE_HANDLE(HICON);05#if !defined(_MAC) || !defined(WIN_INTERNAL)06DECLARE_HANDLE(HMENU);07#endif08DECLARE_HANDLE(HMETAFILE);09DECLARE_HANDLE(HINSTANCE);10typedef HINSTANCE HMODULE; /* HMODULEs can be used in place of HINSTANCEs */11#if !defined(_MAC) || !defined(GDI_INTERNAL)12DECLARE_HANDLE(HPALETTE);13DECLARE_HANDLE(HPEN);14#endif15DECLARE_HANDLE(HRGN);16DECLARE_HANDLE(HRSRC);17DECLARE_HANDLE(HSTR);18DECLARE_HANDLE(HTASK);19DECLARE_HANDLE(HWINSTA);20DECLARE_HANDLE(HKL);復制代碼#if !defined(_MAC) || !defined(GDI_INTERNAL)DECLARE_HANDLE(HFONT);#endifDECLARE_HANDLE(HICON);#if !defined(_MAC) || !defined(WIN_INTERNAL)DECLARE_HANDLE(HMENU);#endifDECLARE_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);#endifDECLARE_HANDLE(HRGN);DECLARE_HANDLE(HRSRC);DECLARE_HANDLE(HSTR);DECLARE_HANDLE(HTASK);DECLARE_HANDLE(HWINSTA);DECLARE_HANDLE(HKL);這里微軟把通用句柄HANDLE定義為void指針,顯然啦,他是不想讓人知道句柄的真實類型,但是和他以往的做法一樣,微軟空有一個好的想法結果沒有實現。馬上,如果定義了強制類型檢查STRICT,他又定義了特殊類型句柄宏DECLARE_HANDLE,這里用到了##,這是比較偏僻的用法,翻譯過來,對于諸如DECLARE_HANDLE(HMENU)定義其實就是
01typedef struct HMENU__02{03int unused;04} *HMENU;復制代碼typedef struct HMENU__{int unused;} *HMENU;到這里,你是不是覺得有一點眉目了呢,對,句柄是一種指向結構體的指針,結合這里的int unused定義很容易猜到結構體的第一個字段就是我們的邏輯地址(指針)。那么,是不是僅僅如此呢,當然不是?。。∮捎谥赶蚪Y構體指針可以強制截斷只獲取第一個字段,這里的struct結構體絕對不止一個字段,聯系我們在Windows中的編程經驗,對于線程HANDLE有計數那么必須有計數段,對于事件HEVENT等內核對象會要求指定屬性那么必須有屬性段,對于內存分配HANDLE有可移動和不可移動之說那么必須有內存可移動屬性段,等等。基于此我們可以大膽猜測Windows的句柄指向的結構類似如下
01struct02{03int pointer;//指針段04int count; //內核計數段05int attribute; //文件屬性段:SHARED等等06int memAttribute; //內存屬性段:MOVABLE和FIXED等等07...08};復制代碼struct{int pointer;//指針段int count; //內核計數段int attribute; //文件屬性段:SHARED等等int memAttribute; //內存屬性段:MOVABLE和FIXED等等...};事實上,Windows內存管理器管理的其實都是句柄,通過句柄來管理指針,Windows的系統整理內存時檢測內存屬性段,如果是可以移動的就能夠移動邏輯地址,移動完后將新的地址更新到對應句柄的指針段中,當要使用MOVABLE地址時的時候必須Lock住,這時候計數加1,內存管理器檢測到計數》0便不會移動邏輯地址,這時候才能獲得固定的邏輯地址來操作物理內存,使用完后Unlock內存管理器又可以移動邏輯地址了,到此MOVABLE的內存訪問為什么不會亂這個問題就解決了。
下面再說一說,FIXED的內存為什么說就是指向分配內存塊的指針。我們看上面的通用句柄定義,可以發現HANDLE的句柄定義一直是void指針,其他的特殊句柄在嚴格類型檢查的時候定義為結構體指針,為什么不把二者定義為一樣的呢。查看MSDN關于GlobalAlloc的敘述對于GMEM_FIXED類型“Allocates fixed memory. The return value is a pointer.”,這里返回的是一個指針,為了驗證這個說法,我寫了一小段程序
01//GMEM_FIXED02hGlobal = GlobalAlloc(GMEM_FIXED, (lstrlen(szBuffer)+1) * sizeof(TCHAR));03pGlobal = GlobalLock(hGlobal);04lstrcpy(pGlobal, szBuffer);05_tprintf(TEXT("pGlobal和hGlobal%sn"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));06GlobalUnlock(hGlobal);07_tprintf(TEXT("使用句柄當做指針訪問的數據為:%sn"), hGlobal);08GlobalFree(hGlobal);復制代碼//GMEM_FIXEDhGlobal = GlobalAlloc(GMEM_FIXED, (lstrlen(szBuffer)+1) * sizeof(TCHAR));pGlobal = GlobalLock(hGlobal);lstrcpy(pGlobal, szBuffer);_tprintf(TEXT("pGlobal和hGlobal%sn"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));GlobalUnlock(hGlobal);_tprintf(TEXT("使用句柄當做指針訪問的數據為:%sn"), hGlobal);GlobalFree(hGlobal);運行結果為
01pGlobal和hGlobal相等02使用句柄當做指針訪問的數據為:Test text復制代碼pGlobal和hGlobal相等使用句柄當做指針訪問的數據為:Test text對比使用GMEM_MOVABLE程序為
01//GMEM_MOVABLE02hGlobal = GlobalAlloc(GMEM_MOVEABLE, (lstrlen(szBuffer)+1) * sizeof(TCHAR));03pGlobal = GlobalLock(hGlobal);04lstrcpy(pGlobal, szBuffer);05_tprintf(TEXT("pGlobal和hGlobal%sn"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));06_tprintf(TEXT("使用句柄當做指針訪問的數據為:%sn"), hGlobal);07GlobalUnlock(hGlobal);08GlobalFree(hGlobal);復制代碼//GMEM_MOVABLEhGlobal = GlobalAlloc(GMEM_MOVEABLE, (lstrlen(szBuffer)+1) * sizeof(TCHAR));pGlobal = GlobalLock(hGlobal);lstrcpy(pGlobal, szBuffer);_tprintf(TEXT("pGlobal和hGlobal%sn"), pGlobal==hGlobal ? TEXT("相等") : TEXT("不相等"));_tprintf(TEXT("使用句柄當做指針訪問的數據為:%sn"), hGlobal);GlobalUnlock(hGlobal);GlobalFree(hGlobal);運行結果為
01pGlobal和hGlobal不相等02使用句柄當做指針訪問的數據為:?pGlobal和hGlobal不相等使用句柄當做指針訪問的數據為:?顯然,使用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時候有些代碼還不是那么完善,所以給了我們機會去挖掘潛在的內容。至于微軟苦心積慮不讓我們看到句柄的真實定義那是必然的,試想一下主要的內存對象結構都被摸清楚了,那么黑客們還不反了天了。
至此,用戶應該明白句柄是什么了吧,其實在不同的領域中,句柄的作用都是差不多的,可以作為一個標識的作用。
相關文章: