lighttpd是目前非常流行的web服务器,很多流量非常大的网站(如youtube)使用的就是lighttpd,它的代码量不多,但是设计巧妙,效率高,功能完备(这是它将来能取代Apache的重要因素),编码风格优美, 是学习网络编程,熟悉http服务器编写的良好范例.在我初学网络编程的时候,就是看的lighttpd的源码进行学习,在其中学到了不少的技巧.我打算将这些写出来与别人分享,可能开始比较杂乱,也不会作完全的分析,因为很多部分的代码我也没有看过,写一点是一点吧.我进行阅读和分析的lighttpd版本是1.4.18.
lighttpd采用的是多进程+多路复用(如select,epoll)的网络模型,它对多路复用IO操作的封装将作为下一个专题的内容,本次将讲解它所采用的多进程模型.
lighttpd中的配置文件有一项server.max-worker配置的是服务器生成的工作进城数.在lighttpd中, 服务器主进程被称为watcher(监控者),而由这个主进程创建出来的子进程被称为woker(工作者),而woker的数量正是由上面提到的配置项进行配置的.watcher创建并且监控woker的代码如下所示,我觉得这是一段很巧妙的代码,在我阅读了这段代码之后,到目前为止,我所写的所有服务器采用的都是类似lighttpd的watcher-woker多进程模型,这段代码在src目录中server.c文件的main函数中:
#ifdef HAVE_FORK
/* start watcher and workers */
num_childs = srv->srvconf.max_worker;
if (num_childs > 0) {
int child = 0;
while (!child && !srv_shutdown && !graceful_shutdown) {
if (num_childs > 0) {
switch (fork()) {
case -1:
return -1;
case 0:
child = 1;
break;
default:
num_childs--;
break;
}
} else {
int status;
if (-1 != wait(&status)) {
/**
* one of our workers went away
*/
num_childs++;
} else {
switch (errno) {
case EINTR:
/**
* if we receive a SIGHUP we have to close our logs ourself as we don't
* have the mainloop who can help us here
*/
if (handle_sig_hup) {
handle_sig_hup = 0;
log_error_cycle(srv);
/**
* forward to all procs in the process-group
*
* we also send it ourself
*/
if (!forwarded_sig_hup) {
forwarded_sig_hup = 1;
kill(0, SIGHUP);
}
}
break;
default:
break;
}
}
}
}
/**
* for the parent this is the exit-point
*/
if (!child) {
/**
* kill all children too
*/
if (graceful_shutdown) {
kill(0, SIGINT);
} else if (srv_shutdown) {
kill(0, SIGTERM);
}
log_error_close(srv);
network_close(srv);
connections_free(srv);
plugins_free(srv);
server_free(srv);
return 0;
}
}
#endif
首先,woker的数量保存在变量num_childs中, 同时另一个变量child是一个标志位, 为0时是父进程(watcher), 为1时则是子进程(worker), 下面进入一个循环:
while (!child && !srv_shutdown && !graceful_shutdown)
也就是说只要是父进程而且服务器没有被关闭该循环就一直进行下去, 接着如果num_childs>0就执行fork函数创建子进程, 对于子进程而言,赋值child=1, 同时num_childs值-1, 于是子进程不满足!child这个条件退出循环继续执行下面的代码, 而父进程还在循环中, 如果num_childs=0也就是所有的子进程都创建成功了, 那么父进程就阻塞在调用wait函数中, 等待着一旦有子进程退出, 那么wait函数返回, 这样num_childs+1, 于是继续前面的调用, 再次创建出子进程.
这也就是watcher和worker的来历:父进程负责创建子进程并且监控是否有子进程退出, 如果有, 那么再次创建出子进程;而子进程是worker, 是具体执行服务器操作的工作者, 在被创建完毕之后退出循环, 去做下面的事情.而如果父进程退出这个循环, 那么一定是srv_shutdown或者graceful_shutdown之一变为了非零值, 所以在循环外, 还要进行判断, 如果是父进程, 那么就是服务器程序要退出了, 最后作一些清理的工作.
用伪码表示这部分代码就是:
如果(是父进程 而且 当前没有要求终止服务器) 就一直循环下去
{
如果还有未创建的子进程
{
创建出一个子进程
如果是子进程, 那么根据最上面的循环条件退出这个循环.
如果是父进程, 那么将未创建的子进程数量 - 1
}
否则 就是没有未创建的子进程
{
一直保持睡眠, 一旦发现有子进程退出父进程就苏醒, 将未创建的子进程数量 + 1;
}
}
父进程的代码永远不会执行到这个循环体之外, 一旦发生, 就是因为要终止服务器的运行, 如果这种情况发生, 就进行最后的一些清理工作....
在这之后, 各子进程分道扬镳, 各自去进行自己的工作, 互不干扰.这也是我非常喜欢多进程编程的原因, 少了多线程编程中考虑到数据同步等麻烦的事情,要考虑的事情相对而言简单的多了.
关于多进程 VS 多线程的话题, 不在这里多加阐述了.