进程间同步的案例研究






4.25/5 (4投票s)
演示进程同步和进程间通信的应用程序。
引言
当这个新网站出现时——这是一个绝佳的举措——我决定通过编写一个“小型”示例项目来支持它,该项目试图回答许多关于进程间通信的问题。后来,越来越多的功能被添加进来,直到您今天看到 IPS 现在的样子。
进程间通信
它将向您展示
- 如何通过 `CreateProcess` 调用启动子进程(基于 Joseph M. Newcomer 的代码,我已移除资源泄漏)。
- 如何通过进程句柄来同步(在此情况下,“等待进程结束”)。
- 如何“尝试”干净地结束一个进程(如果不行,就干掉那个罪犯)。
- 如何创建一个带有线程框架解决方案的窗口,用于监视进程。
除了关于进程信息检索的一些高级功能(这些功能是通过调用未文档化的 API 和读取 NT/W2K 系统表获得的),大多数关于进程间同步的事情都相当简单。
本文中我们演示的发现或“技术”基于 Sven B. Schreiber 的工作,该工作发表在 1999 年 11 月的 Dr. Dobb's Journal #305 [ (c) 1999 Miller Freeman, Inc., San Francisco (CA) ] 上。 (www.ddj.com)。
在“Win32Ext”文件中实现了一些 C 函数,用于以操作系统无关的方式(此处仅涉及 MS OS)检索进程信息。更有趣的函数包括:
DWORD WINAPI GetProcessModuleName(DWORD dwProcessId, PWORD p_UnicodeString, DWORD dwMaxLength);
DWORD WINAPI GetParentProcessId(DWORD dwProcessId, PDWORD p_dwParentPId);
DWORD WINAPI GetChildProcesses (DWORD dwProcessId, PDWORD p_dwChildPIds, DWORD dwMaxLength, PDWORD p_dwNrChildren);
DWORD WINAPI GetNumberOfProcesses(PDWORD p_dwNrProcesses);
DWORD WINAPI GetProcessCommitCharge(DWORD dwProcessId, PDWORD p_dwCommitCharge);
DWORD WINAPI GetProcessUserTime(DWORD dwProcessId, PLARGE_INTEGER p_UserTime);
DWORD WINAPI GetProcessKernelTime(DWORD dwProcessId, PLARGE_INTEGER p_KernelTime);
DWORD WINAPI GetProcessThreadIds(DWORD dwProcessId, PDWORD p_dwThreadIds, DWORD dwMaxLength, PDWORD p_dwNrThreads);
DWORD WINAPI GetProcessCreationTime DWORD dwProcessId, PSYSTEMTIME p_SystemTime);
DWORD WINAPI GetProcessBasePriority(DWORD dwProcessId, PDWORD p_dwBasePriority);
通过消息分发进行的进程间通信(例如,用于礼貌地请求应用程序停止)是通过向进程窗口发送消息来完成的,该功能基于 Martin-Pierre Frenette 发布的代码。(仅凭进程句柄向另一个应用程序的主框架窗口发送消息)。
您还会注意到,有些程序**不遵循**标准的父子进程规则:如果您从“IPS.exe”中启动“Explorer.exe”,您会发现 explorer 进程**不是** IPS 的子进程。
进程对象和进程调试器对象
我重写了原始的 IPS 代码,使其具有一定的结构(坦白说,它很糟糕的 C++,即使现在也是如此……)。所以我想到了 `ProcessObject` 类。它提供了一种简单的方式来处理您对进程想要做的绝大多数事情。提供了启动和终止进程、更改其基本优先级、向其发送消息(如果它有窗口)等功能……
另外,……您可以通过使用 `ProcessDebugger` 对象附加到目标进程并将其作为调试器。
进程调试器通过 C++ 回调机制接收以下事件的通知:
void OnException(DebugEvent_Exception* pDE); void OnProcessStart(DebugEvent_Process* pDE); void OnProcessExit(DebugEvent_Process* pDE); void OnThreadStart(DebugEvent_Thread* pDE); void OnThreadExit(DebugEvent_Thread* pDE); void OnDllLoad(DebugEvent_DLL* pDE); void OnDllUnload(DebugEvent_DLL* pDE); void OnDebugOutput(DebugEvent_Output* pDE);
各自的 `DebugEvent` 对象指针在事件期间提供了所有必要的信息。让调试函数正常工作可能很容易出错,所以如果您想了解 VC 调试器是如何工作的,可以参考一下。
进程调试器还有一个巧妙的线程堆栈转储器对象(通过聚合),当您想知道异常的确切上下文时,它会很有用。我在 Felix Kaska 的 Win32 页面(http://mvps.org/win32/)上找到了我的堆栈转储功能示例。我(几乎完全)重写了该功能,并将其放入相应的 `StackFrame` 和 `ThreadFunctionStack` 类中。它提供了在进程内转储线程堆栈的功能,可能作为对异常的响应(但不仅限于此),并且还可以转储另一个进程中线程的堆栈(当然,前提是您拥有必要的 DEBUG 权限)。调试符号(如果可用)仅在需要时加载。软件工程师需要决定是否要在可执行文件中包含最少量的调试信息。如果链接步骤生成了“.map”文件,则堆栈转储信息与 map 文件结合应该足以弄清楚函数上下文,即使可执行文件中没有包含调试信息。(如果不确定如何添加和选择调试信息类型,请阅读 MSDN)。如果可执行文件中提供了更多的调试符号信息(除了寄存器和指令指针),它将被使用(通过 IPS 和 ImageHlp dll)并记录下来,这样更容易弄清楚出了什么问题。
跟踪
为了内部错误跟踪,IPS 使用 `TraceLogger` 类,它可以将函数(以及函数返回)、错误和未处理的异常记录或跟踪到日志文件中。通过使用宏,我们可以将额外的信息放入日志文件中,以便在现场更容易地进行调试任务。如果需要,日志文件可以分成独立的部分(用于文件使用轮换目的,因此日志记录器使用的最大文件大小是有限的),可以使用例如 CryptLib 或内部开发的流加密算法进行加密。IPS 不加密日志文件,因为源代码对所有人可见。如果发生未处理的异常,IPS 自然会在终止之前转储其自身有问题的线程的堆栈。(如果您愿意,请在发生这种情况时将日志文件发送给我。)
跟踪级别可以在运行时调整(请参阅 IPS 的关于框)。
您也可以通过将宏重新定义为空语句(并重新编译应用程序)来删除一些函数跟踪。
LOGTRACE(l, s) LOGTRACEFUNC(s) LOGTRACEERROR(e, s) LOGTRACEFUNCRETURN(e, s) LOGTRACEDUMPSTACK() LOGTRACEOSTREAM(s) LOGTRACEPROGRESSWITHOSTREAM()
实时调试输出窗口
为了记录调试事件,我使用了一个 Ben Ashley 制作的 `TOutputWnd` 的略微修改版本。原始版本可以在 CodeGuru(www.codeguru.com)上找到。IPS 用户可以将日志文件转储为文本格式,用于向支持部门或崩溃程序的开发者指出有问题的代码。(真是的!)
未来增强功能
系统范围挂钩 `CreateProcess`(和 `CreateThread`)调用,以提高性能,而不是使用简单的计时器。
只是想知道在程序崩溃时是否需要转储所有线程……
提供真正的 UNICODE 兼容代码。
其他(有什么建议吗?)
关于所用编译器的说明
我使用了 Intel 5.0 编译器的评估版本,该版本为 Intel PI/PII/PIII 系列生成(更大)更快且自动优化的代码。它无缝集成到 MS VC 开发环境中,并且与 MS VC 编译器完全(?)兼容。
Gert Boddaert, 别名 GBO。