Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2034062
  • 博文数量: 369
  • 博客积分: 10093
  • 博客等级: 上将
  • 技术积分: 4271
  • 用 户 组: 普通用户
  • 注册时间: 2005-03-21 00:59
文章分类

全部博文(369)

文章存档

2013年(1)

2011年(2)

2010年(10)

2009年(16)

2008年(33)

2007年(146)

2006年(160)

2005年(1)

分类: LINUX

2007-10-10 23:56:30

一些比较关键的应用,我们总是希望它的可用性尽可能高,换句话就是尽量不要让服务中断,或者是中断的时间最短。为了达到这种目的,通常有以下几种解决方案:
  • 多机备份:提供同一应用的多个实例,用特定的“心跳”检测机制探测服务状态,当一个实例发生问题时,及时将服务切换到另一个实例上以确保服务的持续性,当然同时运行多个实例,将服务在其间动态调动也能达到目的,并且比较经济,需要注意的是这个时候分发器可能发生单点错误,仍旧需要保证高可用性。
  • 看门狗(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系列系统调用会保留信号的屏蔽码,所以在安装好信号处理器之后,解除可能的信号阻塞。

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

注意:以上方法对程序的严重错误无效,如:内存泄漏导致操作系统内存溢出,进而被操作系统强制杀死;或者是进程的数据被大规模破坏;还有程序中的死循环。这些情况下针对特定服务的心跳健康检测就显得完备得多了!
阅读(4716) | 评论(6) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2009-12-29 15:40:39

嗯,xiaosuo,我的系统部分实现了core之后,将子进程全部杀死,然后退出,退出之后,外部的监控dog会把服务又给拉起来,呵呵。 我的qq:179030145,欢迎交流,如果能有个群就好啦!

xiaosuo2009-05-17 19:11:55

感觉你所提到的比较难实现,因为内存写可能就是一条简单的赋值操作,也许可以在操作系统里面将对应页设成COW以保存修改前副本,但是这个时候我们又必须知道什么时候完整的内存操作才完成,可能涉及大量代码,不太现实。

chinaunix网友2009-05-16 21:58:25

楼主,你好!这几篇文章都看了,写得很好。 我有一个想法:能否通过注入的方式追踪当前某类进程读写内存状态并记录写操作(比如访问同一服务的多个进程)。如果过程中发生错误,可以根据日志文件恢复至访问前安全状态呢? 很真诚的希望能与你交流,我的email:liaotoad@163.com.平时看blog不是太多,能给我发邮件吗? 谢谢!