DllMain和多线程死锁

DllMain和多线程死锁
在Windows操作系统中,DLL(动态链接库)技术有很多优点。例如,多个应用程序可以共享一个DLL文件,真正实现了资源"共享",大大缩小了应用程序的执行代码,有效地利用了内存,而且DLL文件作为一个单独的程序模块,封装性、独立性好,有利于提高软件开发和维护的效率。
  DllMain是可选择的DLL入口指针,当进程和线程启动和终止时被系统调用,分别进行创建资源和释放资源等操作,特别地,也可以在DLL被装载进进程空间时(即DllMain响应DLL_PROCESS_ATTACH通知时)创建线程,在DLL从进程空间卸载时(即DllMain响应 DLL_PROCESS_DETACH通知时)结束线程。但是,在DllMain中无论是创建线程还是结束线程,都特别要注意一个规则,那就是 DllMain的顺序调用规则。
  1、DllMain的顺序调用规则
  Windows操作系统中是顺序调用DLL的入口函数DllMain的。当进程被创建时,系统也为该进程创建了一个互斥对象。每个进程都有它自己的互斥对象。进程互斥对象的一个作用是,序列化在需要调用DllMain的 4种情况下DllMain的执行:DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH和 DLL_PROCESS_DETACHDLL。DllMain函数的第二个参数指示出调用DllMain的原因。
  在DllMain中创建线程或终止线程时,如果违背了DllMain的这个顺序调用规则,程序就会发生死锁。下面就DllMain中创建线程和终止线程两种情况下的死锁分别进行讲述。
  2、DllMain中创建和终止线程时的死锁
  2.1、装载DLL时创建的线程的为什么没有运行
  考虑在一个多线程程序中,某个DLL被加载进程地址空间时,该DLL的DllMain启动了一个线程,然后立即调用一个应答事件对象的 WaitForSingleObject函数,以确认在继续进行其余的DllMain处理之前,新产生的线程能够正确地执行一些操作。类似的代码如下:
  //----------------------start   ------------
  HANDLE       g_thread_handle =NULL;       // 该DLL内部线程的句柄
  DWORD       g_thread_id =0;       // 该DLL内部线程的ID
  HANDLE g_hEvent=NULL;// 应答事件的句柄
  DWORD WINAPI InSideDll_ThreadProc( LPVOID p )
  {
  /* 表示一些操作。
  如果“---- operations.----”被打印到Output窗口中了,
  说明本线程函数在被执行了。 */
  OutputDebugString(“---- operations.---- /n”);
  /*   InSideDll_ThreadProc的操作完成后,
  通知在g_hEvent处等待的线程,可以继续运行了。*/
  SetEvent(g_hEvent);
  return   1;
  }
  BOOL APIENTRY DllMain( HANDLE hModule,
  DWORD   ul_reason_for_call, LPVOID lpReserved )
  {
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
  //DLL正在映射到进程地址空间中
  {
  // 禁止线程库调用,
  DisableThreadLibraryCalls((HINSTANCE)hModule);
  // 创建DLL内线程使用的事件对象
  g_hEvent = ::CreateEvent( NULL, FALSE, FALSE, _T("hello11" ));
  //创建DLL内线程对象
  g_thread_handle = ::CreateThread(NULL,0,
  InSideDll_ThreadProc,(LPVOID)0,0,   &( g_thread_id) ) ;
  // 等待刚创建的线程完成相关操作
  ::WaitForSingleObject( g_hEvent, INFINITE );
 // 清除资源
  ::CloseHandle(g_thread_handle);
  g_thread_id = 0 ;
  g_thread_handle = NULL ;
  ::CloseHandle(g_hEvent);
  g_hEvent=NULL;
  }
  break;
  case DLL_PROCESS_DETACH:
  // DLL正在从进程地址空间中卸载
  break;
  }
  return TRUE;
  }
  //----------------------end   ------------
  如果对这样的程序进行调试,通过Call Stack窗口可以看到该程序正在等待DllMain内部的线程处理,而Output窗口中也没有打印出“---- operations.---- ”语句。可见线程函数InSideDll_ThreadProc根本就没有得到运行的机会。
  结合DllMain的顺序调用规则,答案就很简单了。在程序运行过程中,第一个线程对LoadLibrary的调用引起操作系统获取进程互斥对象并以DLL_PROCESS_ATTACH值调用该DLL的DllMain。该DLL的DllMain函数产生第二个线程。无论何时当进程产生一个新线程时,操作系统将获取进程互斥对象,以便于它可以为DLL_THREAD_ATTACH值调用每个加载的DLL的DllMain函数。在这个特定的程序中,第二个线程阻塞,因为第一个线程还保持着进程互斥对象。不幸的是,第一个线程然后调用WaitForSingleObject确认第二个线程能够正确地完成一些操作。因为第二个线程被阻塞在进程互斥对象上,这个进程互斥对象还被第一个线程所持有,而第一个线程要等待第二个线程从而也被阻塞,结果就导致了死锁。如下图所示。
  另外,DisableThreadLibraryCalls函数并不能解除这种死锁,相关原因在《Windows核心编程》一书中有更详尽的描述,这里就不再赘述了。
  2.2、卸载DLL时内部线程为什么没有完全退出
  估计很多人都知道装载DLL过程中的多线程死锁是因为DllMain的顺序调用规则,但是很少人了解卸载DLL过程中的多线程死锁也是由于同样的原因。例如,如果一个DLL的DllMain的代码写成下面的形式,且进程中有至少一个DLL的DllMain没有调用DisableThreadLibraryCalls函数的话,那么卸载该DLL过程中就会因为 DllMain的顺序操作特性带来DLL内部线程没有完全退出的错误。
  //----------------------start   ------------
  HANDLE       g_thread_handle =NULL;       // 该DLL内部线程的句柄
  DWORD       g_thread_id =0;       // 该DLL内部线程的ID
  HANDLE g_hEvent=NULL;// 应答事件的句柄
  DWORD WINAPI InSideDll_ThreadProc( LPVOID p )
  {
  while(1){
  // 收到通知就退出线程函数
  DWORD ret = ::WaitForSingleObject( g_hEvent, INFINITE );
  if(WAIT_TIMEOUT = =ret|| WAIT_OBJECT_0 = =ret) break;
  }
  return true ;
  }
  BOOL APIENTRY DllMain( HANDLE hModule,
  DWORD   ul_reason_for_call,
  LPVOID lpReserved
  )
  {
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
  //线程正在映射到进程地址空间中
  {
  // 创建DLL内的线程使用的事件对象
  g_hEvent = ::CreateEvent( NULL, FALSE, FALSE, _T("hello11" ));
 //创建DLL内的线程对象
  g_thread_handle = ::CreateThread(NULL,0,
  InSideDll_ThreadProc,(LPVOID)0,0,   &( g_thread_id) ) ;
  // 禁止线程库调用,
  DisableThreadLibraryCalls((HINSTANCE)hModule);
  }
  break;
  case DLL_PROCESS_DETACH:
  // DLL正在从进程地址空间中卸载
  {
  //   通知内部的线程g_thread_handle 退出
  ::SetEvent(g_hEvent);
  //   等待内部的线程g_thread_handle 退出
  ::WaitForSingleObject(g_thread_handle, INFINITE ) ;
  // 清除资源
  ::CloseHandle(g_thread_handle);
  g_thread_id = 0 ;
  g_thread_handle = NULL ;
  ::CloseHandle(g_hEvent);
  g_hEvent=NULL;
  }
  break;
  }
  return TRUE;
  }
  //----------------------end   ------------
  上述代码的流程是这样的:
  (1)装载DLL时,创建一个   DLL内部的线程g_thread_handle及事件对象g_hEvent,且线程g_thread_handle在事件对象g_hEvent上等待。
  (2)卸载DLL时,通过SetEvent(g_hEvent)通知线程g_thread_handle退出,随即调用 WaitForSingleObject函数等待线程g_thread_handle终止运行。如果线程g_thread_handle终止运行,则执行清除工作。
  但是如果对这样的程序进行调试,就会发现程序在退出时该DllMain没有退出,等待了很长时间也没有退出。
  查看了一下线程Call Stack窗口,注意到程序正在等待DllMain内部的线程g_thread_handle的退出。尽管线程g_thread_handle的线程函数已经返回了,但是整个g_thread_handle线程走到了操作系统的ntdll.dll中并没有完全终止,从而导致整个DLL不能顺利释放。
  线程g_thread_handle为什么没有完全退出呢?
  原来,线程函数返回时,系统并不立即将它撤消。相反,系统要取出这个即将被撤消的线程,让它调用已经映射的DLL的所有带有 DLL_THREAD_DETACH值的、且没有调用DisableThreadLibraryCalls函数的DllMain函数。 DLL_THREAD_DETACH通知告诉所有的DLL执行每个线程的清除操作,例如,DLL版本的C/C++运行期库能够释放它用于管理多线程应用程序的数据块。DisableThreadLibraryCalls函数告诉系统说,特定的DLL的DllMain函数不用接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
  但是,系统是顺序调用DLL的DllMain函数的。
  当线程函数返回时,系统检查进程中是否存在没有调用DisableThreadLibraryCalls函数的DllMain函数,如果存在,系统就以进程的互斥对象的句柄作为第一个参数,在线程内部调用WaitForSingleObject函数。一旦这个将要终止运行的线程拥有该进程互斥对象,系统就让该线程用DLL_THREAD_DETACH的值依次调用每个没有调用DisableThreadLibraryCalls函数的DLL的 DllMain函数。此后,系统才释放对进程互斥对象的所有权。
  在本例所述的应用程序中,进程的退出引起操作系统获取进程互斥对象使操作系统可以为DLL_PROCESS_DETACH通知调用 DllMain()。该DLL的DllMain()函数通知线程g_thread_handle终止运行。无论何时当进程终止一个线程时,操作系统将获取进程互斥对象,以便于它可以为DLL_THREAD_DETACH通知调用每个加载的、没有调用DisableThreadLibraryCalls函数的DLL的DllMain函数。在这个特定的程序中,线程g_thread_handle当线程函数返回后就阻塞了,因为CMySingleton的 DllMain()所处的线程还保持着进程互斥对象。不幸的是,DllMain所处的线程然后调用WaitForSingleObject确认 g_thread_handle线程是否完全终止。因为g_thread_handle线程被阻塞在进程互斥对象上,这个进程互斥对象还被DllMain 线程所持有, DllMain线程要等待g_thread_handle线程从而也被阻塞,结果就导致了死锁。如下图所示:
  注意,从图2可以看出,如果当前进程中的所有 DLL都调用了DisableThreadLibraryCalls函数,那么上述代码中的DLL也能正常退出。曾经写过一个程序,除了加载一个这样有问题的DLL没有加载其他DLL(系统的DLL除外),程序能够正常退出。
  3、结论
  很显然的一个教训就是在DllMain内部应该避免任何Wait*调用。但是进程互斥对象的问题不仅仅限于Wait*函数。操作系统在 CreateProcess、GetModuleFileName、GetProcAddress、wglMakeCurrent、 LoadLibrary和FreeLibrary等函数中在后台获取进程互斥对象,因此在DllMain中不应该调用任何这些函数。因为DllMain获取进程互斥对象,所以一次只能有一个线程执行DllMain。
  ATL singleton的 FinalConstruct函数和FinalRelease函数分别是DllMain在响应DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH时被调用的,所以也要同样注意本文所述的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值