原文:http://www.blogcn.com/User8/flier_lu/blog/24143356.html
Yun Jin 的 blog 上最近有两篇有趣的文章,介绍了 CLR 中线程概念的内部实现以及缺省提供的特殊线程。
Thread, System.Threading.Thread, and !Threads (I)
Special threads in CLR
其中提到 EE 在启动时会初始化一个专用的调试线程。
2. Debugger helper thread. As its name suggests, this thread helps interop debugger to get information of the managed process and to execute certain debugging operations. The thread is created when EE initializes debugger during start up. In Rotor, the thread proc for this thread is DebuggerRCThread::ThreadProcStatic (debug\ee\Rcthread.cpp). Also see Mike Stall's blog about impact of this helper thread。
与传统的 Native Win32 程序不同,CLR 对调试的支持是通过 In-Proc 模式提供的。Mike Stall 在其 blog 上介绍了这种模式的优劣:
Implications of using a helper thread for debugging
首先,我们来看看运行时的调试支持情况。
用 windbg 启动一个 CLR 程序后,可以用 ~ 命令和 sos 的 ~threads 命令,看看 Native 线程和 CLR 线程的对应情况如下:
|
可以看到正如 Yun Jin 所说,除了与主线程 (ID:1fbc) 和 Finalizer 线程 (ID:1e64) 对应的 Native 线程外,还有两个 (ID: 1fc4, 1e14) Native 线程。
用 ~* 命令查看线程的详细情况如下:
|
其中线程 (ID: 1e14) 是 WinDbg 调试用,可以暂且不记。另外一个线程 (ID: 1fc4) 就是我们的目标:调试支持线程。用 sysinternals 的 Process Explorer 可以查看其运行时堆栈如下:
|
在大致了解了其运行时状况后,我们来看看实际的实现方法。
在 《用WinDbg探索CLR世界 [2] 线程》 一文中曾介绍过 CLR 初始化 EE (Execute Engine) 的步骤。通过 CoInitializeEE -> TryEEStartup -> EEStartup 的调用,最终由 EEStartup 函数 (vm\ceemain.cpp:206) 完成实际的初始化工作。
EEStartup 函数会在完成最基本的初始化工作,如启动 IPC 引擎后;在建立任何 EE 线程之前,初始化调试服务:
|
因为 CLR 的调试支持依赖于基于 IPC 的 Notify 机制,因此在初始化调试支持之前,需要先完成对 IPC 和 Notify 服务的支持。
Notify 服务本质上就是一块全局的共享内存块,其 Section 在 Rotor 中名称为 ROTORSvcEventQueue,在 .NET 1.1 中名称为 CORSvcEventQueue。通过这块共享内存,CLR 会维护一个 ServiceEventBlock 类 (clr/src/inc/corsvcpriv.h:70),提供跨进程一级的消息队列机制。这个内存块会由一个独立的服务进程维护,负责对 CLR 启动和服务停止等事件进行分发。如果此全局服务存在,NotifyService 函数会在队列中现有事件已经分发完毕后,构造一个类型为 runtimeStarted 的 ServiceEvent 提交给它,以通告 CLR 正在启动。通过这个机制可以完成一些非常有趣的功能,但因为不涉及此次讨论的主体,暂且放下回头有空专门写文章讨论。
负责初始化调试器支持的 InitializeDebugger 函数 (vm/ceemain.cpp:1868) 主要负责构造调试器对象和初始化调试器接口。InitializeDebugger 函数及其相关伪代码如下:
|
首先调用 EEDbgInterfaceImpl::Init 函数 (vm/eedbginterfaceimpl.h:53) 构造面向外部调试器的 EE 调试接口,并存储在全局变量 g_pEEDbgInterfaceImpl 中。
然后调用 CorDBGetInterface 函数 (debug/ee/debugger.cpp:130) 构造调试器 Debugger 类 (debug/ee/debugger.h:631) 的实例,并存储于全局变量 g_pDebugger 中。而 CLR 内部则通过通用保存全局变量 g_pDebugInterface 中的 DebugInterface 抽象类 (vm/dbginterface.h:34) 的指针使用此实例。
最后在 Debugger 构造成功后,调用其 Startup 方法启动调试器。
如果使用者通过 CLR 的 Host API 接口 ICorRuntimeHost,获取配置接口 ICorConfiguration,并设置调试线程控制接口 IDebuggerThreadControl,则 InitializeDebugger 函数还需要针对此接口做特殊处理。
主要工作包括调用 CorHost::RefreshDebuggerSpecialThreadList 函数 (vm/corhost.cpp:654) 更新特殊线程列表,避免调试器针对这些线程进行处理,不过貌似没有直接使用到此机制。
通过上面的分析我们可以知道,CLR 的调试支持机制实际上是分为两个层面的。
EEDbgInterfaceImpl 通过 facade 模式,将 EE 内部对调试所需功能的支持集成到一个 EEDebugInterface 接口 (vm/eedbginterface.h:55)。
Debugger 则通过实现 DebugInterface 抽象类的功能,将 ICorDebugInfo 接口 (inc/corinfo.h:1168) 暴露给最终的调试功能使用者。
EEDbgInterfaceImpl 的实现基本上是对 EE 内部对象和功能的调用,自身没有数据,因此这儿暂时不讨论。
Debugger 则负责向外部调试机制使用者提供接口,其 Debugger::Startup 方法 (debug/ee/debugger.cpp:513) 完成对调试环境的启动工作。
Debugger::Startup 方法的主要工作包括:
1.初始化多线程安全的调试堆
2.建立、初始化并启动调试控制线程 DebuggerRCThread
3.创建一组调试用 Event/Mutex 内核对象
4.创建并初始化用于枚举 appdomain 的 IPC 共享内存控制块
此外对于在调试器中启动 CLR 的情况,将做特殊处理。
伪代码大致如下:
对 Debugger 对象的调试堆来说,功能上就是为基础的 gmallocHeap 堆,增加了多线程安全的线程同步支持,避免因为并行使用调试接口造成内存分派上的问题。而 debugger.h 中通过重新定义全局 new/delete 操作符的方式,将 DebuggerRCThread 等对象创建的内存分派,都接管到调试堆中。代码如下:
|
|
而调试支持线程 DebuggerRCThread 的构造与初始化,基本上就是对其内部结构的处理。其中值得注意的是调试控制线程中,为 InProc/OutOfProc 两部分调试支持机制,各维护了一个 DebuggerIPCControlBlock 控制块结构,并维护了各部分的调试相关基础信息。对 InProc 这部分的信息,Init 方法会自行初始化;而 OutOfProc 部分的信息,则通过 IPCManager,在调试器 attach 的时候从外部进程获取。
这一小节我们了解了 CLR 调试支持机制的整体结构,下一节我们继续分析调试支持线程是如何处理各种相关事件的。
to be continue...