首页/技术开发/内容

用WinDbg探索CLR世界 [2] 线程

技术开发2022-06-10 阅读()
[2] 线程

在配置好WinDbg之后,我们载入一个CLR程序并执行至CLR被载入,然后开始我们的CLR探索之旅。

首先,使用!threads命令看看当前CLR中有哪些线程正在执行



以下为引用:

0:004> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
PreEmptive GC Alloc Lock
ID ThreadOBJ State GC Context Domain Count APT Exception
0 6ec 0014e708 6020 Enabled 00000000:00000000 00148a90 0 STA
2 a68 00157618 b220 Enabled 00000000:00000000 00148a90 0 MTA (Finalizer)




前面5个计数器分别表示托管(managed)线程、未启动线程、后台线程、阻塞线程和僵死线程的数量。
下面的列表是当前托管线程的详细信息:第一个域是WinDbg的线程编号;ID是Win32线程ID;ThreadObj是线程的对象;State是一个标志位,以后再详细介绍;PreEmptive GC表示GC是否与此线程协作;GC Alloc Context是GC的相关信息;Domain是线程所在AppDomain;Lock Count是线程拥有锁的计数器;APT是线程类型,沿用COM中STA/MTA/NTA(netural)的概念;最后的Exception表示线程类型,除了普通的用户线程外还有finalizer、GC、Theadpool Worker和Threadpool Completion Port,其功能与名字相符。

我们可以在.NET Framework SDK的Tool Developers Guide\Samples\sos子目录下找到所有sos.dll支持命令的详细说明;在rotor的clr\src\tools\sos子目录下找到针对rotor系统的sos.dll的实现代码。这份源代码在功能上实现了与CLR正规发行版本基本上相同的功能,也是我们下面研究的主要目标之一。

其中Strike.cpp是sos功能命令的实现所在。每个sos的命令在strike.cpp中以一个函数实现,通过DECLARE_API宏定义函数参数。


以下为引用:

#define DECLARE_API(s) \
CPPMOD VOID \
s( \
HANDLE hCurrentProcess, \
HANDLE hCurrentThread, \
ULONG dwCurrentPc, \
ULONG dwProcessor, \
PCSTR args \
)




函数参数分别传入WinDbg正在调试的进程句柄、当前线程句柄、当前指令地址、处理器和命令行参数信息。函数内再对此信息进行处理,输出调试信息到WinDbg界面中。

让我们先看看Threads命令(strike.cpp:1237)的实现原理。

Threads函数首先从一个全局线程存储池中获取当前线程统计信息,并将之存储在一个结构并内打印统计值;然后调用GetThreadList函数(sos\util.cpp:2259)获取线程列表;对每个线程获取线程信息,并将之存储在一个结构内并打印线程详细信息;在打印线程信息时,会判断此线程的类型,并打印相关信息。

首先来看看全局线程存储池ThreadStore类(vm\threads.h:1998)的设计和使用思路。

CLR在启动时,会通过 CoInitializeEE 函数(vm\ceemain.cpp:1100)初始化一个执行引擎(Execute Engine),这儿的EE类似JVM的概念,实际上就是CLR的运行时环境。关于CLR的详细启动过程请参见笔者另外一篇文章《.Net平台下CLR程序载入原理分析》。
CoInitializeEE函数使用全局变量保障每个进程最多只有一个CLR环境;对没有构造CLR的进程,调用TryEEStartup函数(vm\ceemain.cpp:500)尝试初始化CLR。伪代码如下:


以下为引用:

HRESULT STDMETHODCALLTYPE CoInitializeEE(DWORD fFlags)
{
if(++g_RefCount <= 1 && !g_fEEStarted && !g_fEEInit)
{
g_EEStartupStatus = TryEEStartup(fFlags);
}
return SUCCEEDED(g_EEStartupStatus) ?
(SetupThread() ? S_OK : E_OUTOFMEMORY) : g_EEStartupStatus;
}




TryEEStartup函数则以异常安全策略包装EEStartup函数(vm\ceemain.cpp:206)完成实际的CLR启动工作。在EEStartup函数中会真正调用InitThreadManager函数(vm\Threads.cpp:2068)完成线程管理器的初始化工作。而InitThreadManager函数出了初始化TLS外,绝大部分工作是由实现ThreadStore类的Singleton模式的ThreadStore::InitThreadStore函数(vm\Threads.cpp:4345)实现的。其中保存全局唯一ThreadStore类实例的就是前面获取线程统计信息的全局线程存储池。


以下为引用:

ThreadStore *g_pThreadStore;

BOOL ThreadStore::InitThreadStore()
{
g_pThreadStore = new ThreadStore;

return (g_pThreadStore != NULL);
}





因此,ThreadStore类实际上是一个全局唯一的线程管理器,新增和终止一个CLR线程都需要在此存储中更新相关信息。此线程管理器除了维护一个当前线程列表的链表外,还维护了一套线程相关信息的统计值。前面Threads命令获取的几个统计值就是从此而来。而获取当前线程列表的GetThreadList函数(sos\util.cpp:2259),实际上也是直接从线程管理器的线程列表中获取每个线程对象的入口。

最后来看看线程信息的获取步骤。

每个线程Thread类(vm\Threads.h:544)的对象表示一个managed线程。此线程是一个逻辑上的线程,如果被启动则可能直接对应于一个系统的物理线程。而一个物理线程则无需绑定到一个被管理的逻辑线程上,物理线程却可以在多个AppDomain中共享以运行被调度到的被管理线程。此外每个被管理的线程必须有一个运行时环境(Contex),但不一定在一个确定的应用程序域(AppDomain)中。呵呵,搞糊涂了吧 :D 这里绕的几个弯子我以后再写篇详细的文章讨论好了 :P
被管理的线程除了可以获取当前线程ID和绑定到的物理线程ID外,还有一个ThreadState状态(vm\Threads.h:576)定义其当前运行情况。
对线程类型的判断逻辑,首先将线程与FinalizerThread(Finalizer)和GcThread(GC)两个全局变量指向的系统功能线程比较,判断是否是这两种特殊线程;然后根据线程状态的Thread::TS_ThreadPoolThread位是否被设置来判断是否在线程池中;如果在线程池中还要通过状态的Thread::TS_TPWorkerThread标志位进一步判断是否为工作者线程(Threadpool Worker),不是工作者线程则为完成端口线程(Threadpool Completion Port)。这几种线程缓冲池中线程的概念,我们以后章节讨论线程池时再详细讨论。



……

相关阅读