我已经上传了一个WinDBG会话的日志,我将参考:https://pastebin.com/TvYD9500
因此,我正在调试一个客户报告的挂起。复制程序是一个小型C#程序:
using System;
using System.Data.Odbc;
using System.Threading;
namespace ConnectionPoolingTest
{
class Program
{
static void Main(string[] args)
{
String connString = "DSN=DotNetUltraLightDSII";
using (OdbcConnection connnection = new OdbcConnection(connString))
{
connnection.Open();
connnection.Close();
}
}
}
}我们出售一个框架来构建ODBC驱动程序,客户正在测试用该框架构建的ODBC驱动程序。其中一个可能相关的细节是,它们使用允许用C#编写业务逻辑的组件,而该组件是用C++/CLI编写的,以连接我们的本机代码&客户代码(因此,ODBC驱动程序DLL是一个向ODBC驱动程序管理器公开C接口的混合模式DLL )。
(如果需要的话,我也可以上传驱动程序二进制文件。)
在此复制器(必须在所使用的DSN上启用连接池的情况下运行)中发生的情况是,进程以一个具有如下所示堆栈的线程挂起:
RetAddr : Args to Child : Call Site
000007fe`fcea10dc : 00000000`00470000 00000000`770d0290 00000000`00000000 00000000`009ae8e0 : ntdll!ZwWaitForSingleObject+0xa
000007fe`f0298407 : 00000000`00999a98 00000000`770d5972 00000000`00000000 00000000`00000250 : KERNELBASE!WaitForSingleObjectEx+0x79
000007fe`f0294d04 : 00000000`00999a98 00000000`00a870e0 00000000`00999a68 00000000`00991a10 : comsvcs!UTSemReadWrite::LockWrite+0x90
000007fe`f0294ca8 : 00000000`00999a68 00000000`00999a98 00000000`00999a20 00000000`7717ba58 : comsvcs!CDispenserManager::~CDispenserManager+0x2c
000007fe`f02932a8 : 00000000`00999a20 00000000`00a871c0 00000000`77182e70 00000000`7717ba58 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::`scalar deleting destructor'+0x68
000007fe`f0293a00 : 000007fe`f0290000 00000000`00000001 00000000`00000001 00000000`00a87198 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::Release+0x20
000007fe`f02949aa : 00000000`00000000 00000000`00a87188 00000000`00992c20 00000000`00992c30 : comsvcs!ATL::CComModule::Term+0x35
000007fe`f0293543 : 00000000`00000000 00000000`00a87190 00000000`00000001 00000000`00a87278 : comsvcs!`dynamic atexit destructor for 'g_ModuleWrapper''+0x22
000007fe`f029355b : 00000000`00000001 00000000`00000000 000007fe`f0290000 00000000`76f515aa : comsvcs!CRT_INIT+0x96
00000000`7708778b : 000007fe`f0290000 00000000`00000000 00000000`00000001 00000000`7717ba58 : comsvcs!__DllMainCRTStartup+0x187
00000000`7708c1e0 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrShutdownProcess+0x1db
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x90
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d我找到了一些UTSemReadWrite类的源代码(但它似乎与我实际运行的有点不同):https://github.com/dotnet/coreclr/blob/616fea550548af750b575f3c304d1a9b4b6ef9a6/src/utilcode/utsem.cpp
将一个断点放入UTSemReadWrite::LockW区,我能够调试挂起的最后一个调用,发现原因是m_dwFlag (用于原子性的)不是零,所以它会等待一个事件(当它释放它时,拥有一个线程来向它发出信号),并且它通过调用UTSemReadW区::GetWriteWaiterEvent来完成这个任务,但是这个调用会创建事件(并且在这里没有其他线程.),然后等待它。砰,僵局。
通过程序集调试,我可以推断出m_dwFlag在对象中偏移了4个字节,并将一个断点放入UTSemReadW区::UTSemReadW区,我能够获得挂起的UTSemReadWrite实例的地址,并在m_dwFlag上放置一个数据断点。
这样做,我可以看到,一个线程函数comsvcs!PingThread调用comsvcs!UTSemReadW区::LockRead(并可能得到了锁),然后在调用comsvcs之前被杀死!UTSemReadW区::UnlockRead。我以前见过这样的情况,它是由一个未处理的SEH异常杀死PingThread引起的,但是这个应用程序防止使用setunhandledexceptionfilter()崩溃,所以我想可能有一些异常正在杀死线程,但原来是CLR本身!
RetAddr : Args to Child : Call Site
00000000`7708c198 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!ZwTerminateProcess+0xa
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x48
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d(这就引出了一个问题;那么ntdll!ZwTerminateProcess实际上不会终止这个进程?因为它显然会被返回并调用atexit处理程序..。我想这是一个不同的函数,同名?https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntddk/nf-ntddk-zwterminateprocess)
所以,我的问题是,我是否正确地解释了调试器向我展示了什么?这实际上是CLR中的一个bug吗?CLR不应该首先优雅地结束线程吗?
客户注意到,如果他将驱动程序中的一个线程作为后台线程创建,那么挂起就不会发生,这很奇怪,因为在卸载驱动程序时,即使是前景线程(通过在驱动程序的句柄上调用SQLFreeHandle()的终结器)也应该很快停止,除非终结器线程被什么东西减慢了,我猜?
发送给我们的复制器驱动程序中的后台线程基本上是
public Driver()
{
this.tokenSource= new CancellationTokenSource();
this.token = this.tokenSource.Token;
this.worker= new Thread(this.DoWork) { IsBackground = false };
this.worker.Start();
}
public override void Dispose()
{
this.tokenSource.Cancel();
this.worker.Join();
this.tokenSource.Dispose();
base.Dispose();
}
private void DoWork() {
while (!this.token.WaitHandle.WaitOne(200)) {
log(this.Log, "Doing some work....");
}
log(this.Log, "Done with work.");
}并正确调用Dispose()并退出。
我不知道下一步该怎么做。
编辑:在阅读了这之后,我觉得这是CLR的一个bug/“怪癖”。在我的场景中,最后一个前台.NET线程位于ODBC驱动程序中。当ODBC驱动程序管理器调用SQLFreeHandle卸载驱动程序(从windows线程池中的某个线程或驱动程序管理器本身拥有的某个线程(不确定))时,这将导致驱动程序终止最后一个前景线程。根据我对从那篇文章中获得的CLR关闭过程的理解,CLR将在它获得实际返回的机会之前终止调用SQLFreeHandle的线程,这是预期的行为。
但是该线程似乎持有UTSemReadWrite锁,因此稍后在atexit处理期间它将陷入死锁。
如果这实际上是CLR的错误,那么我关于如何解决这个问题的唯一想法是在对.NET的最后调用中启动另一个(前台) SQLFreeHandle线程,这个线程在超时后会自动结束(希望足够长的时间让SQLFreeHandle线程释放它持有的任何锁),延迟CLR的关闭。如果这样会阻碍应用程序的关闭,那就不太理想了…
编辑:实际上,即使这个想法也不起作用,因为这意味着当线程执行代码时ODBC驱动程序管理器可能实际上卸载驱动程序,从而导致崩溃。
发布于 2019-11-06 00:39:11
我已经和微软的支持部门谈过了,他们已经确认这是comsvcs组件的一个问题,他们可能会在windows的未来版本中修复这个问题。如果他们告诉我他们已经修好了我会更新的。
https://stackoverflow.com/questions/57120511
复制相似问题