lighttpd的工作模型很简单──一个主进程加多个工作进程的多进程模型,也就是所谓的watcher-worker模型。
整个程序的入口(main函数)在server.c文件中。在main函数的开始部分必然是处理参数和各种繁杂的初始化工作。其中有两个地方要重点看一起。第一个是下面的语句:
1
if
(test_config)
//
没有进行任何测试。。。
2
{
3
printf(
"
Syntax OK\n
"
);
4
}
这个If语句是为了判断配置文件的语法是否合法。但是,明显,没有进行任何的测试,而仅仅是输出了一句话。
第二个是函数daemonize()。这个函数的作用是使程序进入daemon。函数的详细内容如下:
1 static void daemonize(void)
2
{
3
/*
4
* 忽略和终端读写有关的信号
5
*/
6
#ifdef SIGTTOU
7
signal(SIGTTOU, SIG_IGN);
8
#endif
9
#ifdef SIGTTIN
10
signal(SIGTTIN, SIG_IGN);
11
#endif
12
#ifdef SIGTSTP
13
signal(SIGTSTP, SIG_IGN);
14
#endif
15
if
(
0
!=
fork())
/*
产生子进程,父进程退出
*/
16
exit(
0
);
17
if
(
-
1
==
setsid())
/*
设置子进程的设置ID
*/
18
exit(
0
);
19
signal(SIGHUP, SIG_IGN);
/*
忽略SIGHUP信号
*/
20
if
(
0
!=
fork())
/*
再次产生子进程,父进程退出
*/
21
exit(
0
);
22
if
(
0
!=
chdir(
"
/
"
))
/*
更改工作目录为根目录
*/
23
exit(
0
);
24
}
这里作者使用了标准的产生*nix daemon的方法。两次调用fork并退出父进程。具体的原因读者可以参阅《Unix环境高级编程》(APUE)中有关daemon的讲解部分。
顺着main函数继续向下走,沿途的各种初始化工作尽可忽略。下面的语句是本文的重点!
1 /*
2
* 下面程序将产生多个子进程。这些子进程成为worker,
3
* 也就是用于接受处理用户的连接的进程。而当前的主进程将
4
* 成为watcher,主要工作就是监视workers的工作状态,
5
* 当有worker因为意外而退出时,产生新的worker。
6
* 在程序退出时,watcher负责停止所有的workers并清理资源。
7
*/
8
int
child
=
0
;
//
用来标记这个进程是子进程还是父进程。
9
//
当子进程返回到这个while循环的开始的时候,由于标记
10
//
进程是子进程,流程直接跳出while循环执行后面的程序。
11
//
而对于父进程,则继续产生子进程。
12
while
(
!
child
&&
!
srv_shutdown
&&
!
graceful_shutdown)
13
{
14
if
(num_childs
>
0
)
//
watcher继续产生worker
15
{
16
switch
(fork())
17
{
18
case
-
1
:
//
出错
19
return
-
1
;
20
case
0
:
//
子进程进入这个case
21
child
=
1
;
22
break
;
23
default
:
//
父进程进入这个case
24
num_childs
--
;
25
break
;
26
}
27
}
28
else
//
watcher
29
{
30
/*
*
31
* 当产生了足够的worker时,watcher就在这个while
32
* 中不断的循环。
33
* 一但发现有worker退出(进程死亡),立即产生新的worker。
34
* 如果发生错误并接受到SIGHUP信号,向所有的进程
35
*(父进程及其子进程)包括自己发送SIGHUP信号。
36
* 并退出。
37
*/
38
int
status;
39
40
if
(
-
1
!=
wait(
&
status))
41
{
42
/*
*
43
* one of our workers went away
44
*/
45
num_childs
++
;
46
}
47
else
48
{
49
switch
(errno)
50
{
51
case
EINTR:
52
/*
*
53
* if we receive a SIGHUP we have to close our
54
* logs ourself as we don't
55
* have the mainloop who can help us here
56
*/
57
if
(handle_sig_hup)
58
{
59
handle_sig_hup
=
0
;
61
log_error_cycle(srv);
63
/*
*
64
* forward to all procs in the process-group
65
* 向所有进程发送SIGHUP信号。
(父进程及其子进程)
67
* we also send it ourself
68
*/
69
if
(
!
forwarded_sig_hup)
70
{
71
forwarded_sig_hup
=
1
;
72
kill(
0
, SIGHUP);
73
}
74
}
75
break
;
76
default
:
77
break
;
78
}end of
switch
(errno)...
79
}
//
end of if (-1 != wait(&status)) ...
80
}
//
end of if (num_childs > 0)...
81
}
//
end of while(!child...
82
在正常的运行过程中,watcher进程是不会退出上面的while循环。一旦退出了这个循环,那么也就意为着整个程序退出了。
另外,woker的数量可以在配置文件中进行配置。
子进程,也就是worker退出了上面的while循环后就开始处理连接请求等各种工作。
在子进程的一开始,还是各种初始化工作,包括fd时间处理器的初始化(fdevent_init(srv->max_fds + 1, srv->event_handler)),stat cache初始化(stat_cache_init())等。子进程工作在一个大while循环中。
while的工作流程如下:
1、判断连接是否断开。如果断开,则调用处理程序进行处理并重新开始新一轮的日志记录。
2、判断是否接受到了alarm函数发出的信号。接受到信号后,判断服务器记录的时间是否和当前时间相同。如果相同,说明时间还没有过一秒,继续处理连接请求。如果不相同,则时间已经过了一秒。那么,服务器则触发插件,清理超时连接,清理stat-cache缓存。这理里面最重要的是处理超时连接。程序中通过一个for循环查询所有的连接,比较其idle的时间和允许的最大idle时间来判断连接是否超时。如果连接超时,则让连接进入出错的状态(connection_set_state(srv, con, CON_STATE_ERROR);)。
3、判断服务器socket连接是否失效。如果失效了,则在不是服务器过载的情况下将所有连接重新加入的fdevent中。为什么服务器socket会失效呢?可以看到,在后面判断出服务器过载后,即标记了socket连接失效。srv->sockets_disabled = 1;
4、如果socket没有失效,判断服务器是否过载。如果过载了,则关闭所有连接,清理服务器并退出服务器。
5、分配文件描述符。
6、启动事件轮询。等待各种IO时间的发生。包括文件读写,socket请求等。
7、一旦有事件发生,调用相应的处理函数进行处理。
8、最后,检查joblist中是否有未处理的job并处理之。
至此,一次循环结束了。然后,从头开始继续循环直到服务器关闭。
在处理IO事件的时候,程序进入下面的循环:
1 do
2
{
3
fdevent_handler handler;
//
事件处理函数指针
4
void
*
context;
5
handler_t r;
6
//
获得下一个事件的标号
7
fd_ndx
=
fdevent_event_next_fdndx(srv
->
ev, fd_ndx);
8
//
获得这个事件的具体类型。
9
revents
=
fdevent_event_get_revent(srv
->
ev, fd_ndx);
10
//
获得事件对应的文件描述符
11
fd
=
fdevent_event_get_fd(srv
->
ev, fd_ndx);
12
//
获得事件处理函数的指针
13
handler
=
fdevent_get_handler(srv
->
ev, fd);
14
//
获得事件的环境
15
context
=
fdevent_get_context(srv
->
ev, fd);
16
/*
*
17
* 这里,调用请求的处理函数handler处理请求!
18
* 这才是重点中的重点!!
19
*/
20
switch
(r
=
(
*
handler) (srv, context, revents))
21
{
22
case
HANDLER_FINISHED:
23
case
HANDLER_GO_ON:
24
case
HANDLER_WAIT_FOR_EVENT:
25
case
HANDLER_WAIT_FOR_FD:
26
break
;
27
case
HANDLER_ERROR:
28
/*
29
* should never happen
30
*/
31
SEGFAULT();
32
break
;
33
default
:
34
log_error_write(srv, __FILE__, __LINE__,
"
d
"
, r);
35
break
;
36
}
37
}
while
(—n
>
0
);
38
这个循环是worker进程的核心部分。这里由于作者对IO系统的出色的封装。我们不须要理解这些函数的作用就可知道连接的处理流程。在程序中,作者使用回调函数轻松的解决掉了处理工种事件时判断处理函数的问题。代码优雅而高效。在lighttpd中,作者使用了大量的回调函数,这使得代码清晰易懂,效率也很高。
有一点值得注意。程序在处理连接超时的时候是每一秒中轮询所有的连接,判断其是否超时。这必然会降低服务器的效率。在实际的运行中,确实会对lighttpd的效率造成一定的影响。
lighttpd使用的watcher-worker模型虽然简单,但是在实际的运行过程中也非常的高效。有的时候,简单并不一定就不高效。