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模型雖然簡單,但是在實際的運行過程中也非常的高效。有的時候,簡單並不一定就不高效。