信号

371 阅读6分钟

信号概念

每个信号都有一个名字,以 SIG 开头,例如 SIGABRT,SIGALRM 等。

信号产生:

  • 当用户按某些终端键时,引发终端产生信号。如 Ctrl+C 通常产生中断信号(SIGINT)。
  • 硬件异常产生信号:除数为 0、无效的内存引用等。
  • 进程调用 kill(2) 函数可将任意信号发送给另一个进程或进程组。
  • 用户调用 kill(1) 命令将信号发送给其他进程。
  • 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。

信号处理:

  1. 忽略此信号。SIGKILL 和 SIGSTOP 不能被忽略,它们向内核和超级用户提供了使进程终止或停止的方法。另外,如果忽略某种由硬件异常产生的信号(如非法内存引用或除以0),则进程的运行行为是未定义的。
  2. 捕捉信号。通知内核在某种信号发生时,调用一个用户函数,在用户函数中可以执行用户系统对这种事件进行的处理。注意不能捕捉 SIGKILL 和 SIGSTOP 信息。
  3. 执行系统默认动作。注意,对大多数信号的系统默认动作是终止该进程。

函数 signal

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);
  • signo:信号名
  • func:常量 SIG_IGN、常量 SIG_DFL 或接到此信号后要调用的函数的地址。
    • SIG_IGN 忽略此信号
    • SIG_DFL 系统默认动作
    • 指定函数地址,则在信号发生时,调用该函数,此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)

signal 函数原型说此函数要求两个参数,返回一个函数指针。

实例1

#include <signal.h>
#include <printf.h>
#include <unistd.h>


static void sig_usr(int);

int main(void) {
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        printf("can't catch SIGUSR1");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        printf("can't catch SIGUSR2");
    for (;;)
        pause();
}

static void sig_usr(int signo) {
    switch (signo) {
        case SIGUSR1:
            printf("received SIGUSR1\n");
            break;
        case SIGUSR2:
            printf("received SIGUSR2\n");
            break;
        default:
            printf("received signal %d\n", signo);
    }
}

可重入函数

所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。

一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

保证函数的可重入性的方法:

  • 在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量);
  • 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

满足下列条件的函数多数是不可重入(不安全)的:

  • 函数体内使用了静态的数据结构;
  • 函数体内调用了malloc() 或者 free() 函数;
  • 函数体内调用了标准 I/O 函数。

浅谈可重入函数与不可重入函数

信号集

表示多个信号——信号集(signal set)的数据类型。

一个进程有一个信号集,这个信号集表示当前屏蔽(阻塞)了哪些信号。
如果把信号集中的某个位置置1,表示屏蔽了该信号,如果再来此信号,该进程是收不到的。

函数 sigprocmask

sigprocmask主要是设置该进程对应的信号集中的内容:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数how:

how说明
SIG_BLOCKSIG_BLOCK表明设置当前进程新的信号屏蔽字为 “当前信号屏蔽字” 和 第二个参数指向的信号集的并集,set包含了我们希望阻塞的附加信号
SIG_UNBLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集,set包含了我希望解除阻塞的信号
SIG_SETMASK该进程新的信号屏蔽字将被set指向的信号集的值代替

实例

#include <stdio.h>
#include <stdlib.h> //malloc
#include <unistd.h>
#include <signal.h>
#include <errno.h>

//信号处理函数
void sig_quit(int signo) {
    printf("收到了SIGQUIT信号!\n");
    if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) {
        printf("无法为SIGQUIT信号设置缺省处理(终止进程)!\n");
        exit(1);
    }
}

int main(int argc, char *const *argv) {
    sigset_t newmask, oldmask;
    if (signal(SIGQUIT, sig_quit) == SIG_ERR) { //注册信号对应的信号处理函数,"ctrl+"
        printf("无法捕捉SIGQUIT信号!\n");
        exit(1); //退出程序,参数是错误代码,0表示正常退出,非0表示错误,但具体什么错误,没有特别规定
    }

    sigemptyset(&newmask); //newmask信号集中所有信号都清0(表示这些信号都没有来);
    sigaddset(&newmask, SIGQUIT); //设置newmask信号集中的SIGQUIT信号位为1,也就是说,再来SIGQUIT信号时,进程就收不到,设置为1就是该信号被阻塞掉
    //sigprocmask():设置该进程所对应的信号集

    //第一个参数用了SIG_BLOCK表明设置进程新的信号屏蔽字 为 “当前信号屏蔽字 和 第二个参数指向的信号集的并集
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        //一个 ”进程“ 的当前信号屏蔽字,刚开始全部都是0的;所以相当于把当前 "进程"的信号屏蔽字设置成 newmask(屏蔽了SIGQUIT);                                                       //第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给线程         printf("sigprocmask(SIG_BLOCK)失败!\n");
        exit(1);
    }

    printf("我要开始休息10秒了--------begin--,此时我无法接收SIGQUIT信号!\n");
    sleep(10);   //这个期间无法收到SIGQUIT信号的;
    printf("我已经休息了10秒了--------end----!\n");

    if (sigismember(&newmask, SIGQUIT)) {  //测试一个指定的信号位是否被置位(为1),测试的是newmask
        printf("SIGQUIT信号被屏蔽了!\n");
    } else {
        printf("SIGQUIT信号没有被屏蔽!!!!!!\n");
    }
    if (sigismember(&newmask, SIGHUP)) { //测试另外一个指定的信号位是否被置位,测试的是newmask
        printf("SIGHUP信号被屏蔽了!\n");
    } else {
        printf("SIGHUP信号没有被屏蔽!!!!!!\n");
    }

    //把信号集还原回去
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) { //第一个参数用了SIGSETMASK表明设置进程新的信号屏蔽字为第二个参数指向的信号集,第三个参数没用
        printf("sigprocmask(SIG_SETMASK)失败!\n");
        exit(1);
    }

    printf("sigprocmask(SIG_SETMASK)成功!\n");

    if (sigismember(&oldmask, SIGQUIT)) { //测试一个指定的信号位是否被置位,这里测试的当然是oldmask
        printf("SIGQUIT信号被屏蔽了!\n");
    } else {
        printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要sleep(10)秒钟!!!!!!\n");
        int mysl = sleep(10);
        if (mysl > 0) {
            printf("sleep还没睡够,剩余%d秒\n", mysl);
        }
    }
    printf("end main!\n");
    return 0;
}

函数 sigaction

sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作),函数原型:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum参数指出要捕获的信号类型
  • act参数指定新的信号处理方式
  • oldact参数输出先前信号的处理方式(如果不为NULL的话)
struct sigaction {
  union {
    sighandler_t sa_handler;
    void (*sa_sigaction)(int, struct siginfo*, void*);
  };
  sigset_t sa_mask;
  int sa_flags;
  void (*sa_restorer)(void);
};
  • sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
  • sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
  • sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。

v2-bfda756675d1384b50bbf91948beb5ef_r.jpg

sa_sigaction 字段是一个替代的信号处理程序,如果设置了 SA_SIGINFO 标志,使用该信号处理程序。sa_sigaction 字段和 sa_handler 字段可能使用同一存储区,所以只能使用这两个字段中的一个。

void (*sa_sigaction)(int signo, struct siginfo *info, void *context);

siginfo 结构包含了信息产生原因的有关信息。

image.png

应用程序在递送信号时,在 si_value.sival_int 中传递一个整型数或者在 si_value.sival_ptr 中传递一个指针值。

若信号是 SIGCHLD, 则将设置 si_pid、si_status 和 si_uid 字段。
若信号时 SIGBUS、SIGILL、SIGFPE 或 SIGSEGV, 则 si_addr 包含造成故障的根源地址,该地址可能并不准确。

信号处理程序的 context 参数是无类型指针,它可被强制类型转换为 ucontext_t 结构类型,该结构标识信号传递时进程的上下文。

image.png

int register_signal(void (*handler)(int, siginfo_t *, void *)) {
    stack_t ss;
    if (NULL == (ss.ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE))) return XCC_ERRNO_NOMEM;
    ss.ss_size = SIGNAL_CRASH_STACK_SIZE;
    ss.ss_flags = 0;

    if (0 != sigaltstack(&ss, NULL)) return XCC_ERRNO_SYS;

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

    sigfillset(&act.sa_mask);
    act.sa_sigaction = handler;
    act.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;

    size_t i;
    for (i = 0; i < sizeof(signal_crash_infos) / sizeof(signal_crash_infos[0]); i++) {
        if (0 !=
            sigaction(signal_crash_infos[i].signum, &act, &(signal_crash_infos[i].oldact)))
            return XCC_ERRNO_SYS;
    }
    return 0;
}

linux_signal_flow.png

函数 sigsetjmp 和 siglongjmp

c 语言中 setjmp 和 longjmp
Exceptions in C with Longjmp and Setjmp

当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序。

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savemask);
返回值:若直接调用则返回0,若从siglongjmp调用返回则返回非0void siglongjmp(sigjmp_buf env, int val);

这两个函数与setjmp和longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字调用siglongjmp时,如果带 非0 savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字

setjmp()/longjmp() 与 sigsetjmp()/siglongjmp() 区别