博客首页 注册 建议与交流 排行榜 加入友情链接
推荐 投诉 搜索: 帮助

Free Gentux

Free & Open
   xiaosuo.cublog.cn
关于作者  
姓名:xiaosuo
职业:Linuxer
年龄:Not very young
位置:TianJin
个性介绍:Free & Open.

我的分类  




用core dump和错误自动重启技术提高软件可用性
一些比较关键的应用,我们总是希望它的可用性尽可能高,换句话就是尽量不要让服务中断,或者是中断的时间最短。为了达到这种目的,通常有以下几种解决方案:
  • 多机备份:提供同一应用的多个实例,用特定的“心跳”检测机制探测服务状态,当一个实例发生问题时,及时将服务切换到另一个实例上以确保服务的持续性,当然同时运行多个实例,将服务在其间动态调动也能达到目的,并且比较经济,需要注意的是这个时候分发器可能发生单点错误,仍旧需要保证高可用性。
  • 看门狗(WatchDog):用另外一个程序监控同一主机上的应用,当应用意外退出的时候,看门狗负责重启相应服务。systemV中init进程就能对action是respwan的程序提供这种功能,当然您还可以自己实现。除了软件看门狗,一些设备还提供有硬件的看门狗,他们负责在系统崩溃的时候,重启系统。
在编程实践中,为了提高程序的可用性,有时用多进程复合多线程的系统结构取代单纯的多线程结构或者是单线程结构,有人认为这是一种“倒退”,姑且不论是否是“倒退”,单从它对有效性的好处来说,似乎都有些牵强。

而看门狗所引入的附加进程多少也会让有些洁癖的人感到不爽,那么有没有更加干净的方法呢?

偶然想到:在Linux平台上,程序的非法运行,一般都会触发特定的信号(其中“段错误”信号SIGSEGV占多数),这些信号直接导致进程退出。既然如此,我们就可以在信号的处理函数中通过exec系列系统调用重新载入程序运行,这岂不是就能省掉看门狗进程。程序如下:

static void _restart(void)
{
        char buf[4096];
        char exe[512];
        char **argv;
        int argv_size, fd, i;
        ssize_t len;
        char *ptr, *ptr_end;
        struct rlimit limit;

        /* find the file executable. */
        len = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
        if (len == -1 || len == sizeof(exe) - 1)
                _exit(EXIT_FAILURE);
        exe[len] = '\0';

        /* generate the variable argv. */
        fd = open("/proc/self/cmdline", O_RDONLY);
        if (fd == -1)
                _exit(EXIT_FAILURE);
        len = read(fd, buf, sizeof(buf));
        if (len == -1 || len == sizeof(buf))
                _exit(EXIT_FAILURE);

        buf[len] = '\0';
        argv_size = 16;
        argv = malloc(sizeof(char*) * argv_size);
        if (argv == NULL)
                _exit(EXIT_FAILURE);
        for (i = 0, ptr = buf, ptr_end = buf + len;
                        ptr < ptr_end; i ++, ptr += strlen(ptr) + 1) {
                if (i >= argv_size - 1) {
                        argv_size <<= 1;
                        argv = realloc(argv, sizeof(char*) * argv_size);
                        if (argv == NULL)
                                _exit(EXIT_FAILURE);
                }
                argv[i] = ptr;
        }
        argv[i] = NULL;

        /* close all the file descriptors except of stdin/stdout/stderr. */
        getrlimit(RLIMIT_NOFILE, &limit);
        for (i = 3; i < limit.rlim_cur; i ++)
                close(i);

        execvp(exe, argv);

        /* exit if it fails to restart self. */
        _exit(EXIT_FAILURE);
}



程序比较简单,也就不用我再费唇舌解释了。

如果能保存错误发生的现场(即core dump文件)就更好了,为此我翻看了Linux的系统调用,比较遗憾的是没有任何发现。经过几天的冥思苦想之后,终于得到了如下的解决方案:

static pid_t _core_dump(int signo)
{
        pid_t pid;

        pid = fork();
        if (pid == 0) {
#ifdef _FORCE_CORE_DUMP
#ifndef _CORE_SIZE
#define _CORE_SIZE (256 * 1024 * 1024)
#endif /* _CORE_SIZE */
                struct rlimit limit = {
                        .rlim_cur = _CORE_SIZE,
                        .rlim_max = _CORE_SIZE };

                setrlimit(RLIMIT_CORE, &limit);
#endif /* _FORCE_CORE_DUMP */
                /* reset the signal handler to default handler,
                 * then raise the corresponding signal. */

                signal(signo, SIG_DFL);
                raise(signo);
        }

        return pid;
}


这是一个可以在信号处理器中调用的函数,如果原来的信号就能引起core dump,我们就fork出一个子进程,重设此子进程的相应处理器为系统默认值(引起core dump),然后通过系统调用raise手动触发此信号,这样core dump就产生了。它和_restart配合实现的信号处理器函数如下:

static void _dump_and_restart(int signo)
{
        if (_core_dump(signo) == 0)
                return;
        _restart();
}


应用程序只需在程序的开头部分安装相应信号的信号处理器为_dump_and_restart即可:

static int _core_dump_signals[] = {
        SIGABRT, SIGFPE, SIGILL, SIGQUIT, SIGSEGV,
        SIGTRAP, SIGSYS, SIGBUS, SIGXCPU, SIGXFSZ,
#ifdef SIGEMT
        SIGEMT
#endif
};

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#endif

void debug_start(void)
{
        struct sigaction act;
        int i;

        memset(&act, 0, sizeof(act));
        act.sa_handler = _dump_and_restart;
        sigfillset(&act.sa_mask);
        for (i = 0; i < ARRAY_SIZE(_core_dump_signals); i ++)
                sigaction(_core_dump_signals[i], &act, NULL);
        /* unblock all the signals, because if the current process is
         * spawned in the previous signal handler, all the signals are
         * blocked. In order to make it sense of signals, we should
         * unblock them. Certainly, you should call this function as
         * early as possible. :) */

        sigprocmask(SIG_UNBLOCK, &act.sa_mask, NULL);

}


上面的函数还有一个小的细节,就是在信号处理器函数运行的时候屏蔽一切可以屏蔽的信号,以防止其它信号意外中断信号处理器函数的运行,但是因为exec系列系统调用会保留信号的屏蔽码,所以在安装好信号处理器之后,解除可能的信号阻塞。

虽然我已经尽可能谨慎,但是仍然无法避免考虑不周,所以不保证不发生任何意外,请用者自酌,这只是一个想法而已!

注意:以上方法对程序的严重错误无效,如:内存泄漏导致操作系统内存溢出,进而被操作系统强制杀死;或者是进程的数据被大规模破坏;还有程序中的死循环。这些情况下针对特定服务的心跳健康检测就显得完备得多了!

 发表于: 2007-10-10,修改于: 2007-10-27 00:02 已浏览1176次,有评论3条 推荐 投诉

  网友评论
  nkshili 时间:2007-10-11 18:24:03 IP地址:60.28.145.★
师父果然是技术大牛啊

  本站网友 时间:2007-10-19 16:48:23 IP地址:121.35.144.★
我用上面的代码测试,程序在coredump时确实可以重启,但是系统中产生很多僵尸进程,不知道该如何解决

  xiaosuo 时间:2007-10-19 17:15:45 IP地址:60.27.155.★
以上代码是为多线程(2.6的nptl线程库,linuxthreads线程库是用多进程模拟多线程的,不属于此类)或者是单进程的程序设计的。如果是多进程的程序,如果有必要的话,还需要在信号回掉函数中杀掉这个程序的其他进程,并且因为exec系列函数会clear未处理的信号集,所以那些僵尸进程应该是因为漏处理的信号所致,建议在debug_start()函数中以非阻塞方式调用waitpid直到没有任何子进程需要收尸。当然,最好还是不要在多进程程序中使用上述技巧,如果是处理任务的子进程core dump了,可能会产生混乱。


  发表评论



Copyright © 2001-2006 ChinaUnix.net All Rights Reserved

感谢所有关心和支持过ChinaUnix的朋友们
页面生成时间:0.01276

京ICP证041476号