霜天部落 | 关注LAMP高性能、高并发架构的设计与研究

如何编写一个可靠的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小时的运行才是有保证的