找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 6199|回复: 1

解决程序死锁

[复制链接]

307

主题

228

回帖

7349

积分

用户组: 真·技术宅

UID
2
精华
76
威望
291 点
宅币
5599 个
贡献
253 次
宅之契约
0 份
在线时间
949 小时
注册时间
2014-1-25
发表于 2014-7-3 15:53:48 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
    修复状态一致性资源竞争的一个解决方法是为影响到的共享状态用“锁”建立互斥访问机制。如果使用不恰当锁也会带来更多bug,主要是产生死锁。下面介绍如何调试这类错误。
    死锁是指在多个线程执行时处于互相等待的状态,死锁也可以产生于获得锁并且计划执行线程产生的资源竞争的时候,这种情况极难复制,一旦复制,可以用调试器检查死锁条件是如何达到的。
    锁顺序死锁
最普遍的例子是2个线程按相反的顺序错误的获得锁,并且互相等待对方释放获得的锁。下面是例子:

  1. static
  2. DWORD
  3. WINAPI
  4. WorkerThread(
  5. __in LPVOID lpParameter
  6. )
  7. {
  8. CThreadParams* pParameter;
  9. CWin32CriticalSectionHolder autoLockOne, autoLockTwo;
  10. pParameter = reinterpret_cast<CThreadParams*>(lpParameter);
  11. InterlockedIncrement(&(pParameter->m_nThreadsStarted));
  12. wprintf(L"Thread #%d Callback.\n", pParameter->m_nThreadsStarted);
  13. if (pParameter->m_nThreadsStarted % 2 == 0)
  14. {
  15. autoLockOne.Lock(pParameter->m_csOne);
  16. wprintf(L"One-Two locking order...\n");
  17. Sleep(1000);
  18. autoLockTwo.Lock(pParameter->m_csTwo);
  19. }
  20. else
  21. {
  22. autoLockTwo.Lock(pParameter->m_csTwo);
  23. wprintf(L"Two-One locking order...\n");
  24. Sleep(1000);
  25. autoLockOne.Lock(pParameter->m_csOne);
  26. }
  27. return EXIT_SUCCESS;
  28. }
复制代码


Thread #1 Callback.
Two-One locking order...
Thread #2 Callback.
One-Two locking order...

0:003> .symfix
0:003> .reload
0:003> ~*k
0 Id: 4dc.b58 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr
000bfcf0 77456a04 ntdll!KiFastSystemCallRet
000bfcf4 75666a36 ntdll!ZwWaitForMultipleObjects+0xc
000bfd90 7590bd1e KERNELBASE!WaitForMultipleObjectsEx+0x100
000bfdd8 7590bd8c kernel32!WaitForMultipleObjectsExImplementation+0xe0
000bfdf4 0005185d kernel32!WaitForMultipleObjects+0x18
000bfe5c 000518b7 bug!CMainApp::MainHR+0x65
000bfe60 00051a41 bug!wmain+0x5
...
1 Id: 4dc.1ddc Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr
004bfc9c 77456a24 ntdll!KiFastSystemCallRet
004bfca0 77442264 ntdll!NtWaitForSingleObject+0xc
004bfd04 77442148 ntdll!RtlpWaitOnCriticalSection+0x13e
004bfd2c 00051491 ntdll!RtlEnterCriticalSection+0x150
004bfd3c 000516a2 bug!CWin32CriticalSectionHolder::Lock+0x1f
004bfd64 7590ed6c bug!CMainApp::WorkerThread+0x9d
...
2 Id: 4dc.1f04 Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr
002cf88c 77456a24 ntdll!KiFastSystemCallRet
002cf890 77442264 ntdll!NtWaitForSingleObject+0xc
002cf8f4 77442148 ntdll!RtlpWaitOnCriticalSection+0x13e
002cf91c 00051491 ntdll!RtlEnterCriticalSection+0x150
002cf92c 000516a2 bug!CWin32CriticalSectionHolder::Lock+0x1f
002cf954 7590ed6c bug!CMainApp::WorkerThread+0x9d
...
# 3 Id: 4dc.1bc4 Suspend: 1 Teb: 7ffdb000 Unfrozen
ChildEBP RetAddr
0046f9b4 774af161 ntdll!DbgBreakPoint
0046f9e4 7590ed6c ntdll!DbgUiRemoteBreakin+0x3c

第二次执行发生死锁,windbg载入后查看堆栈,发现ntdll!NtWaitForSingleObject,
!cs命令可以查看thread#1和thread#2等待的临界区信息。2个临界区对象地址可以在RtlEnterCriticalSection调用栈的帧指针中看到。
0:003> $ Saved EBP, followed by the return address and then the first argument
0:003> dd 004bfd2c
004bfd2c 004bfd3c 00051491 000bfe18 000bfe50
0:003> !cs 000bfe18
Critical section = 0x000bfe18 (+0xBFE18)
DebugInfo = 0x0013b2a0
LOCKED
LockCount = 0x1
WaiterWoken = No
OwningThread = 0x00001f04
RecursionCount = 0x1
LockSemaphore = 0x3C
SpinCount = 0x00000000
0:003> $ ~ lists all the threads in the process, along with their respective client ID (CID)
0:003> ~
0 Id: 4dc.b58 Suspend: 1 Teb: 7ffde000 Unfrozen
1 Id: 4dc.1ddc Suspend: 1 Teb: 7ffdd000 Unfrozen
2 Id: 4dc.1f04 Suspend: 1 Teb: 7ffdc000 Unfrozen
. 3 Id: 4dc.1bc4 Suspend: 1 Teb: 7ffdb000 Unfrozen

!cs可以解析临界区结构体,很方便的显示每个成员的含义,这是由于临界区是内建类型,和纯内核对象的互斥体不同,临界区只能在进程用户模式下使用。当线程试图进入临界区,LockCount域由用户模式ntdll.dll检查决定该临界区是否空闲,如果空闲则临界区被锁住,线程被该临界区占用,锁计数自动更新,如果不是空闲锁,则线程则在转换成内核态之前处于用户态忙等状态,等待事件派遣对象通知临界区是否仍然处于锁定状态。

避免锁顺序死锁
  一种常见的解决方式是使用锁排序方案,在程序中为每个锁分配任意一个级,强制锁只能按照升序获得。实现这个技术需要设置一个全局结构体记录进程中的所有线程,为每个线程关联一个栈存储他们获得的锁。在请求获得锁时检查要求的锁等级,这样做很容易捕获死锁bug。这种技术应用于CLR内部实现代码以预防死锁bug,Microsoft SQL Server也使用这种技术在开发阶段预防死锁。
回复

使用道具 举报

307

主题

228

回帖

7349

积分

用户组: 真·技术宅

UID
2
精华
76
威望
291 点
宅币
5599 个
贡献
253 次
宅之契约
0 份
在线时间
949 小时
注册时间
2014-1-25
 楼主| 发表于 2014-7-3 21:16:39 | 显示全部楼层
逻辑死锁
    死锁也可以在只有一个锁或者无锁的情况下发生。这是由于windows线程不仅在要获得已经被其他线程占用的锁时会发生阻塞,在等待线程/进程退出事件、异步I/O完成事件、或者显式调用通知内核事件对象(SetEvent)等逻辑事件发生的时候,也会发生死锁。下面是实例:
dll代码
  1. [/color][/size]
  2. #include <stdio.h>
  3. #include <windows.h>
  4. DWORD WINAPI WorkerThread(LPVOID pParam)
  5. {
  6. wprintf(L"Inside WorkerThread.\n");
  7. return 0;
  8. }

  9. BOOL APIENTRY DllMain( HANDLE hInstance,
  10.                        DWORD  dwReason,
  11.                        LPVOID lpReserved
  12.       )
  13. {
  14. HANDLE shThread;
  15. switch(dwReason)
  16. {
  17.   case DLL_PROCESS_ATTACH:
  18.    printf("DLL_PROCESS_ATTACH begin");
  19.    ::DisableThreadLibraryCalls((HINSTANCE)hInstance);
  20.    shThread=CreateThread(NULL,0,WorkerThread,NULL,0,NULL);
  21.    WaitForSingleObject(shThread,INFINITE);
  22.    printf("DLL_PROCESS_ATTACH end");
  23.    break;
  24.   default:
  25.    printf("DLL_DEFAULT\n");
  26.    break;
  27. }
  28. return TRUE;
  29. }

  30. [size=3][color=black]
复制代码


exe代码
  1. [/color][/size]
  2. #include <windows.h>
  3. #include <stdio.h>

  4. void main()
  5. {
  6. HINSTANCE lib=LoadLibrary("dll\\debug\\dll.dll");
  7. printf("main end\n");
  8. }
  9. [size=3][color=black]
复制代码


加载dll时,会发现程序处于阻塞状态,无法打印信息。
windbg调试死锁:
   0  Id: 1c1c.1404 Suspend: 1 Teb: 7efdd000 Unfrozen
ChildEBP RetAddr  
0018faa4 7621149d ntdll!NtWaitForSingleObject+0x15
0018fb10 765a1194 KERNELBASE!WaitForSingleObjectEx+0x98
0018fb28 765a1148 kernel32!WaitForSingleObjectExImplementation+0x75
0018fb3c 100010d2 kernel32!WaitForSingleObject+0x12
0018fba0 10001480 dll!DllMain+0x62 [D:\temp\test\dll\dll.cpp @ 28]
0018fbb8 77989950 dll!_DllMainCRTStartup+0x80 [dllcrt0.c @ 237]
0018fbd8 7798d8c9 ntdll!LdrpCallInitRoutine+0x14
0018fccc 7798d78c ntdll!LdrpRunInitializeRoutines+0x26f
0018fe38 7798c4d5 ntdll!LdrpLoadDll+0x4d1
0018fe70 76212c95 ntdll!LdrLoadDll+0xaa
0018feac 76212cf2 KERNELBASE!LoadLibraryExW+0x1f1
0018fecc 765a49f0 KERNELBASE!LoadLibraryExA+0x26
0018feec 00401045 kernel32!LoadLibraryA+0xba
0018ff48 00401309 test!main+0x25 [d:\temp\test\test.cpp @ 7]
0018ff88 765a33aa test!mainCRTStartup+0xe9 [crt0.c @ 206]
0018ff94 77989ef2 kernel32!BaseThreadInitThunk+0xe
0018ffd4 77989ec5 ntdll!__RtlUserThreadStart+0x70
0018ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
   1  Id: 1c1c.1c18 Suspend: 1 Teb: 7efda000 Unfrozen
ChildEBP RetAddr  
007dfb94 77988df4 ntdll!NtWaitForSingleObject+0x15
007dfbf8 77988cd8 ntdll!RtlpWaitOnCriticalSection+0x13e
007dfc20 7798a0a9 ntdll!RtlEnterCriticalSection+0x150
007dfcb4 77989e4c ntdll!LdrpInitializeThread+0xc6
007dfd00 77989e79 ntdll!_LdrpInitialize+0x1ad
007dfd10 00000000 ntdll!LdrInitializeThunk+0x10
#  2  Id: 1c1c.1fcc Suspend: 1 Teb: 7efd7000 Unfrozen
ChildEBP RetAddr  
009aff58 779ef896 ntdll!DbgBreakPoint
009aff88 765a33aa ntdll!DbgUiRemoteBreakin+0x3c
009aff94 77989ef2 kernel32!BaseThreadInitThunk+0xe
009affd4 77989ec5 ntdll!__RtlUserThreadStart+0x70
009affec 00000000 ntdll!_RtlUserThreadStart+0x1b

查看线程0在kernel32!WaitForSingleObject处的调用:
0:002> dd 0x0018fb3c
0018fb3c  0018fba0 100010d2 00000038 ffffffff
0018fb4c  0018fc88 0018fbcc 00000001 cccccccc
0018fb5c  cccccccc cccccccc cccccccc cccccccc
0018fb6c  cccccccc cccccccc cccccccc cccccccc
0018fb7c  cccccccc cccccccc cccccccc cccccccc
0018fb8c  cccccccc cccccccc cccccccc 00000001
0018fb9c  00000038 0018fbb8 10001480 10000000
0018fbac  00000001 00000000 00000001 0018fbd8

发现句柄值为0x38
0:002> !handle 0x38 f
Handle 38
  Type          Thread
  Attributes    0
  GrantedAccess 0x1fffff:
         Delete,ReadControl,WriteDac,WriteOwner,Synch
         Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
  HandleCount   4
  PointerCount  7
  Name          <none>
  Object Specific Information
    Thread Id   1c1c.1c18
    Priority    10
    Base Priority 0
    Start Address 1000100a dll!ILT+5(?WorkerThreadYGKPAXZ)
0:002> ~
   0  Id: 1c1c.1404 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: 1c1c.1c18 Suspend: 1 Teb: 7efda000 Unfrozen
.  2  Id: 1c1c.1fcc Suspend: 1 Teb: 7efd7000 Unfrozen

可见线程0在等待线程1,再来看线程1的ntdll!RtlEnterCriticalSection

0:002> dd 0x007dfc20
007dfc20  007dfcb4 7798a0a9 77a520c0 76ca45f8
007dfc30  7efda000 7efde000 77a5206c 00000000
007dfc40  00000000 00000000 00000000 00000000
007dfc50  00000000 00000000 00000000 00000000
007dfc60  00000000 00000000 00000000 00000000
007dfc70  00000000 00000000 00000000 00000000
007dfc80  00000000 00000000 00000000 00000000
007dfc90  00000000 7efde000 00000000 007dfc2c

得到的
0:002> !cs 77a520c0
-----------------------------------------
Critical section   = 0x77a520c0 (ntdll!LdrpLoaderLock+0x0)
DebugInfo          = 0x77a54360
LOCKED
LockCount          = 0x1
WaiterWoken        = No
OwningThread       = 0x00001404
RecursionCount     = 0x1
LockSemaphore      = 0x3C
SpinCount          = 0x00000000

可见这个临界区的宿主线程时线程0,这样就产生了线程0和线程1的循环依赖,导致他们都停了下来。
上例的ntdll!LdrpLoaderLock是一个重要的系统锁,称为“加载器锁”,这种锁由系统模块加载器ntdll.dll中的代码用来进行DllMain函数的同步调用,即便用户代码中没有显式获取该锁,系统加载器也会在调用DllMain函数之前加载该锁。这种行为确保了DllMain区域代码的串行化执行,提供了一种线程安全的方式完成用户模式DLL模块初始化工作。当新线程创建后,模块DllMain例程被调用之前,系统加载器就会尝试获取相同的加载器锁,让该例程知道进程中产生了新线程(DLL_THREAD_ATTACH)。即使通过DisableThreadLibraryCalls拒绝接受这类消息,其他系统DLL模块仍希望接收到该消息,系统加载器也总是在创建了新线程后获取加载器锁。这就是为什么线程1处于阻塞状态,等待线程0退出DllMain并显式释放绑定的加载器锁全局变量。
PS:因此DllMain中不能放入如WaitForSingleObject阻塞函数
一个重要的步骤是DllMain总是在加了系统加载器锁之后执行,这样的话DllMain中的代码应该尽量简洁,开发者需要了解所有DllMain中调用的函数所进行的所有潜移默化的操作。以下几点要牢记:
  不能在DllMain中创建新线程,或者调用任何可能创建新线程的函数。例如,DllMain中使用COM对象是非法的,因为这样做会创建新进程(ole32.dll)。
  当显式获得锁时,DllMain要尊守其他的通用规则,尤其是避免执行网络操作或其他耗时操作,因为会占用加载器锁很长时间。通常的准则是,你需要限制DllMain所做的工作,使之简单的初始化DLL模块的全局变量。
  DLL模块中的C++静态(全局)变量也会加了系统加载器锁的状态下,在DllMain例程执行之前由C运行库初始化,这样,C++全局对象的构造函数代码也要遵循这种规则。

加载器锁和调试器注入代码
如果创建新线程需要获取加载器锁,为什么在主线程(线程0)被锁时,调试器能注入附加到的远程线程(线程2)?如果你使用Windows XP运行这个实验,试着在死锁的时候进入执行代码,你会发现windbg.exe等了一会,之后强制挂起线程。
Break-in sent, waiting 30 seconds...
WARNING: Break-in timed out, suspending.
This is usually caused by another thread holding the loader lock

这种做法已经在win7得到了改进,Windbg现在可以使用特殊的系统功能注入远程线程。这种特殊的功能和常规的使用Win32 API创建线程的过程相比独特之处在于,他会请求内核在不执行常规初始化的情况下创建线程,这种初始化不需要得到加载器锁。在TEB结构体中,线程2里你可以看到很多地方都是未初始化的。
0:002> dt ntdll!_TEB @$teb
...
+0x02c ThreadLocalStoragePointer : (null) ...
+0x03c CsrClientThread : (null)
+0x040 Win32ThreadInfo : (null) ...
+0x1a8 ActivationContextStackPointer : (null)
0:002> q

特别的,该线程没有在Windows 服务器/客户端 子系统进程 csrss.exe中注册,这也意味着他不能创建UI窗口,也不能使用局部线程存储空间,然而,在上例中这种情况并不大碍,因为此时调试器在注入线程时只需要执行int3指令的调试断点。

回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-5-4 04:16 , Processed in 0.050384 second(s), 31 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表