一、正文

可靠信号与不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

SA_RESTART

一些IO系统调用执行时,如 read 等待输入期间,如果收到一个信号,系统将中断read,转而执行信号处理函数. 当信号处理返回后,系统遇到了一个问题: 是重新开始这个系统调用,还是让系统调用失败?早期UNIX系统的做法是,中断系统调用,并让系统调用失败,比如read返回 -1,同时设置 errno 为 EINTR中断了的系统调用是没有完成的调用,它的失败是临时性的,如果再次调用则可能成功,这并不是真正的失败,所以要对这种情况进行处理,典型的方式为:

snippet.c
while (1) {
    n = read(fd, buf, BUFSIZ);
    if (n == -1 && errno != EINTR) {
        printf("read error\n");
        break;
    }
 
    if (n == 0) {
        printf("read done\n");
        break;
    }
}

这样做逻辑比较繁琐,事实上,我们可以从信号的角度来解决这个问题,安装信号的时候,设置 SA_RESTART属性,那么当信号处理函数返回后,被该信号中断的系统调用将自动恢复。

sigprocmask

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

参数的how:

假设现在阻塞的信号表是{SIGSEGV,SIGSUSP},我们执行了以下代码:

snippet.c
sigset_t x,y;
sigemtyset(&x);
sigemtyset(&y);
sigaddset(&x,SIGUSR1);
sigprocmask(SIG_BLOCK,&x,&y);

则新的阻塞表是{SIGSEGV,SIGSUSP,SIGUSR1},y中保存的是{SIGSEGV,SIGSUSP}

如果我接着执行sigprocmask(SIG_UNBLOCK,&x,NULL),

则新的阻塞表是{SIGSEGV,SIGSUSP}

如果我接着执行sigprocmask(SIG_SET,&x,NULL);

则新的阻塞表是{SIGUSR1}。

sigaction

注册信号处理函数。

snippet.c
typedef void (*sigaction_func)(int, siginfo_t*, void*);
static void signal_handler(int signo, siginfo_t* siginfo, void* context)
{
    ……
}
 
static sigaction_func signal_install_handler(void)
{
    struct sigaction act, oact;
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = signal_handler;
    act.sa_flags = 0;
    act.sa_flags |= SA_SIGINFO;  // 使用3个参数的信号处理函数
    act.sa_flags |= SA_RESTART;  // 信号返回时,重新调研系统函数。
 
    if (sigaction(RES_SIGNAL, &act, &oact) < 0) {
        errmsg("not able to set up signal action handler");
    }
 
    /* Return previously installed handler */
    return oact.sa_sigaction;
}

sigset_t结构

snippet.c
sigset_t intMask;
sigfillset(&intMask);
sigdelset(&intMask, SIGSEGV);  // SIGSEGV = 11

sigfillset(&intMask); 执行后,intMask值如下,注意,第32和33 bit位为0,这两个位没有使用:

snippet.gdb
(gdb) p/t intMask       
$3 = {__val = {1111111111111111111111111111111001111111111111111111111111111111, 
1111111111111111111111111111111111111111111111111111111111111111 <repeats 15 times>}}

sigdelset(&intMask, SIGSEGV); 执行后,intMask值如下,可见,第11位被清零:

snippet.gdb
(gdb) p/t intMask
$6 = {__val = {1111111111111111111111111111111001111111111111111111101111111111, 
1111111111111111111111111111111111111111111111111111111111111111 <repeats 15 times>}}

sigsetjmp

相关函数:longjmp, siglongjmp, setjmp

表头文件:#include <setjmp.h>

函数定义:int sigsetjmp(sigjmp_buf env, int savesigs)

函数说明:sigsetjmp()会保存目前堆栈环境,然后将目前的地址作一个记号,

而在程序其他地方调用siglongjmp()时便会直接跳到这个记号位置,然后还原堆栈,继续程序的执行。

参数env为用来保存目前堆栈环境,一般声明为全局变量。

参数savesigs若为非0则代表搁置的信号集合也会一块保存。

当sigsetjmp()返回0时代表已经做好记号上,若返回非0则代表由siglongjmp()跳转回来。

返回:若直接调用则为0,若从siglongjmp调用返回则为非0。

snippet.c
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <sys/time.h>
sigjmp_buf jmp_env;
static void connect_alarm(int)
{
    siglongjmp(jmp_env, 1);
}
int main()
{
    // 当超时时间sec_timeout大于等于运行时间run_time时会跳过printf("running...\n");
    int sec_timeout = 3;
    int run_time = 2;
    printf("timeout = %d, run time = %d\n", sec_timeout, run_time);
    if (sec_timeout)
    {
        // 超过用alarm函数设置的时间时产生此信号,调用connect_alarm函数
        signal(SIGALRM, connect_alarm);
        alarm(sec_timeout);
        printf("set timeout\n");
        if (sigsetjmp(jmp_env, 1))
        {
            printf("timeout\n");
            goto out;
        }
    }
    sleep(run_time);
    printf("running...\n");
out:
    if (sec_timeout)
    {
        // 取消先前设置的闹钟
        alarm(0);
        printf("cancel timeout\n");
    }
    return 0;
}

二、参考