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

原创 2004年04月05日 22:56:00

http://flier_lu.blogone.net/?id=1370342

[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)。这几种线程缓冲池中线程的概念,我们以后章节讨论线程池时再详细讨论。

用WinDbg探索CLR世界 [6] AppDomain 的创建过程

用WinDbg探索CLR世界 [6] AppDomain 的创建过程    我们知道 CLR 中 Assembly 是在名为 AppDomain 的逻辑空间中被载入运行的,而 AppDomain 是介...
  • tanliyoung
  • tanliyoung
  • 2007年05月06日 18:51
  • 749

用WinDbg探索CLR世界2--线程

用WinDbg探索CLR世界 [2] 线程http://www.blogcn.com/user8/flier_lu/index.html?id=1370342&run=.0A2F3E7[2] 线程  ...
  • tanliyoung
  • tanliyoung
  • 2007年05月07日 11:13
  • 1277

用WinDbg探索CLR世界 [4] 方法的调用机制

  • zgqtxwd
  • zgqtxwd
  • 2008年04月24日 08:45
  • 167

用WinDbg探索CLR世界[1] - 安装与环境配置

http://flier_lu.blogone.net/?id=1270368    一直以来,我对CLR的分析都是基于MSDN、.NET Framework SDK自带文档和Rotor项目提供的源代...
  • flier_lu
  • flier_lu
  • 2004年04月05日 22:53
  • 1026

使用WinDbg —— .NET篇 (二)

WinDBG作为Microsoft的御用工具,其强大之处使我这等小辈难以望其项背,它设计了极其丰富的功能来支持各种调试任务,包括用户态调试、内核态调试、dump文件调试、远程调试等。其灵活性和可扩展性...
  • ecjtu_luowei
  • ecjtu_luowei
  • 2015年03月14日 16:09
  • 784

关于使用Windbg查看线程死锁问题

关于使用Windbg查看线程死锁问题
  • chenlycly
  • chenlycly
  • 2016年11月26日 14:38
  • 1697

WinDbg命令详解--线程

线程命令是以~开始,后面跟线程id(一个windbg从0开始的一个编号),或者.,#,*等,可和其他命令混合使用。 ~ 简洁地显示当前进程的所有线程, ~. 表示当前线程 ~# ...
  • u014161864
  • u014161864
  • 2014年03月17日 10:19
  • 3586

13.windbg-!runaway、~、|(控制进程、线程切换)

!runaway !runaway命令显示每个线程消费的时间 Bit 0 (0x1) 让调试器显示每个线程消耗的用户模式时间(user time),默认不加就是0x1 Bit 1 (0x2) 显...
  • hgy413
  • hgy413
  • 2012年05月14日 13:44
  • 5104

winDbg定位异常崩溃和线程死锁三步骤

第一步: 附件进程(Attach to process)或者直接打开执行进程,通过.dump /mf C:/dump.dmp保存dump文件;此时可以stop Debuging。 第二步:设置符合文件...
  • lv_yjie2011
  • lv_yjie2011
  • 2014年12月03日 22:28
  • 588

Windbg调试----多线程控制调试

在调试程序的时候,可能经常会有这样的需求,让一个线程在特定的时候才让其开始执行或者暂停执行。比如复杂的多线程导致死锁的问题,又或者多线程中的Race Condition 导致程序执行异常等。很多时候,...
  • CJF_iceKing
  • CJF_iceKing
  • 2017年09月03日 18:29
  • 332
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:用WinDbg探索CLR世界 [2] 线程
举报原因:
原因补充:

(最多只允许输入30个字)