这两天调试一个网络应用程序,出现一个很诡异的问题:程序在关闭连接时失去响应。用Process Explorer工具查看该程序的各个线程,发现一个工作线程的调用栈类似这样: stopProc ==> closesocket ==> EnterCriticalSection ==> RTEnterCriticalSection .... ....
看到CriticalSectoin的相关调用,我想到closesocket函数可能遇到了死锁。一个看似简单的closesocket还需要加锁吗? 带着这个疑问,我翻出了ReactOS和wine的源代码。在这两份代码中,socket句柄都存储在内核维护的一个双向链表中,closesocket执行的操作就是将socket句柄从链表中删除,这样当然需要采用同步机制,以保证同一个时刻只有一个线程能操作链表。closesocket函数采用了关键段来同步socket链表操作,所以在Process Explorer中能看到EnterCriticalSection调用。
分析到这里,还是没能解决问题,到底是什么原因导致死锁。我又开始查MSDN,查看了closesocket相关资料,知道了grace shutdown这个“新名词”,还试了几个偏方,如设置SO_REUSEADDR属性,设置shutdown的linger参数,无奈还是没能解决问题。不过此般学习倒是让我开始注意了关闭连接时的收发操作。这时,我再次回到自己的代码,走查了相关代码,发现当closehandle时,另外一个工作线程会有收发(recv、send)调用,这两个调用很可能是导致closesocket死锁的“元凶”。为了验证这个观点,我用英文关键词Google了一般(之前没找到有用的中文信息),发现了这个帖子"close socket deadlocks blocked threads",虽然帖子中提到的是BSD系统的问题,但其现象我遇到的一样。所以,到现在,我几乎可以肯定,死锁原因就是那个工作线程中的收发调用。接下来的解决办法就比较简单了:在closesocket调用之前,关闭所有可能调用 recv、send的线程。直接关闭线程的做法比较粗暴,但在我的代码,这样的修改是可行的。
总结一下,如果在关闭socket时,有另外的线程在利用这个socket进行收发,那么closesocket可能会死锁。解决的办法就是在closesocket调用之前,关闭所有的recv、send操作。