linux 信号处理

前言
     Linux中的信号是向进程异步发送的事件通知,通知进程有事件(硬件异常、程序执行异常、外部发出信号)发生。当信号产生时,内核向进程发送信号(在进程所在的进程表项的信号域设置对应于该信号的位)。内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时,当一个进程在内核态运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理,进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。内核为每个进程维护一个(未处理)的信号队列,信号产生后首先被放入到未决队列中,如果进程选择阻塞信号,那么如果某个信号发生多次,未决队列中仅保留相同的信号(不可靠信号类型)中的一个,而可靠信号则会被保留。
 
 一、进程信号处理  
int pause(void);     //将调用进程/线程 挂起sleep,直到有信号产生且在信号处理函数完成后返回
int kill(pid_t pid, int sig);     //将sig信号发送到pid进程
int raise(int sig);   //向调用进程/线程发送sig信号
 
sigemptyset, sigfillset, sigaddset, sigdelset, sigismember用来操作信号集合sigset_t,该信号集合可以用于sigwait、sigaction等操作
 
int sigwait(const sigset_t *set, int *sig);     //阻塞等待set中的信号,sig保存发生的信号;同类函数有sigtimedwait,sigwaitinfo
sighandler_t signal(int signum, sighandler_t handler);
 
示例: signal(SIGUSR1, myfunc);    //注册SIGUSR1的信号处理函数myfunc
 
//注:该函数在不同的linux、unix版本实现方式不太一样,为了保证程序的通用性,建议使用sigaction
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 
示例:struct sigaction act, oldact; 
     //注册的信号处理函数类型为void (*sa_handler)(int);
     //act.sa_handler = show_handler;
 
     //注册的信号处理函数,类型为void (*sa_sigaction)(int, siginfo_t *, void *);这种方法功能与sa_handler相同,但是从siginfo_t结构体参数中获取产生该信号的详细信息,特别是对于错误分析特别有用,建议采用这种。
     act.sa_sigaction = show_handler;
 
     sigaddset(&act.sa_mask, SIGQUIT); //在SIGINT的信号处理函数执行时,阻塞SIGQUIT信号,直到函数执行完成
     act.sa_flags = 0;
     sigaction(SIGINT, &act, &oldact);   //设置SIGINT信号新的处理方法,将老的处理方法保留到oldact中,方便在适当的时候还原之前的信号处理方法
 
//注:siginfo_t结构体的具体参数以及sa_flags的一些标志位的含义,参加man手册 man sigaction
int sigpending(sigset_t *set);     //获取当前阻塞的信号集
 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//how包含SIG_BLOCK(将set中包含的信号添加到已有的阻塞信号集合中), SIG_UNBLOCK(将set中信号从阻塞的信号集合中移除), SIG_SETMASK(将阻塞信号集合修改成set中的信号)
 
int sigsuspend(const sigset_t *mask);     //将调用进程的信号集替换成mask指向的信号集,然后挂起,直到有信号(不在mask中)产生且对应的信号处理函数返回,此时将原有的信号集还原
 
//注:sigsuspend通常配合sigprocmask使用,用于保证临界区代码执行。
示例:sigemptyset(&new_mask);
sigemptyset(&zero_mask);      // 清空信号集zero_mask
sigaddset(&new_mask, SIGQUIT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);   // 阻塞SIGQUIT
 
while( quitflag == 0 ) {
    sigsuspend(&zero_mask);   // 将信号掩码替换为空,等待SIGQUIT信号处理函数将quitflag置1
}
 
sigprocmask(SIG_SETMASK, &old_mask, NULL);      // 恢复信号掩码
二、多线程信号处理
     多线程信号处理跟单线程的程序最大的区别就是所有的线程共享信号处理函数,每个线程对信号处理函数的修改,都会同步到其他线程。linux环境下线程是通过轻量级进程(有兴趣可以查资料)实现的,因此内核为每个线程维护一个未决信号队列。创建新的线程时,新线程继承主线程的信号屏蔽字,但是新线程的未决信号队列被清空(防止同一信号被多个线程处理)。各个线程的信号屏蔽字(sigmask)是独立的,可以通过pthread_sigmask函数来控制线程级别的sigmask。
     如果是硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定时器超时触发的信号,该信号会发往引起该事件的线程;其余的所有情况产生的信号都会发送到主线程。因此要想让特定线程处理信号,需要主线程将这些信号屏蔽。
 
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);     //线程级别的sigprocmask
int pthread_kill(pthread_t thread, int sig);     //线程级别的kill 
 
注:进程信号处理中讲到的大部分函数都是可以在多线程程序中使用的
 
 
三、信号处理函数
    通常情况下,我们采用信号处理函数是为了方便我们解决问题。因此我们除了在信号处理函数中进行简单的业务处理(回滚等),还需要将出错的位置以及原因输出保存下来,便于开发分析解决BUG。建议采用sigaction代替signal,因为sigaction可以携带更多的信息,而且更通用。示例如下:
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <execinfo.h>
 
void print_bt(void)
{
    int j, nptrs;
    void *buffer[256];
    char **strings;
 
  /* 利用backtrace获取函数当前的调用堆栈 */
    nptrs = backtrace(buffer, 20);    
    strings = backtrace_symbols(buffer, nptrs);
 
    /* 去除当前函数、信号处理函数以及调用main函数的相关系统函数的堆栈 */
    nptrs -= 2;
    for (j = 2; j < nptrs; j++) {
        std::cout << strings[j] << std::endl;
    }
    free(strings);
}
 
void sig_hdlr(int signo, siginfo_t *info, void *myact)
{
    auto pid = getpid();
    /* info->si_pid 保存着信号发送方的进程id */
    std::cout << "recieve signal " << signo << " from process " << (info->si_pid != pid ? std::to_string(info->si_pid) : "itself") << std::endl;
    print_bt();
}
 
int main(int argc, char *argv[])
{
    struct sigaction act;
 
    memset(&act, 0, sizeof(act));
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = sig_hdlr;
    if (sigaction(SIGSEGV, &act, NULL) < 0) {
        std::cerr << "Install sig SIGSEGV failed :" << strerror(errno) << std::endl;
    }
 
    raise(SIGSEGV);    /* 手动产生SIGSEGV */
 
    return EXIT_SUCCESS;
}               /* ----------  end of function main  ---------- */
在程序链接的时候,需要添加-rdynamic 选项,这样可以更清晰的获取堆栈中的函数名等信息。类似堆栈输出 ./a.out(main+0xa6) [0x4020c2], 我们可以利用addr2line 0x4020c2 -e BIN_FILE 来获取具体的出错行号。(注:如果可以的话,尽量让程序生成core文件,这样更方便找出问题原因)
 
四、踩坑教训
     1、在一个多线程程序中,线程A中会设置定时器,如果超时就会触发SIGALRM的信号处理函数sig_alarm_func,该函数执行了pthread_cancel(A);pthread_create(B);的操作。在测试过程中发现进程中同时存在A, B两个线程。查看pthread_cancel 说明,phtread_cancel是个异步的,需要等到线程A执行到cancellation point才能结束退出。利用gdb查看A的函数调用栈发现,阻塞到了信号处理函数sig_alarm_func中,即发生了“自己取消自己”的问题。根据第二部分讲到的信号通告机制,定时器信号被发往了调用定时器的线程,因而信号处理函数也是在调用线程的上下文中执行,所以出现了异常。
     解决方法:单独设置一个信号处理线程,阻塞除该线程外的其他所有线程的信号。在信号处理线程中,利用 while+sigwait 对信号进行同步处理代替注册信号处理函数的异步处理方式。
 
     2、在处理一个程序堆栈时,发现程序在malloc函数中发生了死锁。进一步分析发现信号处理函数在保存函数调用堆栈时调用了malloc,而信号产生时正好也在执行malloc操作。通过查看malloc的相关文档发现,malloc在申请内存的时候,有加锁操作。
     解决方法:信号处理函数中取消malloc这类不可重入的有锁函数。以后编写信号处理函数的时候,在函数内部尽少做一些耗时处理尽快返回,在调用函数时必须调用可重入(reentrant)函数(即不可以有static、global等全局变量,不可以分配、释放内存,不要修改errno等)。
原文地址:https://www.cnblogs.com/sxhlinux/p/6729384.html