Linux 开发

1. Linux 开发

1.1. Linux 的 core 文件

1.1.1. 什么是 core 文件

core 文件是大多数 UNIX 系统实现的一种特性, 当进程崩溃时, 操作系统会将进程当前的内存映像和一部分相关的调试信息写入 core 文件, 方便人们后面对问题进行定位。

1.1.2. 哪些信号可能会产生 core 文件

操作系统里面有很多信号 (每个信号都有一个名字, 且已 SIG 开头, 用正整数表示, Linux 系统一般在 <bits/signum.h> 头文件中定义), 分别代表了不同的含义, 在 Linux 系统中, 我们可以通过 shell 命令 kill -l 来查看系统有哪些信号。操作系统收到信号时, 内核会按照以下三种方式之一去对信号进行处理:

  1. 忽略此信号。大多数的信号都可以用这种方式去处理, 即内核收到此信号时, 对进程不做任何处理, 直接忽略。但是 SIGKILL 和 SIGSTOP 这两个信号不能被忽略, 因为它们向超级用户提供了使进程终止或停止的可靠方法。

  2. 捕捉信号。即我们向内核注册一个信号处理函数, 当内核收到某个信号时, 就去调用注册的信号处理函数对信号进行处理。比如我们经常使用的命令 kill 默认发的是 SIGTERM 终止信号。注意, 不能捕捉 SIGKILL 和 SIGSTOP 信号。

  3. 执行默认动作。每个系统都有一套自己默认的信号处理函数, 即如果我们不显式的去捕捉信号, 那内核收到信号时, 要么忽略此信号, 要么执行默认的操作。可以理解为操作系统有自己默认的信号处理函数。

Linux 信号有很多, 这里我们列举出默认动作中可能产生 core 文件的信号 (摘自《UNIX 环境高级编程》第二版)

信号名字说明默认动作
SIGABRT异常终止 (调用 abort 函数产生此信号)终止 + core
SIGBUS硬件故障, 比如出现某些内存故障终止 + core
SIGEMT硬件故障终止 + core
SIGFPE算术异常, 比如除以 0, 浮点溢出等终止 + core
SIGILL非法硬件指令终止 + core
SIGIOT硬件故障终止 + core
SIGQUIT终端退出符, 比如 Ctrl+C终止 + core
SIGSEGV无效内存引用终止 + core
SIGSYS无效系统调用终止 + core
SIGXCPU超过 CPU 限制 (setrlimit)终止 + core / 忽略
SIGXFSZ超过文件长度限制 (setrlimit)终止 + core / 忽略

如果我们没有定义上述信号的信号处理函数, 那默认情况下, 内核收到这些信号, 将终止进程, 并产生该进程的 core 文件 (该进程的内存映像以及一些调试信息便保存在该 core 文件中)。

1.1.3. 如何开启与关闭 core 文件

类 UNIX 操作系统为我们提供了一个可以打开与关闭 core 文件的开关, 因为并非所有场景我们都希望可以生成 core 文件。类 UNIX 操作系统为我们提供了一个工具 ulimit 可以用来设置和查看文件大小的限制, 所以我们也可以用这个工具来查看和设置 core 大小与限制。使用 ulimit -a 可以查看系统上面所有的文件大小的限制, 比如下面是我的系统的输出结果:

allan@ubuntu:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7725
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7725
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

显示结果分为三部分, 比如第一行, 第一部分是 “core file size”, 第二部分是 “(blocks, -c)”, 第三部分是 “0”。这一行的含义是说 “core 文件的大小限制为 0(即关闭 core 文件 / 不产生 core 文件), 单位是 blocks; 使用 ulimit -c 命令可以改变此值”。这里我们还可以看到系统很多其他文件相关的设置, 比如数据段大小无限制, 文件大小无限制, 最多打开的文件数为 1024, 栈大小为 8192KB 等等。

所以很明显, 我们可以使用 ulimit -c core_size/unlimited 去设置 core 文件的大小, 注意 core_size 单位是字节。如果 core_size 为 0, 则表示不生成 core 文件, unlimited 表示对 core 文件大小不做限制。我们可以将该命令现在某个用户的环境变量里面去对不同的用户进行设置, 比如写在 / home/allan/.bashrc 文件里面只对 allan 用户有效; 也可以写在系统的环境变量里面, 对所有用户有效, 比如 / etc/.profile。

1.1.4. 如何自定义 core 文件名和目录

  1. /proc/sys/kernel/core_uses_pid 可以控制产生的 core 文件的文件名中是否添加 pid 作为扩展, 如果添加则文件内容为 1, 否则为 0。如果添加了 pid, 生成的 core 文件一般为 core.xxx(xxx 为 core dump 的进程号)。默认为 0, 即不添加进程 pid.

  2. /proc/sys/kernel/core_pattern 可以设置格式化的 core 文件保存位置或文件名, 比如我的系统的默认值为:

|/usr/share/apport/apport %p %s %c %P

我们可以使用以下命令修改此文件:

echo “/corefile/core-%e-%p-%t” > core_pattern , 这样可以将 core 文件统一生成到 / corefile 目录下, 产生的文件名为 core - 命令名 - pid - 时间戳。

这里列举一下常用的参数:

%p - insert pid into filename 添加 pid
%u - insert current uid into filename 添加当前 uid
%g - insert current gid into filename 添加当前 gid
%s - insert signal that caused the coredump into the filename 添加导致产生 core 的信号
%t - insert UNIX time that the coredump occurred into filename 添加 core 文件生成时的 unix 时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名

1.1.5. 如何查看 core 文件

我们可以使用 gdb 来查看 core 文件 (core 文件必须是完好的, 比如如果我们限制的 core 文件大小比较小, 导致 core 文件被截断, 则 gdb 查看时将出错)。查看语法如下:

gdb [exec file] [core file]

比如: gdb ./test core.3937。gdb 进去以后, 可以使用 bt 或 where 来查看进程崩溃之前的堆栈信息。

1.1.6. 一些注意事项

在 Linux 下要保证程序崩溃时生成 Coredump 要注意这些问题:

  1. 要保证存放 Coredump 的目录存在且进程对该目 录有写权限。存放 Coredump 的目录即进程的当前目录, 一般就是当初发出命令启动该进程时所在的目录。但如果是通过脚本启动, 则脚本可能会修改当前目 录, 这时进程真正的当前目录就会与当初执行脚本所在目录不同。这时可以查看 “/proc/< 进程 pid>/cwd” 符号链接的目标来确定进程 真正的当前目录地址。通过系统服务启动的进程也可通过这一方法查看。

  2. 若程序调用了 seteuid()/setegid() 改变 了进程的有效用户或组, 则在默认情况下系统不会为这些进程生成 Coredump。很多服务程序都会调用 seteuid(), 如 MySQL, 不论你用什么用 户运行 mysqld_safe 启动 MySQL, mysqld 进行的有效用户始终是 msyql 用户。如果你当初是以用户 A 运行了某个程序, 但在 ps 里看到的 这个程序的用户却是 B 的话, 那么这些进程就是调用了 seteuid 了。为了能够让这些进程生成 core dump, 需要将 /proc/sys/fs/suid_dumpable 文件的内容改为 1(一般默认是 0)。

  3. 这个一般都知道, 就是要设置足够大的 Core 文件大小限制 。程序崩溃时生成的 Core 文件大小即为程序运行时占用的内存大小。但程序崩溃时的行为不可按平常时的行为来估计, 比如缓冲区溢出等错误可能导致堆栈被 破坏, 因此经常会出现某个变量的值被修改成乱七八糟的, 然后程序用这个大小去申请内存就可能导致程序比平常时多占用很多内存。因此无论程序正常运行时占用 的内存多么少, 要保证生成 Core 文件还是将大小限制设为 unlimited 为好。

1.2. Linux 使用信号量监控程序异常退出

1.2.1. 何为黑匣子程序及其必要性

飞机上面的黑匣子用于飞机失事后对事故的时候调查, 同理, 程序的黑匣子用于程序崩溃后对崩溃原因进程定位。其实Linux提供的core dump机制就是一种黑匣子(core文件就是黑匣子文件)。但是core文件并非在所有场景都适用, 因为core文件是程序崩溃时的内存映像, 如果程序使用的内存空间比较大, 那产生的core文件也将会非常大, 在64bit的操作系统中, 该现象更为显着。但是, 其实我们定位程序崩溃的原因一般只需要程序挂掉之前的堆栈信息、内存信息等就足够了。所以有的时候没有必要使用系统自带的core文件机制。

阅读本文前, 推荐先看一下我的另外一篇博客《Linux 的 core 文件》, 里面讲解了core文件, 并介绍了一些Linux信号的基本知识。

1.2.2. 黑匣子程序设计

程序异常时, 往往会产生某种信号, 内核会对该信号进行处理。所以设计黑匣子程序的实质就是我们定义自己的信号处理函数, 来代替内核的默认处理。在我们的信号处理函数中, 我们可以将我们想要的信息保存下来(比如程序崩溃时的堆栈信息), 以方便后面问题的定位。

下面我们先给出一个我写的程序, 然后边分析程序边讲具体如何设计一个黑匣子程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

/* 定义一个数据结构用来保存信号 */
typedef struct sigInfo
{
    int     signum;
    char    signame[20];
} sigInfo;

/* 增加我们想要捕捉的异常信号, 这里列举了6个 */
sigInfo sigCatch[] = {
    {1, "SIGHUP"}, {2, "SIGINT"}, {3, "SIGQUIT"},
    {6, "SIGABRT"}, {8, "SIGFPE"}, {11, "SIGSEGV"}
};

/* 我们自定义的信号处理函数 */
void blackbox_handler(int sig)
{
    printf("Enter blackbox_handler: ");
    printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig);
        // 打印堆栈信息
    printf("Stack information:\n");
    int j, nptrs;

    #define SIZE 100
    void *buffer[100];

    char **strings;
    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addresses\n", nptrs);
    
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbol");
        exit(EXIT_FAILURE);
    }
    for(j = 0; j < nptrs; j++)
        printf("%s\n", strings[j]);
    
    free(strings);
    _exit(EXIT_SUCCESS);
}


/* 有bug的程序, 调用该程序, 将随机产生一些异常信号 */

void bug_func()
{
    int rand;
    struct timeval tpstart;
    pid_t  my_pid = getpid();

    // 产生随机数
    gettimeofday(&tpstart, NULL);
    srand(tpstart.tv_usec);
    while ((rand = random()) > (sizeof(sigCatch)/sizeof(sigInfo)));
    printf("rand=%d\n", rand);
    //随机产生异常信号
    switch(rand % (sizeof(sigCatch)/sizeof(sigInfo)))
    {
        case 0:
        {
            // SIGHUP
            kill(my_pid, SIGHUP);
            break;
        }
        case 1:
        {
            // SIGINT
            kill(my_pid, SIGINT);
            break;
        }
        case 2:
        {
            // SIGQUIT
            kill(my_pid, SIGQUIT);
            break;
        }
        case 3:
        {
            // SIGABRT
            abort();
            break;
        }
        case 4:
        {
            // SIGFPE
            int a = 6 / 0;
            break;
        }
        case 5:
        {
            // SIGSEGV
            kill(my_pid, SIGSEGV);
            break;
        }
        default:
            return;
    }
}

int main()
{
    int i, j;
    struct  sigaction   sa;
    
    // 初始化信号处理函数数据结构
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    for (i = 0; i < sizeof(sigCatch)/sizeof(sigInfo); i++)
    {
        // 注册信号处理函数
        if(sigaction(sigCatch[i].signum, &sa, NULL) < 0)
        {
            return EXIT_FAILURE;
        }
    }
    bug_func();
    while(1);
    return EXIT_SUCCESS;

}
1.2.2.1. 定义一些数据结构

这里我们定义了一个sigInfo的数据结构, 用来保存信号。利用这个数据结构我们可以将信号值与信号名映射起来。你可以在你的系统中使用 kill –l 命令去查看他们的对应关系。当然, 在程序中, 如果得到了信号值, 也可以使用Linux提供的API函数strsignal来获取信号的名字, 其函数原型如下:

#include <string.h>

char *strsignal(int sig);

之后定义了一个全局变量sigCatch来增加我们想要处理的信号。

1.2.2.2. sigaction函数

在main函数里面, 除了调用一些函数外, 主要是注册了一下我们要处理的信号。其实就是将特定的信号与某个信号处理函数关联起来。这里我们所要捕获的信号的信号处理函数都是同一个blackbox_handler, 因为我们想在这些信号出现时保存堆栈信息, 所以使用同一个函数完全可以。这里需要介绍的是sigaction函数, 其函数原型如下:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

使用该函数可以改变程序默认的信号处理函数。

第一个参数signum指明我们想要改变其信号处理函数的信号值。注意, 这里的信号不能是SIGKILL和SIGSTOP。这两个信号的处理函数不允许用户重写, 因为它们给超级用户提供了终止程序的方法( SIGKILL and SIGSTOP cannot be caught, blocked, or ignored)。

第二个和第三个参数是一个struct sigaction的结构体, 该结构体在<signal.h>中定义, 用来描述信号处理函数。如果act不为空, 则其指向信号处理函数。如果oldact不为空, 则之前的信号处理函数将保存在该指针中。如果act为空, 则之前的信号处理函数不变。我们可以通过将act置空, oldact非空来获取当前的信号处理函数。

我们来看一下这个重要的结构体:

struct sigaction {
       void     (*sa_handler)(int);
       void     (*sa_sigaction)(int, siginfo_t *, void *);
       sigset_t   sa_mask;
       int        sa_flags;
       void     (*sa_restorer)(void);   // 该成员现在已废弃
};

可以看到, 该结构体共有5个成员:

  • sa_handler是一个函数指针, 指向我们定义的信号处理函数, 该值也可以是SIG_IGN(忽略信号)或者SIG_DEL(使用默认的信号处理函数)。

  • sa_mask字段说明了一个信号集, 信号处理函数执行期间这一信号集要加到进程的信号屏蔽字中。仅当从信号处理函数返回时再将进程的信号屏蔽字复位为原先的值。这样在调用信号处理函数时就能阻塞某些信号。在信号处理函数被调用时, 操作系统建立的新信号屏蔽字包括正在被递送的信号。因此保证了在处理一个给定信号时, 如果这种信号再次发生, 那么它会被阻塞到对前一个信号的处理结束为止。

  • sa_flags字段指定对信号处理的一些选项, 常用的选项及其含义说明如下(在 <signal.h> 中定义):

选项含义
SA_INTERRUPT由此信号中断的系统调用不会自动重启
SA_NOCLDSTOP若signo是SIGCHLD, 当子进程停止(作业控制)时, 不产生此信号。当子进程终止时, 仍产生此信号(参加SA_NOCLDWAIT说明)。若已设置此标志, 则当停止的进程继续运行时, 作为XSI扩展, 不发送SIGCHLD信号。
SA_NOCLDWAIT若signo是SIGCHLD, 则当调用进程的子进程终止时, 不创建僵尸进程。若调用进程在后面调用wait, 则调用进程阻塞, 直到其所有子进程都终止, 此时返回-1, 并将errno设置为ECHILD。
SA_NODEFER当捕捉到此信号时, 在执行其信号处理函数时, 系统不自动阻塞此信号(除非sa_mask包括了此信号)。
SA_ONSTACK若用sigaltstack声明了以替换栈, 则将此信号递送给替换栈上的进程。
SA_RESETHAND在此信号处理函数的入口处, 将此信号的处理方式复位为SIG_DEF, 并清除SA_SIGINFO标志。但是, 不能自动复位SIGILL和SIGTRAP这两个信号的配置。设置此标志是sigaction的行为如同SA_NODEFER标志也设置了一样。
SA_RESTART由此信号中断的系统调用会自动重启动。
SA_SIGINFO此选项对信号处理程序提供了附加信息: 一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。

sa_sigaction是一个替代的信号处理函数, 当sa_flags字段设置为SA_SIGINFO时, 使用该信号处理函数。需要注意的是, 对于sa_sigaction和sa_handler字段, 其实现可能使用同一存储区, 所以应用程序只能一次使用这两个字段中的一个。通常, 按如下方式调用信号处理函数:

void handler(int signo);

但是, 如果设置了SA_SIGINFO标志, 则按照如下方式调用信号处理函数:

void handler(int signo, siginfo_t *info, void *context);

可见第二种方式比第一种方式多了后面两个参数。其中第二个参数为一个siginfo_t结构的指针, 该结构描述了信号产生的原因, 该结构一般定义如下:

struct siginfo_t
{
    int     si_signo;       // signal number
    int     si_errno;       // if nonzero, errno value from <errno.h>
    int     si_code;        // additional info (depends on signal)
    pid_t   si_pid;         // sending process ID
    uid_t   si_uid;         // sending process real user ID
    void    *si_addr;       // address that cased the fault
    int     si_status;      // exit value or signal number
    long    si_band;        // band number for SIGPOLL
    
    /* possibly other fileds also */
}

一般siginfo_t结构至少包含si_signo和si_code成员。第三个参数context是一个无类型的指针, 它可以被强制转换为ucntext_t结构类型, 用于标识信号传递时进程的上下文。

1.2.2.3. 信号集

信号种类数目可能超过一个整型量所包含的位数, 所以一般而言, 不能用整型量中的一位代表一种信号, 也就是不能用一个整型量表示信号集(使用信号集可以表示多个信号)。POSIX.1定义了数据结构sigset_t以包含一个信号集, 并且定义了下面5个处理信号集的函数:

#include <signal.h>

/* 前四个函数成功返回0, 失败返回-1 */
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

/* 真返回1, 假返回0, 出错返回-1 */
int sigismember(const sigset_t *set, int signum);

每一个进程都有一个信号屏蔽字, 它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号, 该屏蔽字中都有一位与之对应。对于某种信号, 若其对应为已设置, 则它当前是被阻塞的。进程可以调用sigprocmask来检测和更改当前信号的屏蔽字。

函数sigemptyset初始化由set指向的信号集, 清除其中所有的信号。函数sigfillset初始化由set指向的信号集, 使其包括所有信号。所有应用程序在使用信号集前, 要对该信号集调用sigemptyset或sigfillset一次。这是因为C编译器把未赋初值的外部和静态变量都初始化为0. 一旦已经初始化了一个信号集, 以后就可以在该信号集中增、删特定的信号。函数sigaddset将一个信号添加到现有集中, sigdelset则从信号集中删除一个信号。

1.2.2.4. kill&&raise&&abort 函数

bug_func函数的作用是产生一些异常信号, 用于我们的测试。里面有两个注意点: (1)我们使用微秒数来作为随机数种子, 这样产生的伪随机数分布会比其他很多方式更均匀一些。(2)我们调用了kill函数和abort函数来产生一些信号。其函数原型如下:

#include <signal.h>

int kill(pid_t pid, int sig);
int raise(int sig);

#include <stdlib.h>
void abort(void);

kill函数将信号发送给进程或进程组。kill的pid参数有4种不同的情况:

  • pid>0. 将该信号发送给进程ID为pid的进程。
  • pid==0. 将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID), 而且发送进程具有向这些进程发送信号的权限。注意, 这里的"所有进程"不包括实现定义的系统进程集。对于大多数UNIX系统, 系统进程集包括内核进程以及init(pid等于1)进程。
  • pid<0. 将该信号发送给其进程组ID等于pid的绝对值, 而且发送进程具有向其发送信号的权限。如上所述, "所有进程集"不包括某些系统进程。
  • pid==-1. 将该信号发送给发送进程有权限向它们发送信号的系统上所有的进程。不包括某些系统进程。

raise函数等价于kill(getpid(), signo).

abort函数会先清除对SIGABRT信号阻塞(如果有阻塞的话), 然后调用raise函数向调用进程发送信号。注意: 如果abort函数使得进程终止了, 那终止前会刷新和关闭所有打开的流。

1.2.2.5. backtrace&&backtrace_symbols 函数

在黑匣子信号处理函数中我们使用了backtrace和backtrace_symbols函数来获取进程崩溃时的堆栈信息。这两个函数的函数原型如下:

#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);

backtrace函数会返回进程的调用栈信息, 并保存在buffer指向的二维数组中; size指明buffer中可以保存的最大栈帧数目, 如果调用栈信息超过了size的值, 则只会保存近期的调用栈信息。返回值是保存的栈帧数。

使用backtrace函数得到调用栈信息后, 我们就可以使用backtrace_symbols函数将调用栈的地址信息翻译为用符号描述的信息, 保存在返回值里面。需要注意的是我们只需要定义返回值的指针, 其空间由函数backtrace_symbols自己调用maolloc分配, 但是使用完以后的空间由我们负责释放。

backtrace_symbols_fd没有返回值, 它与backtrace_symbols的不同之处在于它会将翻译的调用栈信息保存在文件里面。

注意:

  1. 使用backtrace函数时, 在编译选项中需要加上 –rdynamic 选项, 比如: gcc –rdynamic blackbox.c –o blackbox 。

  2. backtrace_symbols函数会输出出错时的16进制的地址, 此时我们可以使用addr2line命令将其转换为我们具体的代码行数, 命令格式为: addr2line –e execute_file addr , 比如 addr2line –e ./a.out 0x400d62 。

在该黑匣子程序中, 涉及到了很多Linux信号的知识, 以及一些相关的数据结构和API, 希望对大家有用。但其实该黑匣子程序在有些极端情况下还是有一定的问题, 后面我们会分析并进一步优化。

1.2.3. Bug分析

在前文中, 我们实现了一个黑匣子程序——在进程崩溃后, 可以保存进程的调用栈。但是, 在文章结尾我们说程序有bug, 那bug是什么呢? 先看下面一个程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

void blackbox_handler(int sig)
{
    printf("Enter blackbox_handler: ");
    printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig);
    
    // 打印堆栈信息
    printf("Stack information:\n");
    int j, nptrs;
#define SIZE 100
    void *buffer[100];

    char **strings;

    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addresses\n", nptrs);

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbol");
        exit(EXIT_FAILURE);
    }

    for(j = 0; j < nptrs; j++)
        printf("%s\n", strings[j]);

    free(strings);
    _exit(EXIT_SUCCESS);

}

long count = 0;
void bad_iter()
{

   int a, b, c, d;
   a = b = c = d = 1;
   a = b + 3;
   c = count + 4;
   d = count + 5 * c;
   count++;
   printf("count:%ld\n", count);
   bad_iter();
}

int main()
{
    struct  sigaction   sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;

    if (sigaction(SIGSEGV, &sa, NULL) < 0)
    {
        return EXIT_FAILURE;
    }
    bad_iter();
    while(1);
    
    return EXIT_SUCCESS;

}

该程序的执行结果如下:

... ...

count:261856

count:261857

count:261858

count:261859

count:261860

count:261861

Segmentation fault (core dumped)

allan@ubuntu:temp$

该程序是一种极端情况: 我们的程序中使用了无线层次的递归函数, 导致栈空间被用尽, 此时会产生SIGSEGV信号。但是从输出看, 并没有走到我们的信号处理函数里面。这是因为但由于栈空间已经被用完, 所以我们的信号处理函数是没法被调用的, 这种情况下, 我们的黑匣子程序是没法捕捉到异常的。

但是该问题也很好解决, 我们可以为我们的信号处理函数在堆里面分配一块内存作为"可替换信号栈"。

1.2.4. 使用可替换信号栈&&sigaltstack函数

使用可替换栈优化后的程序如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>



void blackbox_handler(int sig)
{

    printf("Enter blackbox_handler: ");
    printf("SIG name is %s, SIG num is %d\n", strsignal(sig), sig);

    // 打印堆栈信息
    printf("Stack information:\n");
    int j, nptrs;
#define SIZE 100
    void *buffer[100];
    char **strings;
    
    nptrs = backtrace(buffer, SIZE);

    printf("backtrace() returned %d addresses\n", nptrs);

    

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbol");
        exit(EXIT_FAILURE);
    }

    for(j = 0; j < nptrs; j++)
        printf("%s\n", strings[j]);

    free(strings);
    _exit(EXIT_SUCCESS);
}



long count = 0;
void bad_iter()
{
    int a, b, c, d;
    a = b = c = d = 1;
    a = b + 3;
    c = count + 4;
    d = count + 5 * c;
    count++;
    printf("count:%ld\n", count);
    bad_iter();
}



int main()
{

    stack_t ss;
    struct  sigaction   sa;
  

    ss.ss_sp = malloc(SIGSTKSZ);
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;
    if (sigaltstack(&ss, NULL) == -1)
    {
        return EXIT_FAILURE;
    }

    
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_ONSTACK;
    
    if (sigaction(SIGSEGV, &sa, NULL) < 0)
    {
       return EXIT_FAILURE;
    }

   
    bad_iter();
    while(1);

    
   return EXIT_SUCCESS;

}

编译 gcc –rdynamic blackbox_overflow.c 后运行, 输出为:

... ...

count:261989

count:261990

count:261991

count:261992

Enter blackbox_handler: SIG name is Segmentation fault, SIG num is 11

Stack information:

backtrace() returned 100 addresses

./a.out(blackbox_handler+0x63) [0x400c30]

/lib/x86_64-linux-gnu/libc.so.6(+0x36ff0) [0x7f6e68d74ff0]

/lib/x86_64-linux-gnu/libc.so.6(_IO_file_write+0xb) [0x7f6e68db7e0b]

/lib/x86_64-linux-gnu/libc.so.6(_IO_do_write+0x7c) [0x7f6e68db931c]

/lib/x86_64-linux-gnu/libc.so.6(_IO_file_xsputn+0xb1) [0x7f6e68db84e1]

/lib/x86_64-linux-gnu/libc.so.6(_IO_vfprintf+0x7fa) [0x7f6e68d8879a]

/lib/x86_64-linux-gnu/libc.so.6(_IO_printf+0x99) [0x7f6e68d92749]

./a.out(bad_iter+0x7a) [0x400d62]

./a.out(bad_iter+0x84) [0x400d6c]

./a.out(bad_iter+0x84) [0x400d6c]

./a.out(bad_iter+0x84) [0x400d6c]

./a.out(bad_iter+0x84) [0x400d6c]

./a.out(bad_iter+0x84) [0x400d6c]

... ...

可以看到,使用可替换栈以后,虽然同样栈溢出了,但是我们的黑匣子程序还是起作用了。所以这种优化是有效的。下面我们来看优化的代码。

可以看到我们的代码中使用了sigaltstack函数,该函数的作用就是在在堆中为函数分配一块区域,作为该函数的栈使用。所以,虽然递归函数将系统默认的栈空间用尽了,但是当调用我们的信号处理函数时,使用的栈是它实现在堆中分配的空间,而不是系统默认的栈,所以它仍旧可以正常工作。

该函数函数原型如下:

#include <signal.h>
 
int sigaltstack(const stack_t *ss, stack_t *oss);

该函数两个个参数为均为stack_t类型的结构体,先来看下这个结构体:

typedef struct {
   void  *ss_sp;     /* Base address of stack */
   int    ss_flags;  /* Flags */
   size_t ss_size;   /* Number of bytes in stack */
}

如果想要禁用已存在的一个可替换信号栈,可将ss_flags设置为SS_DISABLE。要想创建一个新的可替换信号栈,ss_flags必须设置为0,ss_sp和ss_size分别指明可替换信号栈的起始地址和栈大小。系统定义了一个常数SIGSTKSZ,该常数对极大多数可替换信号栈来说都可以满足需求,MINSIGSTKSZ规定了可替换信号栈的最小值。

而sigaltstack第一个参数为创建的新的可替换信号栈,第二个参数可以设置为NULL,如果不为NULL的话,将会将旧的可替换信号栈的信息保存在里面。函数成功返回0,失败返回-1.

一般来说,使用可替换信号栈的步骤如下:

  1. 在内存中分配一块区域作为可替换信号栈

  2. 使用sigaltstack()函数通知系统可替换信号栈的存在和内存地址

  3. 使用sigaction()函数建立信号处理函数的时候,通过将sa_flags设置为SA_ONSTACK来告诉系统信号处理函数将在可替换信号栈上面运行。

1.2.5. 附录:黑匣子程序封装

sig_handler.h

#ifndef __SIG_HANDLER_H_
#define __SIG_HANDLER_H_

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

/* 定义一个数据结构用来保存信号 */
typedef struct sig_info
{
    int     signum;
    char    signame[20];
} sig_info_t;

/* 我们自定义的信号处理函数 */
extern void blackbox_handler(int sig);

extern void registe_sig_handler();

#endif

sig_handler.c

其中日志输出内容部分需要改成自己的IO输出

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>

#include "config.h" //日志相关
#include "sig_handler.h" //日志相关
#include "log.h"  //日志相关

/* 增加我们想要捕捉的异常信号,这里列举了6个 */
sig_info_t sigCatch[] = {
    {1, "SIGHUP"}, {2, "SIGINT"}, {3, "SIGQUIT"},

    {6, "SIGABRT"}, {8, "SIGFPE"}, {11, "SIGSEGV"}
};

void blackbox_handler(int sig)
{

    log_write(CONF.lf,LOG_INFO,"Enter blackbox_handler: ");
    log_write(CONF.lf,LOG_INFO,"SIG name is %s, SIG num is %d\n", strsignal(sig), sig);
    int j, nptrs;
#define SIZE 100
    void *buffer[100];
    char **strings;
    
    nptrs = backtrace(buffer, SIZE);

    log_write(CONF.lf,LOG_INFO,"backtrace() returned %d addresses\n", nptrs);
    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        log_write(CONF.lf,LOG_INFO,"backtrace_symbol");
        exit(EXIT_FAILURE);
    }

    for(j = 0; j < nptrs; j++)
    	log_write(CONF.lf,LOG_INFO,"%s\n", strings[j]);

    log_write(CONF.lf,LOG_INFO,"server exit unormal");
    
    free(strings);
    _exit(EXIT_SUCCESS);
}

void registe_sig_handler(){
	int i=0;

	struct  sigaction   sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_handler = blackbox_handler;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;

    //register sig
    for (i = 0; i < sizeof(sigCatch)/sizeof(sig_info_t); i++)
    {
        // 注册信号处理函数
        if(sigaction(sigCatch[i].signum, &sa, NULL) < 0)
        {
            return EXIT_FAILURE;
        }
    }

}

2. Linux 子进程替换保护进程

2.1. 基本思路

  1. 使用 fork 函数创建一个新的进程, 在进程表中创建一个新的表项, 而创建者 (即父进程) 按原来的流程继续执行, 子进程执行自己的控制流程
  2. 运用 execv 函数把当前进程替换为一个新的进程, 新进程由 path 或 file 参数指定, 可以使用 execv 函数将程序的执行从一个程序切换到另一个程序
  3. 当 fork 启动一个子进程时, 子进程就有了它自己的生命周期并将独立运行, 此时可以在父进程中调用 wait 函数让父进程等待子进程的结束

2.2. 基本的实现步骤

  1. 首先使用 fork 系统调用, 创建子进程
  2. 在子进程中使用 execv 函数, 执行需要自动重启的程序
  3. 在父进程中执行 wait 函数等待子进程的结束, 然后重新创建一个新的子进程

2.3. 具体实现的代码如下: supervisor.c

/** 
 * 
 * supervisor 
 * 
 * date: 2016-08-10 
 * 
 */ 
 
#include <stdio.h> 
#include <unistd.h> 
#include <errno.h> 
#include <string.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <stdlib.h> 
#include <time.h> 
 
#define LOG_FILE "/var/log/supervisor.log" 
 
void s_log(char *text) { 
  time_t   t; 
  struct tm *tm; 
  char *log_file; 
  FILE *fp_log; 
  char date[128]; 
   
  log_file = LOG_FILE; 
  fp_log = fopen(log_file, "a+"); 
  if (NULL == fp_log) { 
    fprintf(stderr, "Could not open logfile'%s'for writing\n", log_file); 
  } 
   
  time(&t); 
  tm = localtime(&t); 
  strftime(date, 127, "%Y-%m-%d %H:%M:%S", tm); 
   
  /* write the message to stdout and/or logfile */   
  fprintf(fp_log, "[%s] %s\n", date, text); 
  fflush(fp_log); 
  fclose(fp_log); 
}

int main(int argc, char **argv) { 
  int ret, i, status; 
  char *child_argv[100] = {0}; 
  pid_t pid; 
  if (argc < 2) { 
    fprintf(stderr, "Usage:%s <exe_path> <args...>", argv[0]); 
    return -1; 
  } 
   
  for (i = 1; i < argc; ++i) { 
    child_argv[i-1] = (char *)malloc(strlen(argv[i])+1); 
    strncpy(child_argv[i-1], argv[i], strlen(argv[i])); 
    //child_argv[i-1][strlen(argv[i])] = '0'; 
  } 
   
  while(1) { 
    pid = fork();  
    if (pid == -1) { 
      fprintf(stderr, "fork() error.errno:%d error:%s", errno, strerror(errno)); 
      break; 
    } 
    if (pid == 0) { 
      s_log(child_argv[0]); 
      ret = execv(child_argv[0], (char **)child_argv); 
      s_log("execv return"); 
      if (ret < 0) { 
        fprintf(stderr, "execv ret:%d errno:%d error:%s", ret, errno, strerror(errno)); 
        continue; 
      } 
      s_log("exit child process"); 
      exit(0); 
    } 
    if (pid > 0) { 
      pid = wait(&status); 
      fprintf(stdout, "Child process id: %d\n", pid); 
      //fprintf(stdout, "wait return"); 
      s_log("Wait child process return"); 
    } 
  } 
   
  return 0; 
} 

2.4. 测试验证

  1. 假设需要自动重启的程序为 demo.c, 其代码实现如下所示:
/* 
* 
* demo  
* 
*/ 
#include <stdio.h> 
#include <unistd.h> 
#include <errno.h> 
#include <string.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <stdlib.h> 
#include <time.h> 
 
#define LOG_FILE "/var/log/demo.log" 
 
void demo_log(int num) { 
  time_t   t; 
  struct tm *tm; 
  char *log_file; 
  FILE *fp_log; 
  char date[128]; 
   
  log_file = LOG_FILE; 
  fp_log = fopen(log_file, "a+"); 
  if (NULL == fp_log) { 
    fprintf(stderr, "Could not open logfile'%s'for writing\n", log_file); 
  } 
   
  time(&t); 
  tm = localtime(&t); 
  strftime(date,127,"%Y-%m-%d %H:%M:%S",tm); 
   
  /* write the message to stdout and/or logfile */   
  fprintf(fp_log, "[%s] num = %d\n", date, num); 
  fflush(fp_log); 
  fclose(fp_log); 
}  
 
int main(int argc, char **argv[]) { 
  int num = 0; 
   
  while(1) { 
    sleep(10); 
    num++; 
    demo_log(num); 
  } 
} 
  1. 测试准备和说明:

b1. 以上相关服务程序编译后的二进制文件为: supervisor 和 demo

b2. 执行如下测试命令 ./supervisor ./demo

  1. 测试的结果:

c1. execv(progname, arg) 执行成功后, 其后的代码不会执行; 只有当执行错误时, 才会返回 -1。原来调用 execv 进程的代码段会被 progname 应用程序的代码段替换。

c2. 当 kill 掉子进程时, 父进程 wait 函数会接收到子进程退出的信号, 进而循环再启动子进程, 此过程实时性非常高。

c3. 当 kill 掉父进程时, 子进程会被 init 进程接管, 如果此时再 kill 掉子进程, 则子进程会退出。

c4. 当同时 kill 掉父子进程, 则父子进程都会退出。

3. Linux 运行执行文件

3.1. 后台运行可执行文件

# sudo nohup ./minigame-api &

一般采用 sudo 权限,我之前没有使用 sudo 导致程序会退出,后来改用 sudo 了,同一个程序,没有再出现自动退出的现象,不确定是不是这个原因,反正现象是这样的。

还有,记得给所有程序设定好 ulimit 的值,是这个影响?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值