天天看點

如何編寫一個可靠的linux守護程序

    linux服務端程式都需要提供7 * 24不間斷的服務,如何保證工作程序一直不退出或者不被kill掉,常見的方法就是啟動一個守護程序來檢測工作程序的狀态,如果發現工作程序退出,就再fork一個出來。一般的實作見下面一段代碼:

// 守護程序(父程序)
int status;
for ( ; ; )  {
    if ( 0 == ( pid = fork()) ) {
        // 工作程序(子程序)
        run();
    }
    waitpid(-1, &status, 0);
    if (WIFEXITED(status))
            if (WEXITSTATUS(status) == 0)
                exit(0);
        if (WIFSIGNALED(status)) {
            switch (WTERMSIG(status)) {
            case SIGKILL:
                exit(0);
                break;
            case SIGINT:
            case SIGTERM:
                exit(1);
            default:
                break;
            }
        }
}      

    守護程序fork出工作程序之後,就阻塞在waitpid系統調用,等待工作程序的退出,waitpid傳回之後,根據status選擇繼續fork工作程序還是退出守護程序。status為int型變量,但隻用到了低16位(見struct wait),0-6位表示使子程序退出的信号(可以通過 $kill -l 檢視信号的值),8-15位表示子程序退出時的傳回碼(exit或者return的值)。

struct wait{
# if    __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int __w_termsig:7; /* Terminating signal.  */
    unsigned int __w_coredump:1; /* Set if dumped core.  */
    unsigned int __w_retcode:8; /* Return code if exited normally.  */
    unsigned int:16;
# endif             /* Little endian.  */
};      

     判斷status的狀态可以通過下面的宏完成:

WIFEXITED(status)               //子程序調用exit()或者從main return退出時為true;
WEXITSTATUS(status)        //在WIFEXITED為true時,表示exit()或return的傳回碼;
WIFSIGNALED(status)        //子程序被信号終止時為true;
WTERMSIG(status)            //在WIFSIGNALED為true時,表示終止子程序信号的值;      

    宏的定義如下:

/* If WIFEXITED(STATUS), the low-order 8 bits of the status.  */
#define __WEXITSTATUS(status)   (((status) & 0xff00) >> 8)
/* If WIFSIGNALED(STATUS), the terminating signal.  */
#define __WTERMSIG(status)  ((status) & 0x7f)
/* Nonzero if STATUS indicates normal termination.  */
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
/* Nonzero if STATUS indicates termination by a signal.  */
#define __WIFSIGNALED(status) \
  (((signed char) (((status) & 0x7f) + 1) >> 1) > 0)      

    是以根據waitpid傳回的status,守護程序可以清楚地知道工作程序的死因,但上面的程式有兩個問題:     1. 對SIGKILL, SIGINT, SIGTERM信号,守護程序直接退出了,沒有fork工作程序出來;     2. 守護程序被kill掉了,工作程序就隻能裸奔了。     對于第一個問題:SIGKILL有可能是人為($kill -9 pid)發出的,也有可能是工作程序占用記憶體過多,被OOM掉了(關于OOM參見 這裡),都不是我們想要的結果,是以工作程序被SIGKILL掉,守護程序一定要将其重新開機。SIGINT是 CTRL+C 發出的(非daemon模式下),SIGTERM是 $killall servicename(或者 $kill pid)發出的,這兩個信号都是在結束程序的時候用到,這個時候工作程序應該捕獲被處理這兩個信号,正常地退出(return 0;)。而守護程序隻有在工作程序正常退出的情況下才完成自己的使命,否則(工作程序被其他信号結束掉,如SIGABRT, SIGKILL, SIGSEGV)重新開機工作程序。是以守護程序和工作程序的實作應該是下面的代碼邏輯:

// 守護程序(父程序)
int status;
for ( ; ; )  {
    if ( 0 == ( pid = fork()) ) {
        // 工作程序(子程序)
        run();
        //信号處理函數signal_handler
        if (sig == SIGTERM || sig == SIGINT) {
           destroy();
           return 0;
        }
    }
    waitpid(-1, &status, 0);
    if (WIFEXITED(status) && (WEXITSTATUS(status) == 0)) exit(0);
}      

     對于第二個問題:守護程序的監控可以用daemontools工具集中的supervise來監控,也可以自己實作,但是不能隻是通過另外一個應用程式去做,因為做守護的程序自身也需要被守護,如此循環解決不了問題。這個時候就要借助于linux系統的init程序了,因為init程序是不能被信号kill掉的(強大到無視SIGKILL)。

The  only  signals  that can be sent task number one, the init process, are those for which init has explicitly installed signal handlers.  This is done to assure the system is not brought down accidentally.      

    是以讓init程序來守護我們的應用程式是最可靠的,看看supervise的作法:     在/etc/inittab中添加如下一行:

SV1:23:respawn:/usr/local/bin/svscanboot      

    每行用“:”分隔開為4個部分:         id - 該行的辨別,自定義的名稱SV1。         runlevels - 該行為應該發生的運作級的清單,這裡在level 2和level 3下運作。         action - 應發生的行為,respawn表示init應該監視這個程序,即使其結束後也應該被重新啟動。         process - 應由init啟動的程式的路徑。     修改完成後,可以通過$kill -HUP 1 來立刻生效。     解決了上面兩個問題,守護程序和工作程序提供7 * 24小時的運作才是有保證的。Have fun!