分析调用阻塞问题

当进程在直接或间接地执行 RPC 调用时,如果持有关键节或资源,就会出现常见问题。 在这种情况下,RPC 调用将转到另一个进程或计算机,并调度到管理器例程(服务器例程),然后就会挂起或耗时过长。 这会导致原始调用方遇到关键节超时。

通过调试器进行检查时,RPC 位于拥有关键节的线程堆栈之上,但不清楚它正在等待的内容。

下面是此类堆栈的一个示例。 可以有很多变化。

0:002> ~1k
ChildEBP RetAddr
0068fba0 77e9e8eb ntdll!ZwWaitForSingleObject+0xb
0068fbc8 4efeff73 KERNEL32!WaitForSingleObjectEx+0x5a
0068fbe8 4eff0012 RPCRT4!UTIL_WaitForSyncIO+0x21
0068fc0c 4efe6e2b RPCRT4!UTIL_GetOverlappedResultEx+0x44
0068fc44 4ef973bf RPCRT4!WS_SyncRecv+0x12a
0068fc68 4ef98d5a RPCRT4!OSF_CCONNECTION__TransSendReceive+0xcb
0068fce4 4ef9b682 RPCRT4!OSF_CCONNECTION__SendFragment+0x297
0068fd38 4ef9a5a8 RPCRT4!OSF_CCALL__SendNextFragment+0x272
0068fd88 4ef9a9cb RPCRT4!OSF_CCALL__FastSendReceive+0x165
0068fda8 4ef9a7f8 RPCRT4!OSF_CCALL__SendReceiveHelper+0xed
0068fdd4 4ef946a7 RPCRT4!OSF_CCALL__SendReceive+0x37
0068fdf0 4efd56b3 RPCRT4!I_RpcSendReceive+0xc4
0068fe08 01002850 RPCRT4!NdrSendReceive+0x4f
0068ff40 01001f32 rtclnt+0x2850
0068ffb4 77e92ca8 rtclnt+0x1f32
0068ffec 00000000 KERNEL32!CreateFileA+0x11b

下面介绍如何解决此问题。

逐步排除调用阻塞问题

  1. 确保调试器正在调试拥有阻塞单元的进程。 (这是包含疑似在 RPC 中挂起的客户端线程的进程。)

  2. 获取此线程的堆栈指针。 堆栈将类似于前述示例中所示的堆栈。 在此示例中,堆栈指针为 0x0068FBA0。

  3. 获取此线程的调用信息。 为此,请使用带有线程堆栈指针的 !rpcexts.rpcreadstack 扩展作为其参数,如下所示:

0:001> !rpcexts.rpcreadstack 68fba0
    CallID: 1
    IfStart: 19bb5061
    ProcNum: 0
            Protocol Sequence:      "ncacn_ip_tcp"  (Address: 00692ED8)
            NetworkAddress: ""      (Address: 00692F38)
            Endpoint:       "1120"  (Address: 00693988)

此处显示的信息将允许您跟踪调用。

  1. 网络地址为空,表示本机。 终结点为 1120。 需要确定托管此终结点的进程。 未完成此操作,可以将此终结点编号传递给 !rpcexts.getendpointinfo 扩展,如下所示:
    0:001> !rpcexts.getendpointinfo 1120
    Searching for endpoint info ...
    PID  CELL ID   ST PROTSEQ        ENDPOINT
    --------------------------------------------
    0278 0000.0001 01            TCP 1120
  1. 从上述信息可以看出,该进程 0x278 包含此终结点。 可以使用 !rpcexts.getcallinfo 扩展来确定此进程是否知道有关此调用的任何内容。 此扩展需要四个参数:CallIDIfStart ProcNum(可在步骤 3 中找到),以及 0x278 的 ProcessID
    0:001> !rpcexts.getcallinfo 1 19bb5061 0 278
    Searching for call info ...
    PID  CELL ID   ST PNO IFSTART  TIDNUMBER CALLFLAG CALLID   LASTTIME CONN/CLN
    ----------------------------------------------------------------------------
    0278 0000.0004 02 000 19bb5061 0000.0002 00000001 00000001 00072c09 0000.0003
  1. 步骤 5 中的信息非常有用,但有些简略。 第二列中的单元格 ID 为 0000.0004。 如果将进程 ID 和此单元格号传递给 !rpcexts.getdbgcell 扩展,您将看到此单元格的更清晰显示:
    0:001> !rpcexts.getdbgcell 278 0.4
    Getting cell info ...
    Call
    Status: Dispatched
    Procedure Number: 0
    Interface UUID start (first DWORD only): 19BB5061
    Call ID: 0x1 (1)
    Servicing thread identifier: 0x0.2
    Call Flags: cached
    Last update time (in seconds since boot):470.25 (0x1D6.19)
    Owning connection identifier: 0x0.3

这表明调用处于“已调度”状态,意味着已离开 RPC 运行时。 上次更新时间为 470.25。 可以使用 !rpcexts.rpctime 扩展了解当前时间:

    0:001> !rpcexts.rpctime
    Current time is: 6003, 422

这表明,该通话的最后一次联系大约为 5533 秒之前,即大约 92 分钟前。 因此,这一定是调用阻塞。

  1. 在将调试器附加到服务器进程之前的最后一步,可以使用服务线程标识符隔离当前应为调用提供服务的线程。 这是另一个单元格号;它在步骤 6 中显示为“0x0.2”。 可以按如下所示使用它:
    0:001> !rpcexts.getdbgcell 278 0.2
    Getting cell info ...
    Thread
    Status: Dispatched
    Thread ID: 0x1A4 (420)
    Last update time (in seconds since boot):470.25 (0x1D6.19)

现在,您知道当前正在进程 0x278 中查找线程 0x1A4。

线程可能正在进行另一个 RPC 调用。 如有必要,可以通过重复此过程来跟踪此调用。

注意此过程演示在已知客户端线程的情况下如何查找服务器线程。 有关反向技术的示例,请参阅从服务器线程识别调用方