1. 程式人生 > >Lighttpd1.4.20原始碼分析 筆記 網路服務主模型

Lighttpd1.4.20原始碼分析 筆記 網路服務主模型

一.概述

Lighttpd採用多程序網路服務模型。

程序分兩種:監控程序watcher 和 工作程序 workers。

監控程序:fork工作程序並監視工作程序的數目,一旦有工作程序退出,監控程序立即fork新的工作程序。

工作程序:接收客戶端請求並做出服務響應。

一般情況下,存在一個監控程序和多個工作程序。

max-worker值預設為0時,沒有監控程序,只有一個工作程序。

關於初始化:Lighttpd很多地方記憶體申請都是採用calloc,malloc()和calloc()的主要區別是前者不能初始化所分配的記憶體空間,而後者能。

主流程入口檔案:server.c

二.Lighttpd程序守護化

main函式中的函式daemonize呼叫使得Lighttpd程序轉換為守護程序,從而脫離控制終端,在後臺提供服務,避免了執行過程中的資訊在終端上顯示,也避免了服務被終端資訊打斷。
(啟動時假如選項D可不守護化)

下面我們來分析下:

#ifdef HAVE_FORK
static void daemonize(void) {
#ifdef SIGTTOU
/* 下面用於遮蔽一些有關控制終端操作的訊號,防止守護程序沒有正常運作之前控制終端受到干擾退出或掛起 */
    signal(SIGTTOU, SIG_IGN); //忽略後臺程序寫控制終端訊號
#endif
#ifdef SIGTTIN
signal(SIGTTIN, SIG_IGN); //忽略後臺程序讀控制終端訊號 #endif #ifdef SIGTSTP signal(SIGTSTP, SIG_IGN); //忽略終端掛起 #endif /* 下面開始從普通程序轉換為守護程序 * 目標1:後臺執行。 * 做法:脫離控制終端->呼叫fork之後終止父程序,子程序被init收養,此步達到後臺執行的目標。 */ if (0 != fork()) exit(0); /*,目標2:脫離控制終端,登陸會話和程序組。 * 做法:使用setsid建立新會話,成為新會話的首程序,則與原來的 * 登陸會話和程序組自動脫離,從而脫離控制終端。 * (上一步的fork保證了子程序不可能是一個會話的首程序,這是呼叫setsid的必要條件) */
if (-1 == setsid()) exit(0); /* 上面已經完成了大部分工作,但是有的系統上,當會話首程序開啟 * 一個尚未與任何會話相關聯的終端裝置時,該裝置自動作為控制 * 終端分配給該會話。 * 為避免該情況,我們再次fork程序,於是新程序不再是會話首程序。 * 會話首程序退出時可能會給所有會話內的程序傳送SIGHUP,而該 * 訊號預設是結束程序,故需要忽略該訊號來防止孫子程序意外結束。 */ signal(SIGHUP, SIG_IGN); if (0 != fork()) exit(0); /* 最後目標:改變工作目錄到根目錄。 * 原因:程序活動時,其工作目錄所在的檔案系統不能卸下。 */ if (0 != chdir("/")) exit(0); } #endif

程序屬於一個程序組(一個或多個程序的集合),登陸會話是包含一個或多個程序組的集合,這些程序組共享一個控制終端。

三.多程序網路服務模型

Lighttpd一開始是單程序的,在完成一組公共操作後開始轉換為多程序。

程式碼框架如下:

這裡寫圖片描述

具體程式碼分析註釋:

#ifdef USE_ALARM
    signal(SIGALRM, signal_handler);

    /* setup periodic timer (1 second) */
    if (setitimer(ITIMER_REAL, &interval, NULL)) { //每隔一秒產生一個ALARM
        log_error_write(srv, __FILE__, __LINE__, "s", "setting timer failed");
        return -1;
    }

    getitimer(ITIMER_REAL, &interval);
#endif

#ifdef HAVE_FORK
    /* start watcher and workers */
    num_childs = srv->srvconf.max_worker; //存放最大的子程序的數目
    if (num_childs > 0) {
        int child = 0; //child變數用於標記是否為子程序,0代表父程序,1代表子程序
        while (!child && !srv_shutdown && !graceful_shutdown) { //子程序不可進入,srv_shutdown=1 或 graceful_shutdown=1時父程序跳出
            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只調用一次
                                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

下面的是子程序部分的關鍵程式碼分析:

一開始子程序進行各種初始化工作,包括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中。

4、如果socket沒有失效,判斷伺服器是否過載。如果過載了,則關閉所有連線,清理伺服器並退出伺服器。

5、分配檔案描述符。

6、啟動事件輪詢。等待各種IO時間的發生。包括檔案讀寫,socket請求等。

7、一旦有事件發生,呼叫相應的處理函式進行處理。

8、最後,檢查joblist中是否有未處理的job並處理之。

至此,一次迴圈結束了。然後,繼續迴圈直到伺服器關閉。

    /* main-loop */
    while (!srv_shutdown) { //只要srv_shutdown不為1,工作程序持續執行
        int n;
        size_t ndx;
        time_t min_ts;

        if (handle_sig_hup) { //如果收到HUP訊號
            handler_t r;

            /* reset notification */
            handle_sig_hup = 0; //重置標識


            /* cycle logfiles */

            switch(r = plugins_call_handle_sighup(srv)) { //通過plugins_call_handle_sighup來呼叫各個模組的HUP處理函式
            case HANDLER_GO_ON:
                break;
            default:
                log_error_write(srv, __FILE__, __LINE__, "sd", "sighup-handler return with an error", r);
                break;
            }

            if (-1 == log_error_cycle(srv)) { //重新開啟日誌檔案,並寫入收到HUP訊號到日誌。此處並沒有重新讀取配置檔案
                log_error_write(srv, __FILE__, __LINE__, "s", "cycling errorlog failed, dying");

                return -1;
            } else {
#ifdef HAVE_SIGACTION
                log_error_write(srv, __FILE__, __LINE__, "sdsd", 
                    "logfiles cycled UID =",
                    last_sighup_info.si_uid,
                    "PID =",
                    last_sighup_info.si_pid);
#else
                log_error_write(srv, __FILE__, __LINE__, "s", 
                    "logfiles cycled");
#endif
            }
        }

        if (handle_sig_alarm) { //收到ALARM訊號
            /* a new second */

#ifdef USE_ALARM
            /* reset notification */
            handle_sig_alarm = 0;
#endif

            /* get current time */
            min_ts = time(NULL);

            if (min_ts != srv->cur_ts) {
                int cs = 0;
                connections *conns = srv->conns;
                handler_t r;

                switch(r = plugins_call_handle_trigger(srv)) { //呼叫plugins_call_handle_trigger來處理各個模組的ALARM訊號處理函式
                case HANDLER_GO_ON:
                    break;
                case HANDLER_ERROR:
                    log_error_write(srv, __FILE__, __LINE__, "s", "one of the triggers failed");
                    break;
                default:
                    log_error_write(srv, __FILE__, __LINE__, "d", r);
                    break;
                }

                /* trigger waitpid */
                srv->cur_ts = min_ts; //更新伺服器記錄時間

                /* cleanup stat-cache */
                stat_cache_trigger_cleanup(srv); //清除快取,刪除一些比較舊的節點
                /**
                 * check all connections for timeouts
                 *
                 */
                for (ndx = 0; ndx < conns->used; ndx++) { //處理超時連線
                    int changed = 0;
                    connection *con;
                    int t_diff;

                    con = conns->ptr[ndx];

                    if (con->state == CON_STATE_READ ||
                        con->state == CON_STATE_READ_POST) {
                        if (con->request_count == 1) {
                            if (srv->cur_ts - con->read_idle_ts > con->conf.max_read_idle) {
                                /* time - out */
#if 0
                                log_error_write(srv, __FILE__, __LINE__, "sd",
                                        "connection closed - read-timeout:", con->fd);
#endif
                                /* lighttpd 中採用了狀態機(state-engine)去處理每一個連線,
                                 * 狀態機中的每一種節點表示連線當時所處的狀態,包括 connect 、
                                 * reqstart 、 read 、 reqend 、 readpost 、handlereq、
                                 * respstart、write、respend、error、close 這 11 個狀態
                                 */
                                connection_set_state(srv, con, CON_STATE_ERROR); //呼叫connection_set_state進行狀態機的狀態轉換
                                changed = 1;
                            }
                        } else {
                            if (srv->cur_ts - con->read_idle_ts > con->conf.max_keep_alive_idle) {
                                /* time - out */
#if 0
                                log_error_write(srv, __FILE__, __LINE__, "sd",
                                        "connection closed - read-timeout:", con->fd);
#endif
                                connection_set_state(srv, con, CON_STATE_ERROR);
                                changed = 1;
                            }
                        }
                    }
        ………………
                    /* we don't like div by zero */
                    if (0 == (t_diff = srv->cur_ts - con->connection_start)) t_diff = 1;
                    /* 處理傳輸速度限制
                     * 如果某一時刻平均傳輸速度達到了使用者設定的最大值,則停止傳送資料(con->traffic_limit_reached將被設為1,
                     * 進入下面if中處理)。只要檢測到平均傳輸速度小於使用者設定的最大值就繼續傳送資料,
                     * 則滿足if的條件,con->traffic_limit_reached設為 0,同時呼叫狀態機切換函式。
                     */                 
                    if (con->traffic_limit_reached &&
                        (con->conf.kbytes_per_second == 0 ||
                         ((con->bytes_written / t_diff) < con->conf.kbytes_per_second * 1024))) {
                        /* enable connection again */
                        con->traffic_limit_reached = 0;

                        changed = 1;
                    }

                    if (changed) {
                        connection_state_machine(srv, con);
                    }
                    con->bytes_written_cur_second = 0;
                    *(con->conf.global_bytes_per_second_cnt_ptr) = 0;

#if 0
                    if (cs == 0) {
                        fprintf(stderr, "connection-state: ");
                        cs = 1;
                    }

                    fprintf(stderr, "c[%d,%d]: %s ",
                        con->fd,
                        con->fcgi.fd,
                        connection_get_state(con->state));
#endif
                }

                if (cs == 1) fprintf(stderr, "\n");
            }
        }

        /* 根據當前的資源利用情況禁用或啟用 server sockets 服務
         *
         * 禁用:當檔案描述符(當前檔案描述符和等待檔案描述符之和)大於 0.9 倍伺服器最大
         * (系統允許或使用者設定)檔案描述符數目或當前連線大於最大(系統允許或使用者設定)連線
         * 數目或收到終止伺服器指令時。
         *
         * 啟用:當檔案描述符(當前檔案描述符和等待檔案描述符之和)小於 0.8 倍伺服器最大
         * (系統允許或使用者設定)檔案描述符數目並且當前連線小於 0.9 倍最大(系統允許或使用者設
         * 置)連線數目並且終止伺服器標誌為 0 時。
         */
        if (srv->sockets_disabled) {
            /* our server sockets are disabled, why ? */

            if ((srv->cur_fds + srv->want_fds < srv->max_fds * 0.8) && /* we have enough unused fds */
                (srv->conns->used < srv->max_conns * 0.9) &&
                (0 == graceful_shutdown)) {
                for (i = 0; i < srv->srv_sockets.used; i++) {
                    server_socket *srv_socket = srv->srv_sockets.ptr[i];
                    fdevent_event_add(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd, FDEVENT_IN);
                }

                log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets enabled again");

                srv->sockets_disabled = 0;
            }
        } else {
            if ((srv->cur_fds + srv->want_fds > srv->max_fds * 0.9) || /* out of fds */
                (srv->conns->used > srv->max_conns) || /* out of connections */
                (graceful_shutdown)) { /* graceful_shutdown */

                /* disable server-fds */

                for (i = 0; i < srv->srv_sockets.used; i++) { //逐個刪除該工作程序上的所有在 socket 描述符上的事件監聽器(通過 fdevent_event_del 函式)。
                    server_socket *srv_socket = srv->srv_sockets.ptr[i];
                    fdevent_event_del(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd);

                    if (graceful_shutdown) { //如果是關閉伺服器則登出事件監聽器結構(主要是釋放記憶體空間,防止記憶體洩露)並且關閉檔案描述符、刪除附屬檔案。
                        /* we don't want this socket anymore,
                         *
                         * closing it right away will make it possible for
                         * the next lighttpd to take over (graceful restart)
                         *  */

                        fdevent_unregister(srv->ev, srv_socket->fd);
                        close(srv_socket->fd);
                        srv_socket->fd = -1;

                        /* network_close() will cleanup after us */

                        if (srv->srvconf.pid_file->used &&
                            srv->srvconf.changeroot->used == 0) {
                            if (0 != unlink(srv->srvconf.pid_file->ptr)) {
                                if (errno != EACCES && errno != EPERM) {
                                    log_error_write(srv, __FILE__, __LINE__, "sbds",
                                            "unlink failed for:",
                                            srv->srvconf.pid_file,
                                            errno,
                                            strerror(errno));
                                }
                            }
                        }
                    }
                }

                if (graceful_shutdown) {
                    log_error_write(srv, __FILE__, __LINE__, "s", "[note] graceful shutdown started");
                } else if (srv->conns->used > srv->max_conns) {
                    log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets disabled, connection limit reached");
                } else {
                    log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets disabled, out-of-fds");
                }

                srv->sockets_disabled = 1;
            }
        }
if (graceful_shutdown && srv->conns->used == 0) {
            /* we are in graceful shutdown phase and all connections are closed
             * we are ready to terminate without harming anyone */
            srv_shutdown = 1;
        }

        /* we still have some fds to share */
        if (srv->want_fds) { //如果有待處理的檔案描述符,則通過狀態機切換函式進行處理,為了合理利用資源,程式會保證至少有 16 個空閒檔案描述符
            /* check the fdwaitqueue for waiting fds */
            int free_fds = srv->max_fds - srv->cur_fds - 16;
            connection *con;

            for (; free_fds > 0 && NULL != (con = fdwaitqueue_unshift(srv, srv->fdwaitqueue)); free_fds--) {
                connection_state_machine(srv, con);

                srv->want_fds--;
            }
        }
        /* 通過 fdevent_poll -> epoll_wait(以 USE_LINUX_EPOLL 為例)來輪詢 I/O 事件的發生,
         * 其中等待 I/O 事件發生的超時值 timeout_ms=1000milliseconds,即 1 秒。
         * 如果在等待的這 1 秒內有 I/O 事件發生,則返回的 n 值記錄事件數目,隨後用一個 do-while
         * 迴圈對每一個發生的 I/O 事件進行處理。
         */
        if ((n = fdevent_poll(srv->ev, 1000)) > 0) {
            /* n is the number of events */
            int revents;
            int fd_ndx;
#if 0
            if (n > 0) {
                log_error_write(srv, __FILE__, __LINE__, "sd",
                        "polls:", n);
            }
#endif
            fd_ndx = -1;
            do {
                fdevent_handler handler;
                void *context;
                handler_t r;

                fd_ndx  = fdevent_event_next_fdndx (srv->ev, fd_ndx); //獲得發生了 I/O 事件的檔案描述符在 fdarray 中的索引
                revents = fdevent_event_get_revent (srv->ev, fd_ndx); //獲得該檔案描述符上發生的 I/O 事件型別
                fd      = fdevent_event_get_fd     (srv->ev, fd_ndx); //獲得該檔案描述符
                handler = fdevent_get_handler(srv->ev, fd);           //獲得 I/O 事件處理的回撥函式
                context = fdevent_get_context(srv->ev, fd);           //獲得 I/O 事件處理的上下文環境

                /* connection_handle_fdevent needs a joblist_append */
#if 0
                log_error_write(srv, __FILE__, __LINE__, "sdd",
                        "event for", fd, revents);
#endif
                switch (r = (*handler)(srv, context, revents)) {      //呼叫回撥函式進行 I/O 事件處理,並傳入相關引數。
                case HANDLER_FINISHED:
                case HANDLER_GO_ON:
                case HANDLER_WAIT_FOR_EVENT:
                case HANDLER_WAIT_FOR_FD:
                    break;
                case HANDLER_ERROR:
                    /* should never happen */
                    SEGFAULT();
                    break;
                default:
                    log_error_write(srv, __FILE__, __LINE__, "d", r);
                    break;
                }
            } while (--n > 0);
        } else if (n < 0 && errno != EINTR) {
            log_error_write(srv, __FILE__, __LINE__, "ss",
                    "fdevent_poll failed:",
                    strerror(errno));
        }

參考資料:《Lighttpd原始碼分析》 高群凱 編著