深入了解 Windows 句柄






3.13/5 (14投票s)
2004年12月20日
8分钟阅读

135200

1303
本文介绍 Windows 句柄的一些内部工作原理和行为。
引言
当 Charles Petzold 说窗口句柄是系统范围内的句柄时,很多人可能没有真正理解他这句话的含义。他的意思是,进程 A 可以找到它的窗口句柄,并通过某种方式将句柄值发送到进程 B,进程 B 一旦收到该句柄,就可以成功地对其执行 SendMessage (...)
或 PostMessage (...)
操作。我演示了两种实现此目的的技术。一种是使用文件映射,另一种则更直接。进程 A 获取一个句柄,并将其写入文件,然后进程 B 可以读取该文件,提取句柄,并成功地对其执行 SendMessage (...)
或 PostMessage (...)
操作。本文讨论了共享句柄的各种方法、共享句柄的含义以及哪些句柄可以共享……是的,并非所有句柄都可以共享。
共享句柄
句柄是 Windows OS 拥有的 32 位无符号值。通常是程序员使用 Windows 原生 API 来使用这些句柄。这也是我展示第二种技术的原因之一。如果你是一个注重细节的人,你会希望亲眼看到句柄被封送(marshaled),并确信 Windows 在我们不知道的情况下什么也没做。当 Windows 编程的初学者执行 FindWndow (…)
并对 FindWndow (..)
返回的窗口句柄执行 SendMessage (...)
或 PostMessage (..)
时,可能会产生这种疑问。他/她可能会认为,要获取窗口句柄,应该请求 Windows OS,并且 Windows OS 代表我们进行某种封送。在阅读完本文后,这种疑问应该会消除。有些句柄,例如线程句柄或事件对象句柄,其句柄值无法写入文件并提供给其他应用程序使用。(这里就需要使用 Windows 原生 API 将它们从一个进程封送到另一个进程。)其他一些句柄,例如设备上下文句柄(HDC
),即使有 API 的帮助,也无法直接从一个进程发送或封送到另一个进程。在所有句柄中,窗口句柄至高无上。它们能够直接发送,而无需任何封送,从一个进程发送到另一个进程,而无需使用 Windows 原生 API。这就是为什么 Charles Petzold 称它们为系统范围内的。
句柄的封送
让我们看看这些句柄在从一个进程发送到另一个进程时是如何工作的。
例如,线程句柄和事件对象句柄在其他进程中直接发送是没有意义的。我们需要封送它们。我们需要特别定制封送。我们将定制封送一些句柄,并将封送的句柄值写入文件/文件映射内核对象,其他进程可以从该文件/文件映射内核对象读取并对其进行操作。
BOOL DuplicateHandle ( HANDLE hSourceProcessHandle, // handle to the source process HANDLE hSourceHandle, // handle to duplicate HANDLE hTargetProcessHandle, // handle to process to duplicate to LPHANDLE lpTargetHandle, // pointer to duplicate handle DWORD dwDesiredAccess, // access for duplicate handle BOOL bInheritHandle, // handle inheritance flag DWORD dwOptions // optional actions );
这就是将句柄封送到目标进程的 API,lpTargetHandle
变量将被写入文件或放入文件映射内核对象中。
看看 hTargetProcessHandle
,是的,你是对的,这种复制是按进程复制的。为此,我还想补充一点,一个线程的线程句柄在它创建的子进程中是没有意义的。即使在那里,你也需要使用 DuplicateHandle (…)
API 来复制句柄。
在父进程和子进程之间,事情会更容易一些。你需要在 CreateProcesss (…)
API 的 bInheritHandles
参数中传递 TRUE
,以便子进程能够自动继承父进程的句柄。
BOOL CreateProcess ( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES pszThread, BOOL bInheritHandles, DWORD dwCreationFlags, PVOID pvEnvironment, PCTSTR pszCurrentDirectory, LPSTARTUPINFO pStartupInfo, PPROCESS_INFORMATION pProcessInformation );
注意: 线程 ID 和进程 ID 是系统范围的。
我们不能定制封送所有类型的句柄。有关可以定制封送的句柄的完整列表,请点击此链接。
关于项目
SendWindowHandle 项目包含了所有内容。只需运行它,它就会运行 ReceiveWindowHandle EXE。首先,点击 SendWindowHandle
对话框中的任何一个按钮,然后点击 ReceiveWindowHandle
对话框中对应的按钮,通过文本文件或文件映射对象接收句柄。该句柄可以在 ReceiveWindowHandle 对话框应用程序中使用。
一个可以提出的论点是,事件对象已经内置了跨进程共享它们的机制,那么为什么我们需要进行 DuplicateHandle (...)
?是的,我永远不会要求你对事件对象进行 DuplicateHandle
,将其放在这里是为了某种完整性。但如果你有一个未命名的事件对象,你肯定会使用 DuplicateHandle
API。
项目中使用的某些 MFC 内部机制
如果我不解释项目中使用的 Windows/MFC 内部机制,那将是骇人听闻的,因为它们占用了文章的重要部分。初学者可能会觉得有用。允许我在以下附录中介绍它们。
附录 (A)
在 SendWindowHandle 项目的最后,我编写了一个函数 KillReceiveDialog ()
。它应该很有趣。首先,SendWindowHandle EXE 拥有 ReceiveWindowHandle
的进程 ID。使用该进程 ID,它会获取你系统上所有线程的快照,当它定位到 ReceiveWindowHandle
进程时,它会向该线程发送一个 WM_CLOSE
消息,从而杀死该线程,因此 ReceiveWindowHandle
EXE 退出。
HANDLE WINAPI CreateToolhelp32Snapshot ( DWORD dwFlags, DWORD th32ProcessID );
此 API 获取进程、堆、模块以及进程使用的线程的快照。请参阅 MSDN 获取完整文档。这是一个非常实用的 API,用于扫描系统范围的线程、进程和模块。
注意:不喜欢使用 CreateToolhelp32Snapshot 的读者可以使用自己的方式来终止进程。
附录 (B)
深入了解处理来自 PostThreadmessage (...)
API 的消息
SendWindowHandle 项目中的一个函数是 RunMesageLoop (...)
,原因是:PostThreadMessage (…)
发送的消息与窗口无关。与窗口无关的消息无法由 DispatchMessage
函数分派。因此,如果接收线程处于模态循环中(如 MessageBox
或 DialogBox
所使用的),消息将会丢失。要在模态循环中拦截线程消息,请使用线程特定的钩子。
以上内容直接摘自微软网站。
由于我不想为此目的使用钩子,所以我做了如下操作:
在 MFC 体系结构中,注册的窗口过程始终是 DefWindowProc (…)
。MFC 通过安装 WH_CBT
钩子来捕获窗口创建,并通过 SetWindowLong (…)
替换窗口过程。
LONG SetWindowLong ( HWND hWnd, // handle of window int nIndex, // offset of value to set LONG dwNewLong // new value );
其中 nIndex
值是 GWL_WNDPROC
,dwNewLong
是一个全局 Afx API AfxWndProc (..)
。因此,现在所有消息都转到 AfxWndProc (..)
,AfxWndProc(..)
调用。
AfxCallWndProc (...)
,并在某个时候,AfxCallWndProc (..)
会进一步调用 CallWindowProc (..)
来让它处理未处理的消息。
LRESULT CallWindowProc ( WNDPROC lpPrevWndFunc, // pointer to previous procedure HWND hWnd, // handle to window UINT Msg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter );
lpPrevWndFunc
是 DefWindowProc(...)
,所以没问题,一切正常。
由于线程消息不会传递给它们的窗口过程,因此我们需要自己处理。好的,我写了类似这样的内容来获取由 PostThreadmessage (...)
发送的消息:
MSG msg; While (::GetMessage (&msg,0,0,0)) { if (msg.hWnd == NULL) { AfxCallWndProc (dlg,dlg->m_hWnd,WM_THRDMESSAGE,0,0); } DispatchMessage (&msg); }
但这里有一个问题,我无法进入消息循环,一旦调用 DoModal (…)
,它就已经存在了。
幸运的是,MFC 以几乎相同的方式实现了模态和非模态对话框。在两种情况下,它都会调用 CreateDialogInderict (...)
,这是创建非模态对话框的 Win32 API,并自己实现模态。所以有一个消息循环在里面。
尽管如此,我仍然无法进入已经运行的消息循环,而且我不想使用钩子,所以我自己替换了原始的消息循环。在 InitDialog (..)
中,我执行 PostMessage (WM_MESSAGE_LOOP)
(WM_MESSAGE_LOOP
是一个自定义消息),这会运行另一个消息循环。请看 RunMesageLoop (WPARAM wParam, LPARAM lParam)
函数。现在,主消息循环停止了,我的自定义消息循环接管,并准备分派线程消息。
需要记住的一件事是,一旦这个消息循环终止(当用户关闭对话框时),主消息循环会在 OnCancel (..)
中启动。我必须终止那个消息循环,因此我在 OnCancel ()
中调用 PostMessage (WM_CLOSE);
,从而非常顺利地退出应用程序。所以,这样我们也可以定制封送消息。
在我做这一切的同时,微软团队也解决了这个问题,而微软一如既往地在它的帮助和支持网站上提供了一个完全不同的解决方案。读者也可以查看我在CodeGuru上发表的一篇文章,以更好地理解 MFC 中的消息映射。
结论
尽管本文是关于句柄的一些内部工作原理和行为,但让我们也来看看为什么我们需要在进程之间共享句柄。通常,当主应用程序由许多小型可执行文件组成时,这些小型可执行文件可能需要获取主可执行文件中的一些句柄来进行操作。例如,主应用程序可能只是一个用户界面,而依赖的小型可执行文件可能是执行后台处理的程序。曾经有一个案例,我有一个可执行文件运行,只是为了确保它在主应用程序意外退出或崩溃时能够正确清理(临时文件)。我曾有一个可执行文件监控应用程序的一个线程;如果线程崩溃,它将导致应用程序崩溃;然后这个可执行文件将从头开始重新启动应用程序。否则,例如,如果大量数据必须从一个 exe 传递到另一个 exe 进行数据处理。我们需要将指向数据的未命名文件映射对象句柄跨进程发送。