在用多进程下载模型可耻地失败之后,OSD Lyrics使用了多线程模型来对歌词进行下载。在完成之后发现每当DNS超时时,程序都会崩溃。经过不断地调试,确定是curl的问题。看了文档后发现,原来curl在多线程环境下,由于DNS查找超时是使用信号实现的,所以在多线程环境下是不安全的。当时想了两个方法:在自己的程序里包含curl,启用异步DNS查询cares;或者大幅修改程序,使用curl的异步查询接口重写下载引擎,把程序重新变成一个单进程、单进程的程序。第一种方法会让代码的大小和编译时间大幅增长,第二种方法要把两个下载引擎(搜狗和千千的)给大幅改写,过于复杂。于是一直纠结……
后来请教了 @henry_ 老师,一句话把我打回原点——用多进程才是王道。
重新审视自己的设计和多进程模型失败的原因:因为在子进程中使用了X的操作。为什么会有X的操作呢?看看下载一个歌词的步骤:
之所以会有X操作,是因为第2步弹出了一个窗口。由于子线程同样不能操作GUI元素,因此多线程模型同样要在主线程中处理弹出窗口。我在设计中将下载过程给模块化了,有一个专门的下载模块,将具体的下载引擎和主程序分开。线程化后的下载过程是这样的:
也就是说,只是将原来的第1步和第3步线程化了,而这两步是没有X操作的。而且由于将下载过程都放在下载模块里了,因此多线程部分完全是在下载模块里实现的,主程序和下载引擎一点也不知道多线程的存在。
既然如此,为何不讲它们进程化呢?把实现中的线程改成进程,就是多进程模型了。一旦这样做的话:
首先搞清楚自己需要怎样的多进程操作:
fork
风格简单来说,就是要一个刚好能替代现有线程操作的方案,而且它是和现在的线程操作类似的,这样使用起来直观,而且修改难度小。
然而我遇到了一个问题。
从子进程中返回数据的问题思考了很久,因为进程间通信不像线程间通信那么简单,直接传一个指针完事。@henry_ 老师给我指明了两条路:使用管道或共享内存。
就此思考了一番。管道需要将返回数据序列化,再通过管道传送,最后反序列化还原数据。共享内存不需要序列化,但是需要自己管理内存,而且不知道要开多大的共享内存,内存的分配与释放都要仔细考虑。
最后的决定是使用管道。虽然要实现序列化操作,但是过程简单清晰。如果使用共享内存,那么设计会变得很复杂,容易出错,而且内存相互分离的优点也没了。
把一切都想清楚之后设计就很清晰了。
最后的设计只有一个函数:
pid_t ol_fork (OlForkCallback callback, void *userdata);
回调函数的定义如下:
typedef void (*OlForkCallback) (void *ret_data, size_t ret_size, int status, void *userdata);
同时,为子进程提供了用于返回数据的文件描述符和文件流:
extern int ret_fd; extern FILE *fret;
剩下的最大问题就是如何在子进程退出时调用回调函数了。最直接的想法就是处理SIGCHLD
信号。由于不同的子进程可能需要调用不同的回调函数,也有不同的用户数据,因此需要有一个子进程ID和回调函数的对应表。感谢GTK、GDK、GLib,现在只需要用g_child_watch_add
就一切都搞定了。
关于序列化,本来是想用protobuf的,它的C绑定似乎还直接提供了RPC的支持。由于我只是简单的需要一个IPC的数据传递,干脆自己写一个来练手好了。
序列化的协议很简单。因为只有两种结构需要序列化——数组和结构体,所以就简单地定义了一下。对于结构体,将其字段按一定顺序输出,以换行符间隔。对于数组,先输出一行总数,再输出各元素,每个元素后再加上一个换行符。由于是程序内部用的IPC数据,因此只要单元测试通过了,数据的正确性就可以完全信赖,不需要复杂的容错机制。
一开始在考虑子进程返回的触发时,@henry_ 提示可以用select或者poll。实现了之后发现有两个问题,一是如果子进程的返回数据是分段输出的话,回调函数可能会调用几次,每次收到一段数据;二是如果子进程没有返回数据(也就是返回数据为空),那么回调函数根本就不会运行。因此还是老老实实采用子进程退出来触发回调函数。
开始的设计是向回调函数传递一个fd,让回调函数自己读取数据。这么做是为了能让回调函数利用fgets
来读取一行。思考之后觉得这样很不自然,而且回调函数要考虑的东西反而多了,复杂了,于是改成了现在这个样子。
在实现序列化的时候没有想清楚,把不需要序列化的OlMusicInfo
给序列化了,白做工了。
在反序列化结构时,一开始是返回true or false,后来发现一个问题,就是在反序列化数组时,不知道一个元素反序列化后,剩下未被反序列化的数据从哪开始。于是把接口改成了返回指向剩下数据开头的指针,如果失败就返回NULL。
把fork模块和序列化做好之后,修改代码就很简单了,只需要把对应的地方改一下,程序很欢快地跑了起来。
使用多进程的设计,避免了修改curl使用方式的缺点,而且使得程序更健壮。由于Linux轻量级进程的优势,多进程和多线程的性能应该没有太大的差别,而且实际上下载也不是程序的瓶颈。
此次设计进行了仔细考虑,而没有像之前边做边迭代的方法。在明确自己的目标,排除多余的东西,对各方面作出权衡和折衷之后,得到了一个比较简洁的接口和实现,自己觉得还是很满意的。
实现的过程中对新的代码做了充足的单元测试,发现了一些问题,同时修正了设计,最后用起来自己也相当放心。在代码不是立刻可以整合到系统中的时候,单元测试往往是能立刻看到成果的方式,而且也能检验自己的设计是否合理。
从多进程到多线程,最后又回到多进程,貌似绕了个大弯又回到原点。但是这两个多进程的设计高度完全不一样。在实现了多线程机制后,对整个下载的过程和实现有了充分的了解,现在的多进程流程也是在多线程的设计中定下的。如果没有多线程模型这个探索,也就很难有现在这个设计。所谓螺旋式上升嘛,说不定以后又会改成多线程,但是可以肯定的一点是,一定会比现在的要更好。
今天OSD Lyrics收到了一个issue,说是在歌手不存在的情况下xmms2搜索歌词成功,下载歌词失败。之前在Exaile下也有类似的bug,不过这个bug的问题不同(应该说Exaile下也还是会有这个问题)。
OSD Lyrics对歌词文件名的查找是基于模板的,模板中有一些占位符,例如%t代表歌名,%p代表歌手。有一个函数ol_path_get_lrc_pathname,负责给定路径模板和文件名模板以及歌曲信息,输出根据模板转换后的完整文件路径。成功时返回输出的路径长度,失败则返回-1。
但是在程序日志中发现,当歌手为空时,文件名模板“%p-%t”应该是失败的,然而却依然被当作成功来使用。这样一来,匹配的结果就成了只有目录,缺少文件名的路径,把歌词下载的目标地址设成本机的一个目录,那肯定是会失败的嘛。
把ol_path_get_lrc_pathname相关的函数里外查了一遍,没有任何问题,出错时也的确返回了-1,于是目光落到调用这个函数的地方:
if ((ol_path_get_lrc_pathname ( path_list[i], name_list[j], info, file_name, MAX_PATH_LEN)) >= 0) { /* do something */ }
发现把>=0
改成!=-1
就正常了。
为什么会这样?原因就在于ol_path_get_lrc_pathname的返回类型是size_t,而size_t在我的系统里的定义是unsigned int。任意一个unsigned类型都不会小于0。
那么为什么改成!=-1后,反而又可以了呢?任意一个unsigned类似应该都不会等于-1呀。
这就涉及到C语言的类型提升问题了。在C语言中,那个操作数的类型不同时,会把操作数都转化成两个操作数中能表示的范围最大的类型。就表示的类型来说,shot<=int<=long<float<double,这个是没有任何问题的。问题在于,当出现了unsigned后,怎样计算。具体来说,两个操作数是unsigned int 和 int时,如何转化?
如果是unsigned short int和short int就好办了,直接转成int就行。然而很可惜,在32位机子里,int = long,把unsigned int和int转成long不能解决任何问题。
怎么办呢?很简单,两个都转成unsigned int。
在这个例子中,两个规则是一样的,因为返回的-1变成了size_t后,变成了最大的unsigned long,所以-1也跟着被转成了unsigned long,变成了0xFFFFFFFF,两个刚好又相等了:)
作为一个例子,请看下面一段代码:
#include <stdio.h> int main() { unsigned long a = 0; long b = -1; printf ("%s\n", a > b ? "Greater" : "Less"); return 0; }
会输出什么呢?gcc 4.4.1给出的答案是Less,因为-1被转型了:(
三月实习回来之后就有点手痒痒,和舍友sarlmolapple一起说想做个自由小项目,一则练习一下Linux下的编程,二则有点项目经验,三则可以自己用。敲定的项目是做桌面歌词,也就是在屏幕上显示的OSD歌词,我们规划了一下,基本应该具备的功能有:
项目用C来编写,基于GTK+。死党simplyzhao听了之后也很有兴趣,加入了进来。
其实现在已经有了挺不错的外挂歌词脚本lrcdis,为什么我们还要弄一个呢?其一是因为想自己做点东西,丰富经验,而做项目最大的动力就是自己要用。其二是因为lrcdis的OSD功能还很弱,而且由于是用bash脚本写的,应该很难增强(lrcdis是调用gnome-osd来实现OSD的)。
至于为什么用C,其实我觉得这个项目用Python来写会比较好,用C的唯一理由是……大家都想好好学学C-_-|||
其实这玩意计划了挺久了,现在原形设计得差不多了,技术问题也基本解决了,才敢公开出来。等到有可用代码的时候我会把项目地址给放出来的^ ^
转自http://hi.baidu.com/yilinghl/blog/item/5dc7d55cb4271f43faf2c069.html
用emacs写程序也有5个年头了,深切地体会到Emacs的强大。程序员有三种,一种是用vi的,一种是用emacs的,还有一种是其他。或许有些夸张,但也颇能体现出emacs在程序员中的地位。
emacs最大的问题在于入门门槛较高。它看起来和多数人想象中的IDE相差甚远,很多人看到emacs的第一眼就觉得它是个记事本(还是个非常难用的记事本),稍微好些的往往觉得emacs也就是个ultraEditor而已,真是暴殄天物了。
我是个懒人,不喜欢记太多的快捷键,相信很多人和我一样。所以从我后面的叙述可以看出来,除了常用的命令都是快捷键外,其他命令多数都是用M-x执行或者用鼠标点菜单。这仅仅是个人风格问题,先说明一下。
我的基本编程环境是:
后面的叙述都基于上述环境。另外,本文主要针对C/C++程序开发,对其他语言有些也适用,从难度上说,本文主要针对入门者。
本文肯定会有很多错误,请指正,谢谢。
写C++程序基本上是这么几个步骤:
当然,往往还需要阅读别人的代码。
根据上述步骤,本文主要针对以下几个方面:
要写C++程序,当然要用到cc-mode插件。CC-Mode原本是支持C语言的,但现在也能支持很多语言,比如C++,Java, Objective-C,CORBA,AWK,Pike等等。CC-Mode是gnu-emacs的标准插件。如果您要求不高,那么默认的配置或许就能满 足。CC-Mode的各种行为都可以自由地定制,您可以参考这里的文档:CC-Mode参考文档
这里是我的.emacs文件中关于CC-Mode配置的部分,仅供参考:
注意一下,上面最后两行是代码自动补齐的快捷键。后面我会提到代码自动补齐。
自动补齐通常用的都是hippie-expand,我也用了很长时间。不过有时候会觉得这个自动补齐“傻”了一点,常会补齐出一些毫不相干的东西,因为hippie-expand是根据你敲过的词和kill-ring等进行判断的,并不对程序语法进行分析。
所以你还需要安装一个代码分析工具,然后把它加进hippie-expand的扩展策略里去。我们可以用semantic。实际上,hippie-expand+semantic是我所发现的最好的选择了,如果您有更好的,请您也告诉我一声:)
Semantic是CEDET中的一个工具,CEDET是Collection of Emacs Development Environment Tools的缩写,它包含了好几个工具,都挺不错的。可惜我只会用其中两个。
您可以在.emacs中对Semantic进行配置,下面是我的.emacs相关的配置,仅供参考:
导入cedet:
配置Semantic的检索范围:
自定义自动补齐命令,这部分是抄hhuu的,如果在单词中间就补齐,否则就是tab。
hippie的自动补齐策略,优先调用了senator的分析结果:
注意一下我前面CC-Mode配置中有这么两行:
这样,我们在CC-Mode中就可以调用自定义的hippie补全了,快捷键是Tab。
另外,我还把快捷键“Alt + / ”绑定到了semantic-ia-complete-symbol-menu命令上,这是semantic的命令,它会根据分析结果弹出补齐的菜单,效果如图显示:
CEDET中还有一个不错的工具是speedbar,你可以用它在多个文件中快速切换。在我的.emacs配置文件里,我把speedbar关联到了F5上:
这样用F5就可以调出speedbar,效果如下:
不过说实话,我自己很少用到speedbar,我通常都是用dired配合bookmark使用:)
按上面的配置,写完程序和Makefile文件后,在Emacs源代码窗口中按F7就可以进行编译。因为在my-c-mode-common-hook()函数里,有这么一行:
默认情况下,emacs的compile命令是调用make -k,我把它改成了make。你也可以把它改成其他的,比如gcc之类的。改下面的“make”就行了。
Emacs会划分一个窗格显示编译的消息,在编译结束后,emacs会自动将编译器的输出和程序关联起来,告诉你第几行的程序有问题。直接在出错的行号上按Enter,就可以跳转到相应文件的相应行。其实我通常都是用鼠标中键去点出错行号:)
搞定了编译错误后,接着要和逻辑错误斗争了。其实对简单的程序来说,把中间结果打印到终端是最简单好用的调试办法:)不过稍微复杂点的程序就会晕菜了,这时我们就需要拿gdb跟踪程序流程了。
你用下面的命令就可以启动gdb了。
通常我喜欢进入gdb-many-windows模式,这样就会把一个Frame划分为5个窗格,同时显示:gdb命令窗口,当前局部变量,程序文本,调用栈和断点。
gdb的命令就不在这里说了,它的文档几乎到处都是。emacs把gdb的命令和快捷键做了绑定,对于常用的命令,还是输入快捷键比较方便。比如,C-c C-n是Next line,C-c C-s是step in,其实用的最多的快捷键也就是这两个。
下面是我的gdb效果图:
在emacs下读代码通常有三种工具,最简单的是etags,最复杂的是ecb(emacs code browser),位于中间的是cscope。
etags和ctags一样,只不过前者是用于emacs的,后者是用于vi的。我个人觉得etags功能稍稍显得不够用一点,当然,也可能是我用的不好:) 欢迎大牛指导。
使用tags之前要先对源代码分析建立tags文件,在代码所在目录中运行:etags -R 即可。
我常用的就这几个命令和快捷键:
ecb据说功能强大,但是太复杂了,我懒得折腾它。谁搞定了教教我吧:) 下面是一张ecb的效果图。
cscope是我感觉比较合适的一个工具。它其实是一个独立的软件,完全可以脱离vi和emacs使用。但是结合emacs的强大功能, cscope就显得更加方便了。GNU Emacs默认自带cscope的支持。在使用之前,cscope也需要对代码进行索引。在emacs中可以这样做:
建完索引之后,你就可以用cscope在代码里游荡了。常用的一些命令如下:
上面这些快捷键其实我自己也常常记不全,没关系,抬头看看上面的菜单栏,有一栏就是Cscope,这些命令里头都有:)
贴一个cscope的效果图吧:
安装配置程序开发的相关环境: emacs、eclipse、GTK、doxygen、svn