Windbg+Rotor:Managed Process中的各种Special Threads分析
这几天Oracle培训,数据库功力倒是没太大长进,倒是Debug,Windows架构和实现还有CLR的觉悟突飞猛进。
开篇前首先3ks下rick,他把他写的一票经典的文章都发到sscli.cnblogs.com团队里面来了。Rick可是我在看雪bbs上面久仰的大牛…由于写的文章时间在创建团队的时间之前,故需要翻到第一页才能看到rick的文章。
首先就从sscli中TLS预先定义的一个结构体说起了:
enum TlsThreadTypeFlag // flag used for thread type in Tls data
{
ThreadType_GC = 0x00000001,
ThreadType_Timer = 0x00000002,
ThreadType_Gate = 0x00000004,
ThreadType_DbgHelper = 0x00000008,
ThreadType_Shutdown = 0x00000010,
ThreadType_DynamicSuspendEE = 0x00000020,
ThreadType_Finalizer = 0x00000040,
ThreadType_ADUnloadHelper = 0x00000200,
ThreadType_ShutdownHelper = 0x00000400,
ThreadType_Threadpool_IOCompletion = 0x00000800,
ThreadType_Threadpool_Worker = 0x00001000,
ThreadType_Wait = 0x00002000,
};
这个枚举类型的数据结构,是表示的TLS初始化参数中的Thread的类型。
首先不说一个User App可以创建多少个不同类型的Thread,CLR中,当一个托管Process在启动之后,至少需要创建三种类型的Process:一个Main Thread用来启动CLR和执行托管代码。一个CLR Debugger Helper thread。主要用来提供调试services。For interop debuggers,just as visual studio和windbg之类。另外就是一个finalizer thread,用来完成对各种可达和不可达的Object的析构。
这三种thread,是一个托管Process启动之后最少需要创建的。另外,根据这个Process是干嘛的不同,还可能需要创建不同的threads来适应不同的功能。例如webform和winform,涉及到I/O,lock,synchronization,GC,timer以及ThreadPool的时候,都分别需要创建不同的threads。
Well,对于上面的thread,下面先来个简单的叙述先:
Finalizer Thread:ThreadType_Finalizer = 0x00000040
对于Finalizer Thread,主要是在EE在启动的时候,GC Heap启动的时候,由GC来创建的这个线程。
等等,找一小白鼠演示下先,这里,选择一个webform作为小白鼠,方便Threadpool的介绍和其余的一些Thread的演示:
namespace TestWebApp
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("test for web app");
}
}
}
Windbg Attach上aspnet_wp.exe这个Process:
0:007> !threads
ThreadCount: 9
UnstartedThread: 0
BackgroundThread: 8
PendingThread: 0
DeadThread: 1
Hosted Runtime: no
ID GC Domain APT Exception
1 1 Enabled 0016ab98 Ukn (Threadpool Completion Port)
7 2 Enabled 0016ab98 MTA (Finalizer)
8 3 Enabled 0016ab98 MTA (Threadpool Completion Port)
10 4 Enabled 0016ab98 Ukn
XX 6 Enabled 0016ab98 Ukn (Threadpool Worker)
12 5 Enabled 0016ab98 MTA (Threadpool Worker)
13 7 Enabled 0016ab98 MTA (Threadpool Completion Port)
16 8 Enabled 0016ab98 MTA (Threadpool Completion Port)
11 9 Enabled 0016ab98 MTA (Threadpool Worker)
这里为了显示方便,去掉了一些列。上面显示的托管Thread里面,第七个就是Finalizer Thread,用来析构some Objects用的。来看看其堆栈调用情况:
0:007> k
ChildEBP RetAddr
0132fcd8 7c92e9ab ntdll!KiFastSystemCallRet
0132fcdc 7c8094e2 ntdll!ZwWaitForMultipleObjects+0xc
0132fd78 7c80a075 kernel32!WaitForMultipleObjectsEx+0x12c
0132fd94 79f60b6a kernel32!WaitForMultipleObjects+0x18
0132fdb4 79f34fb4 mscorwks!SVR::WaitForFinalizerEvent+0x7a
0132fdc8 79ecb4a4 mscorwks!SVR::GCHeap::FinalizerThreadWorker+0x75
0132fdd8 79ecb442 mscorwks!Thread::UserResumeThread+0xfb
0132fe6c 79ecb364 mscorwks!Thread::DoADCallBack+0x355
0132fea8 79ed5e8b mscorwks!Thread::DoADCallBack+0x541
0132fed0 79ed5e56 mscorwks!ManagedThreadBase_NoADTransition+0x32
0132fedc 79f6fd87 mscorwks!ManagedThreadBase::FinalizerBase+0xb
0132ff14 79ecb00b mscorwks!SVR::GCHeap::FinalizerThreadStart+0xbb
0132ffb4 7c80b683 mscorwks!Thread::intermediateThreadProc+0x49
0132ffec 00000000 kernel32!BaseThreadStart+0x37
啊哈,从下往上看,第一个是调用OS的base Thread的初始化方法。在托管Process的创建中,任何一个thread的创建都是基于一个base OS Thread的。关于soft thread,hard thread和!threads的区别,可以参见我以前的文章。还是有很大区别的。Then,第二行调用的是intermediateThreadProc,这个是任何一个soft thread在创建之前都需要调用的一个方法:
DWORD __stdcall Thread::intermediateThreadProc(PVOID arg)
{
WRAPPER_CONTRACT;
m_offset_counter++;
if (m_offset_counter * offset_multiplier > PAGE_SIZE)
m_offset_counter = 0;
_alloca(m_offset_counter * offset_multiplier);
intermediateThreadParam* param = (intermediateThreadParam*)arg;
LPTHREAD_START_ROUTINE ThreadFcnPtr = param->lpThreadFunction;
PVOID args = param->lpArg;
delete param;
return ThreadFcnPtr(args);
}
这个是Thread类中的intermediateThreadProc方法,是一个softThread创建的时候需要调用的。注意,这里的Thread不是由ThreadPool来创建的。对于Thread Pool创建一个Hard thread的时候,ThreadPoolMgr也有一个相同的intermediateThreadProc方法:
DWORD __stdcall ThreadpoolMgr::intermediateThreadProc(PVOID arg)
{
WRAPPER_CONTRACT;
//多调用了一组宏,来进行完整性的Contract的检验的。
STATIC_CONTRACT_SO_INTOLERANT;
offset_counter++;
if (offset_counter * offset_multiplier > PAGE_SIZE)
offset_counter = 0;
_alloca(offset_counter * offset_multiplier);
intermediateThreadParam* param = (intermediateThreadParam*)arg;
LPTHREAD_START_ROUTINE ThreadFcnPtr = param->lpThreadFunction;
PVOID args = param->lpArg;
delete param;
return ThreadFcnPtr(args);
}
这个方法,主要是用来避免P4 cpu 上面的64kb/1mb的命名问题。在Enable了HyperThreading的时候,这个可以非常影响app的性能。
GCHeap在启动的时候,就启动了Finalizer Thread:
DWORD __stdcall GCHeap::FinalizerThreadStart(void *args)
这个方法的实现,就不具体说了,比较麻烦的说。乱七八糟的检查啊,初始化的东西比较多。同时也不是这里的重点。
Debugger Helper Thread(ThreadType_DbgHelper = 0x00000008)
还是上面的小白鼠,由于Thread太多了,一个一个的找起来不方便,就先用~*k命令把调用堆栈先都列了出来,找到了第四个线程是Debugger Helper Thread:
0:004> k
ChildEBP RetAddr
00dbfe38 7c92e9ab ntdll!KiFastSystemCallRet
00dbfe3c 7c8094e2 ntdll!ZwWaitForMultipleObjects+0xc
00dbfed8 7c80a075 kernel32!WaitForMultipleObjectsEx+0x12c
00dbfef4 79ed4b06 kernel32!WaitForMultipleObjects+0x18
00dbff54 79ed4a63 mscorwks!DebuggerRCThread::MainLoop+0xcf
00dbff84 79ed49a6 mscorwks!DebuggerRCThread::ThreadProc+0xca
00dbffb4 7c80b683 mscorwks!DebuggerRCThread::ThreadProcStatic+0x82
00dbffec 00000000 kernel32!BaseThreadStart+0x37
从这个地方,可以看到,是mscorwks这个模块启动的这个Thread。这种在Managed Process种植入调试线程的方式,就有一些good tips和一些bad tips了。
00dbff54 79ed4a63 mscorwks!DebuggerRCThread::MainLoop+0xcf
这一句表明,在这个helper Thread的大部分时间,是在执行一个loop循环来获取外部debugger的request的。当这个helper thread得到一个requets的时候,它就direct进入CLR的内部data structure,得到一系列的结果,然后返回结果到debugger。
Since是由托管代码启动的,这个debugger helper thread只能提供对于mixed mode或者是managed debugger的支持。而对于windbg这样的native debugger是不支持的。不过,如果如果windbg加载了sos或者是SIEExtPub或者是外部托管扩展调试模块的话,那就另行讨论了。
另外提一下调试线程的“in-process model”和“out-process model”两种调试模型的比较:
在CLR的Debug Service里面“in-Process Model”就意味着这个debugger help thread和ee thread一样运行在managed Process里面了,当然,ee Thread也是提供一部分调试信息的。
这样的好处是显而易见的,因为EE Thread也是提供一部分调试信息的,这样以来,helper Thread就可以重用EE的这一部分代码了。这样比out-of-process调试模型更加容易得获取了CLR的内部的data stracture。性能的提升也是不用说了的。还有一个好处,就是可以于GC Thread在一些问题的处理上面更加好的协作和共享信息了,譬如,在对lock和在对synchronization objects的操作上面。同一个process里面的两个thread总比不同process里面的线程交互起来方便吧。
当然,也是有bad tips的。由于一个helper debug thread只能存活在一个live process里面,这样,在使用dump分析问题的时候,helper debugger的功能特性代码就用不上鸟。What a pity..还有一个非常棘手的问题,由于inprocessmode的调试thread对于获取托管代码中的信息用起来比较方便,这样,在interop的时候,托管代码和native code交互起来之后,这整个调试模型就被破坏掉了,这也是为什么在vs 2003的时候,调试interop的时候经常死锁和特别慢的原因。
还有一个问题,在需要完全调试这个process,下了一个断点,所有的执行都中断的时候,这个thread还是运行着的。这样,在进行压力测试的时候,就带来了麻烦了….
额,还有一些问题,罗嗦起来就麻烦了扯远了没完没了了,打住打住。
在Rotor里面,对于这个helper debugger thread的实现,是藏在一个阴暗的角落了(debug/ee/rcthread.cpp):
/*static*/ DWORD WINAPI DebuggerRCThread::ThreadProcStatic(LPVOID)
{
// We just wrap the instance method DebuggerRCThread::ThreadProc
//为了保持一致性的宏的合同检查
WRAPPER_CONTRACT;
BEGIN_SO_INTOLERANT_CODE_NO_THROW_CHECK_THREAD_FORCE_SO();
//这个地方是上面的枚举类型了,设置clr的这个thread flag,标识其类型为调试线程。
ClrFlsSetThreadType(ThreadType_DbgHelper);
//写入调试日志。
LOG((LF_CORDB, LL_EVERYTHING, "ThreadProcStatic called\n"));
DebuggerRCThread* t = (DebuggerRCThread*)g_pRCThread;
//这句才是重点,又调用别的地方去了,不继续找了,查看堆栈
t->ThreadProc(); // this thread is local, go and become the helper
END_SO_INTOLERANT_CODE;
return 0;
}
ThreadPool threads and relative threads
这个是一个比较大的部分。ThreadPool Thread和一些由ThreadPool启动的相关的Threads。这些thread,并不是一个托管Process必须有的Thread,这取决于一个Host到宿主的进程如何使用CLR的特性和功能了。
还有,some of 这些thread type启动之后,更加CPU模式,所处理的工作和一些用户的配置文件,这些同类型的thread,可以只有一个,也可以有多个。例如以前提到的在多cpu的时候,如果GC运行在Server mode的话,一个cpu对应一个gc thread的,管理一个GC Heap和一个LOH。
啊哈,刚才在在代码里面找到了一个枚举类型的结构体:
enum ThreadpoolThreadType
{
WorkerThread,
CompletionPortThread,
WaitThread,
TimerMgrThread
};
这个枚举类型的结构体还是表明了很多信息的。^_^
这些由ThreadPool启动的thread的type就多了,主要有上面的四种类型:包括wait Thread,对应上面的Thread Type wait。Wait Thread用来处理同步等待,这种类型的Thread可以有多个。WorkerThread,这些worker thread是用来执行托管代码的,根据需要执行的功能的不同,可以有多个。Completion port threads,这些线程用来处理I/O和network Port相关的功能。Ms在Rotor1.1里面是没有这个东西的..2.0里面有不用说。
另外,和worker thread匹配的,还有一个叫做gate thread的thread。这个thread本身意义上面讲,也是ThreadPool创建的,但是却不属于ThreadPoolThreadType。因为,worker thread并不是由ThreadPool直接创建管理的,worker thread是由Gate Thread创建并且管理的。因为根据程序的功能不同,worker thread可以多种多样而且有很多。来个分层设计,我喜欢。最后一个就是timer thread了。这个是用来控制timer queue的,只能有一个。
具体的,就不一一介绍这些线程的实现和功能了,太多了。按照上面讲解的思路和方法,可以得到的挺清楚。如有疑问可以follow commets。
AppDomain Unload Helper Thread (ThreadType_ADUnloadHelper = 0x00000200)
这个Thread,是帮助AppDomain卸载用的。在CLR1.0中,如果需要将一个用户启动的AppDomain卸载的话,就会在System Domain中创建一个Worker Thread,用这个Thread来完成AppDomain的卸载工作。当目标AppDomain被卸载了之后,这个Thread就死掉了。而在Rotor里面,则是专门的用一个AppDomain Unload Helper Thread来完成这个工作的。关于AD的卸载步骤和细节,以后在研究这个问题。
GC Thread
根据托管Process是运行的workstation模式的gc还是server模式的gc,还有一种用叫做concurrent模式的GC,这种模式的GC在Rotor里面是没有的。
另外,还有一些RPC thread和一些LRPC threads。
最后的剩下的,很大一部分就是COM Threads。
额,到吃饭的点了,催吃饭去了,收尾收的有点急,虎头蛇尾的感觉…………后续吧。
lbq1221119@4/12/2008 11:52 AM
首发:sscli.cnblogs.com
posted on 2008-04-12 18:12 lbq1221119 阅读(3488) 评论(11) 编辑 收藏 举报