多线程环境下libcurl的一个Bug
今天运气不错,解决了两个非常棘手的Bug,这一个是关于libcurl的。
我的程序在一个线程中执行主循环,需要从网络下载东西时,就开启另一个线程,执行curl_easy_perform。问题是,程序在执行一段时间后会自己崩溃掉,backtrack如下:
#0 0x4001e416 in __kernel_vsyscall () #1 0x40491941 in raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:64 #2 0x40494e42 in abort () at abort.c:92 #3 0x404c9305 in __libc_message (do_abort=2, fmt=0x4059f36a "*** %s ***: %s terminated\n") at ../sysdeps/unix/sysv/linux/libc_fatal.c:189 #4 0x4054c970 in __fortify_fail (msg=<value optimized out>) at fortify_fail.c:32 #5 0x4054c8da in ____longjmp_chk () at ../sysdeps/unix/sysv/linux/i386/____longjmp_chk.S:76 #6 0x4054c849 in __longjmp_chk (env=<value optimized out>, val=1) at ../setjmp/longjmp.c:40 #7 0x4020dd08 in alarmfunc () from /usr/lib/libcurl-gnutls.so.4 #8 <signal handler called> #9 0x4001e416 in __kernel_vsyscall () #10 0x4004b930 in sem_wait@GLIBC_2.0 () at ../nptl/sysdeps/unix/sysv/linux/i386/i686/../i486/sem_wait.S:318 #11 0x400fa7ac in PeekMessageEx (pMsg=0xbffff0b8, hWnd=134580240, iMsgFilterMin=0, iMsgFilterMax=0, bWait=1, uRemoveMsg=1) at message.c:628 #12 0x0804b45f in GetMessage (this=0xbffff104) at /usr/local/include/minigui/window.h:1935 #13 WeiboUI::Application::run (this=0xbffff104) at application.cc:99 #14 0x08049df0 in MiniGUIAppMain (argc=1, argv=0xbffff274) at main.cc:56 #15 0x08049f4c in main (args=1, argv=0xbffff274) at main.cc:56
简单说明一下,就是主循环在执行sem_wait時,被一个信号打断,然后导致整个程序的崩溃。问题的关键就在那个alarmfunc (),是libcurl打断了我的循环。
最后以alarmfunc为关键词搜索后才发现,原来libcurl居然使用alarm来实现超时的判断!在多线程环境里,alarm产生的信号会打断一切主循环,然后导致非常难以察觉的Bug。
解决方法很简单,把CURLOPT_NOSIGNAL设为1就行了,这样libcurl将不会产生任何信号:
http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTNOSIGNAL
记一次修复MiniGUI相关Bug的过程
这个Bug连着困扰了我六七个小时,而且表现神秘不定,我觉得很有必要把它详细地记录下来。
事情源于我正在编写的一个GUI库,底层基于MiniGUI,主要用在嵌入式Linux下显示图形界面。最开始的表现是,我在本机编译完成后跑得好好的程序,在交叉编译后放到arm板子上一启动就崩溃,而且诡异的是,崩溃的几率和我类的参数有关,最开始我在类的destructor前加上了virtual之后,崩溃消失了,然后我又在类里加了些变量,程序又崩溃了。而且本机上一点崩溃的迹象也没有。所以我一度认为这是交叉编译器的Bug,无奈。
之后事情终于有了转机,本机上编译的程序终于也崩溃了!可见,很多时候程序员们是多么渴望程序崩溃啊!
于是马上调出gdb,崩溃处的代码如下:
dskOnNewCtrlInstance (hWnd=1075427776, message=361, wParam=134576144, lParam=134575776) at desktop-comm.c:2683 2683 pNode->hWnd = (HWND)pNewCtrl;
p pNode 发现pNode是0x00,找到你了!难道是MiniGUI的Bug?
再仔细观察代码:
if (pNewCtrl->dwExStyle & WS_EX_CTRLASMAINWIN) { PZORDERNODE pNode = pNewCtrl->pZOrderNode;
不对!代码理应不会跳到这一段,我的dwExStyle并没有包含WS_EX_CTRLASMAINWIN,我设置的应该是WS_EX_NONE,最后前往注册窗口类的代码处,终于发现了Bug所在:
/* Register the common WINDOW_CLASS_NAME */ WNDCLASS WindowClass; WindowClass.spClassName = (char*) WINDOW_CLASS_NAME; WindowClass.dwStyle = WS_EX_NONE; WindowClass.hCursor = GetSystemCursor(IDC_ARROW); WindowClass.iBkColor = COLOR_lightwhite; WindowClass.WinProc = BasicWindow::dispatch; RegisterWindowClass(&WindowClass);
原来我没有设置dwExStyle,于是dwExStyle被填充入了垃圾数据,运气好時不会启动这个Bug,运气不好時就会崩溃,归根到底问题还是出在我自己身上……
最后虽然MiniGUI没有错,但也许一句 assert (NULL != pNode); 就能省下我许许多多的时间,也算是今后自己写代码的一个教训吧。
用printf的风格格式化std::string
一直很偏爱C风格的printf式格式化字符串,清晰而且高效。但是很遗憾C++的std::string并不支持这种格式化方法,只能用丑陋的ostringstream来模仿:
std::ostringstream os; os << "Hello test " << someint << ' ' << somestring; std::string buffer(os.str());
不过我们可以自己写一个:
#include <stdio.h> #include <stdarg.h> std::string format(const char *fmt, ...) { va_list ap; va_start(ap, fmt); const size_t SIZE = 512; char buffer[SIZE] = { 0 }; vsnprintf(buffer, SIZE, fmt, ap); va_end(ap); return std::string(buffer); }
原理很简单,vsnprintf可以直接使用变长变量列表来格式化字符串,我们只需要提供一个返回std::string的变长变量函数,然后使用vsnprintf来进行格式化操作即可。
当然还有几个小问题:buffer是固定字长的,会截断结果;返回的std::string需要对buffer进行一次复制,略有效率损失。
为什么不用匈牙利命名法
一直不喜欢匈牙利命名法,但周围的人几乎都推荐使用匈牙利命名法,尤其是很多计算机老师。大概他们都是伴随着微软长大的吧,匈牙利命名法见得多了就开始信奉了。
不过今天总算是找到支持者了。Imperfect C++ 里举了这么一个例子:
typedef map<string, map<string, int> > string_2_string_2_int_map_map_t; string_2_string_2_int_map_map_t s2s2immIncludesDependencyTree;
不许笑!这可是实实在在出现在项目里的代码。真觉得,喜欢匈牙利命名法的人写代码時大概经常复制粘贴吧。
当然,并非微软的命名方式都是坏的,比如因为MFC而流行起来的 m_value ,就是在数据成员的名字前加上 m_ 前缀,我就很喜欢。Imperfect C++ 的脚注里这样评价道:
You see, there is something good about MFC!
嗯,这本书真对我胃口。