apue(六)、信号

159 阅读11分钟

1、简介

信号不是中断,中断只能由硬件产生,信号是模拟硬件中断的原理在软件层面上进行的实现。

Linux中的并发环境可以分为 多进程+信号 和 多线程两种,信号属于初级异步,多线程属于强烈异步。对于异步事件的获取通常有查询和通知两种方式,如果异步事件到来的频率比较高就采取查询法,如果异步事件到来频率比较低就采取通知法。

2、信号相关概念

2.1 信号处理方式

在某个信号出现时,可以告诉内核使用以下方式进行处理:

  • 忽略:忽略此信号,不进行处理。大多数信号都可以忽略,但是 SIGKILL、SIGSTOP 两种信号不能忽略。
  • 终止:使程序异常结束,前面提到过的使进程终止的3种异常情况中被信号杀死。
  • 终止+core:终止进程,并在进程当前工作目录的core文件中复制该进程的内存映象,系统调试程序可以通过 core文件检查进程终止时的状态。
  • 停止进程::将运行中的进程中的进程中断。
  • 继续:使被停止的进程继续运行,只有 SIGCONT 信号具有此功能。

下面是一些常见的信号:

信号默认动作说明
SIGABRT终止+core调用 abort 函数会向自己发送该信号使程序异常终止,通常在程序自杀时使用。
SIGALRM终止调用 alarm 或 setitimer 定时器超时时向自身发送的信号。 setitimer(2) 设置 which 参数的值为 ITIMER_REAL 时,超时后会发送此信号。
SIGCHLD(某些平台是 SIGCLD)忽略当子进程状态改变系统会将该信号发送给其父进程。 状态改变是指由运行状态改变为暂停状态、由暂停状态改变为运行状态、由运行状态改变为终止状态等等。
SIGHUP终止如果终端接口检测到链接断开则将此信号发送给该终端的控制进程,通常会话首进程就是该终端的控制进程。
SIGINT终止 当用户按下中断键(Ctrl+C)时,终端驱动程序产生此信号并发送给前台进程组中的每一个进程。 大家经常使用 Ctrl + C 来杀死进程,这回知道是什么原理了吧?
SIGPROF终止  setitimer 设置 which 参数的值为 ITIMER_PROF 时,超时后会发送此信号。
SIGQUIT终止+core 当用户在终端上按下退出键(Ctrl+)时,终端驱动程序产生此信号并发送给前台进程组中的所有进程。该信号与 SIGINT 的区别是,在终止进程的同时为它生成 core dump 文件。
SIGTERM终止 使用 kill 命令发送信号时,如果不指定具体的信号,则默认发送该信号。
SIGUSR1终止 用户自定义的信号。 有童鞋说不明白什么是用户自定义的信号, 其实所谓自定义的信号就是系统不赋予它什么特殊的意义,你想用它来做什么都行, 根据你的程序逻辑为它定义好相应的信号处理函数就行了。
SIGUSR2终止 另一个用户自定义的信号,作用同上。
SIGVTALRM终止   setitimer 设置 which 参数的值为 ITIMER_VIRTUAL 时,超时后会发送此信号。

2.2 信号处理函数 signal

#include <signal.h>

void (*signal(int signo, void (*func)(int))) (int);
/* 成功返回以前信号的处理配置,失败返回 SIG_ERR */

参数列表:

  • signo:待处理的信号编号,可通过 kill -l 查看所有宏名;
  • func:接收到信号时的处理函数,值可以是 SIG_IGN(忽略)、SIG_DFL(系统默认动作)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static void handler(int s) {
    write(1, "!", 1);
}

int main(void) {
    int i = 0;
    signal(SIGINT, handler);

    for (i = 0; i < 10; i++) {
        write(1, "*", 1);
        sleep(1);
    }
    
    return 0;
}

对于 signal 函数而言,多个不同的信号能够注册同一个信号处理函数,在一个信号到来时并不会把注册了同一个信号处理函数的信号屏蔽掉。

后面会讲解 sigaction 函数,它相比于 signal 函数而言能够在执行信号处理时阻塞其它信号。

2.3 信号处理的流程

  1. 内核为进程维护两个位图: 一个是 pending,一个是 masks。其中 masks 是信号的标志位,代表信号是否会被忽略。pending 用于提示是否收到了该信号。masks 中各位默认设置为1,pending 默认设置为0。

  2. 信号响应的过程:当我们在键盘上键入 CTRL+C 时,进程会中断从用户态进入内核态调度任务队列中等待再次被调度。排队结束后,内核态会重新变为用户态,重定向的地址为 main 函数的地址,但是此时会先将 masks 与 pending 按位与操作,如果某一位结果不为0则表示需要调用相应的信号处理函数或执行信号的默认动作。所以将地址重定向到信号的处理函数 sig_handler 的地址,并且将 masks 与 pending 的值都设置为0。信号处理函数处理完毕后,重新将 masks 设置为1,pending 设置为0,恢复为之前的状态,并将最终地址重定向到 main函数。

image.png

注意:

  • 信号并不是一接收就立刻响应,一般最长延迟为 10 ms。只有程序被打断并且重新被调度才有机会发现收到了信号,所以实际上按下 CTRL+C 程序并没有立刻结束,只是这个时间很短暂。
  • 当一个信号没有被处理,再次接收到多少个相同的信号都只能保留一个,因为 pending 作为位图,它只能保留最后接收到该信号的状态。
  • 信号处理函数不允许使用 longjmp 进行跳转,因为处理信号之前系统会将 mask 对应的位设置为0来避免信号处理函数重入,当信号处理完之后会把对应的 mask位设置为1。如果进行了跳转就无法对 masks 位进行恢复。
  • 信号的响应是嵌套执行的。假设进程先收到了 SIGINT 信号,当它的信号处理函数还没有执行完毕时又收到了另一个信号 SIGQUIT,那么它会先去处理 SIGQUIT 然后再回到上次被打断的地址处理 SIGINT。

2.4 发送信号

#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);

kill函数的参数列表:

  • pid > 0:将信号发送给进程ID为 pid的进程;
  • pid == 0:将信号发送给与发送进程属于同一进程组的所有进程;
  • pid < 0:将信号发送给其进程组ID等于 pid绝对值;
  • pid == -1:将信号发送给所有进程。

2.5 alarm、pause

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm 函数设置了一个定时器,在将来的某个时刻该定时器会超时。当定时器超时会产生 SIGALRM 信号,该信号的默认动作是终止调用该 alarm 函数的进程。

如果有以前注册的尚未超时的闹钟时间,则该闹钟的余值作为本次 alarm函数调用的返回值,以前注册的闹钟时间被新值替换。

下面是采用 alarm 实现的简单 sleep命令,它有几个问题:

(1):如果在调用 sleep1 之前,调用者已设置了闹钟,那么会被第一次 alarm 擦除。可以这样更正:检查第一次调用 alarm 的返回值,如果小于本次调用的参数值,则只等到已有的闹钟超时;如果大于本次调用的值,则等到之前设置的闹钟超时。

(2):如果其它函数要调用该函数,则需要先保存原始配置,在该函数返回前再恢复配置。可以这样更正,在调用 signal 时存储原始配置,在返回前重置原配置。

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

static void sig_alrm(int signo) {
   /* 什么都不做,返回等待超时 */
}

unsigned int sleep1(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) 
        return seconds;
    alarm(seconds);          /* 开始执行定时器 */
    pause();                 /* 等待 */
    return (alarm(0));
}

(3):在第一次调用 alarm和 pause之间存在竞争条件,可能 alarm在调用 pause之前超时,那么此时没有收到信号就会一直阻塞。解决方式可以通过setjmplongjmp函数实现跳跃。

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
#include "../err.h"

static jmp_buf env_alrm;

static void sig_alrm(int signo) {
    longjmp(env_alrm, 1);
}

unsigned int sleep2(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) 
        return (seconds);
    if (setjmp(env_alrm) == 0) {
        alarm(seconds);
        pause();
    }
    return (alarm(0));
}

static void sig_int(int signo) {
    int i, j;
    volatile int k;
    printf("\nsig_int starting\n");
    for (i = 0; i < 300000; i++) {
        for (j = 0; j < 4000; j++) {
            k += i * j;
        }
    }   
    printf("sig_int finished\n");
}

int main(void) {
    unsigned int unslept;
    if (signal(SIGINT, sig_int) == SIG_ERR) {
        err_sys("signal (SIGINT) error");
    }
    unslept = sleep2(5);
    printf("sleep2 returned : %u\n", unslept);
    exit(0);
}

sleep2 函数所引用的 longjmp 会提前结束信号处理程序 sig_int,即使它并未完成。alarm 函数除了实现sleep函数外,还常用于对可能阻塞的操作设置时间上限值。

在学习信号处理后,可以实现一个更优雅的版本,利用sigsuspend 函数来代替 pause 函数实现等待接收新信号:

static void sig_alrm(int signo) {
    /* 什么都不做,唤醒等待 */
}

unsigned int sleep3(unsigned int seconds) {
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    /* 设置SIGALRM信号处理函数 */
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    /* 阻塞SIGALRM信号保存 */
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    /* alarm等待超时 */
    alarm(seconds);
    suspmask = oldmask;
    /* 恢复SIGALRM */
    sigdelset(&suspmask, SIGALRM);
    /* 等待新信号 */
    sigsuspend(&suspmask);
    unslept = alarm(0);

    /* 恢复信号状态 */
    sigaction(SIGALRM, &oldact, NULL);
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return (unslept);
}

通过 alarm 来实现漏通流量控制:

需求:实现一个漏桶,每隔1s 打印10个字节的数据

实现思路:采用 alarm方法设置1s定时器,在SIGALRM 信号处理函数中重置alarm方法。维护一个全局变量 loop,当loop值为1时执行pause() 等待;当loop值为0时执行文件数据读取。在SIGALRM信号处理函数中将loop值设置为0,如此可以实现。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFSIZE 10
static volatile int loop = 0;

static void alarm_handler(int s) {
    alarm(1);
    loop = 0;
}

int main(int argc, char **argv) {
    int fd = -1;
    char buf[BUFSIZ] = "";
    ssize_t readsize = -1;
    ssize_t writesize = -1;
    ssize_t off = 0;

    if (argc < 2) {
        fprintf(stderr, "Usage %s <filepath>\n", argv[0]);
        return 1;
    }

    do {
        fd = open(argv[1], O_RDONLY);
        if (fd < 0) {
            if (EINTR != errno) {
                perror("open ()");
                goto e_open;
            }
        }
    } while (fd < 0);

    loop = 1;
    signal(SIGALRM, alarm_handler);
    alarm(1);

    while(1) {
        while (loop) {
            pause();
        }
        loop = 1;

        while((readsize = read(fd, buf, BUFSIZE)) < 0) {
            if (readsize < 0) {
                if (EINTR == errno) {
                    continue;
                }
                perror("read()");
                goto e_read;
            }
        }
        if (!readsize) {
            break;
        }
        off = 0;
        do {
            writesize = write(1, buf+off, readsize);
            off += writesize;
            readsize -= writesize;
        } while(readsize > 0);
    }
    close(fd);
    return 0;

e_read:
    close(fd);
e_open:
    return 1;
}

令牌桶与漏通类似,但是没有数据可读时漏桶会闲着,而令牌桶会不断积攒权限,当到来大数据时能够一次处理多个,可以很好应对流量激增的情况,令牌桶需要注意三个要素:可用令牌数、令牌上限、流量速率,下面通过 alarm 实现令牌桶:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFSIZE 20                //流量速率
#define MAXTOKEN 1024             //最大令牌数

static volatile int token = 0;    //积攒令牌数

static void alarm_handler(int s) {
    alarm(1);
    if (token < MAXTOKEN) {       //空闲时增加令牌数
        token++;
    }
}

int main(int argc, char **argv) {
    int fd = -1;
    char buf[BUFSIZE] = "";
    ssize_t readsize = -1;
    ssize_t writesize = -1;
    ssize_t off = 0;

    if (argc < 2) {
        fprintf(stderr, "Usage %s <filePath>\n", argv[0]);
        return 1;
    }

    do {
        fd = open(argv[1], O_RDONLY);
        if (fd < 0) {
            if (EINTR != errno) {
                perror("open()");
                goto e_open;
            }
        }
    } while(fd < 0);

    signal(SIGALRM, alarm_handler);
    alarm(1);

    while(1) {
        while(token <= 0) {    //令牌不足则等待增加令牌
            pause();
        }
        token--;

        while ((readsize = read(fd, buf, BUFSIZE)) < 0) {
            if (readsize < 0) {
                if (EINTR == errno) {
                    continue;
                }
                perror("read()");
                goto e_read;
            }
        }
        if (!readsize) {
            break;
        }
        off = 0;
        do {
            writesize = write(1, buf + off, readsize);
            off += writesize;
            readsize -= writesize;
        } while(readsize > 0);
    }
    close(fd);
    return 0;

e_read:
    close(fd);
e_open:
    return 1;
}

2.6 getitimer、setitimer

setitimer() 函数类似于 alarm(),均可以当作定时器或延时器使用,但是它能够精确到微秒。同时能实现定时的还有 select()函数,但是select() 函数会不断积累定时误差,运行到一定时刻可能误差会超出允许范围。所以推荐使用 setitimer()当作为定时函数。

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

struct itimerval {
    struct timeval it_interval; /* 周期性定时器的间隔 */
    struct timeval it_value;    /* 距离下次到期的时间 */
};

struct timeval {
    time_t      tv_sec;         /* 秒 */
    suseconds_t tv_usec;        /* 微秒 */
};

参数列表:

  • which:结束时发送不同的信号,包括 ITIMER_PROF(SIGPROF)、ITIMER_PEAL(SIGALRM)、ITIMER_VIRTUAL(SIGVTALRM);
  • new_value:新的定时器周期;
  • old_value:由该函数回填以前设定的定时器周期,不需要可以保存为NULL。

setitimer()的工作方式为:先对it_value进行递减,当值为0时触发信号。然后原子的将it_value重置为it_interval,之后继续对it_value倒计时,如此往复当it_value为0时停止。

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

void sigHanlder(int signo) {
    struct timeval tm;
    gettimeofday(&tm, NULL);

    switch (signo) {
        case SIGALRM:
            printf("Get the SIGALRM signal! time = %ld.%03ld\n",
                tm.tv_sec, tm.tv_usec/1000);
            break;
        case SIGVTALRM:
            printf("Get the SIGVTALRM signal! time = %ld.%03ld\n",
                tm.tv_sec, tm.tv_usec / 1000);
            break;
        case SIGPROF:
            printf("Get the SIGPROF signal! time = %ld.%03ld\n",
                tm.tv_sec, tm.tv_usec / 1000);
            break;
    }
}

int main() {
    struct itimerval new_value = {0};
    signal(SIGALRM, sigHanlder);

    //启动定时器时间
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;

    //定时器间隔时间
    new_value.it_interval.tv_sec = 3;
    new_value.it_interval.tv_usec = 0;

    if (setitimer(ITIMER_REAL, &new_value, NULL) < 0) {
        printf("Setitimer Failed : %s\n", strerror(errno));
        _exit(EXIT_FAILURE);
    }

    while (1) {
        pause();
    }
    return 0;
}

3、信号集

3.1 信号集操作

信号集代表一组信号的集合,通过 sigset_t 表示,并且定义了下面 5 个处理信号集合的函数:

#include <signal.h>

int sigemptyset(sigset_t *set);                 //清空信号集
int sigfillset(sigset_t *set);                  //初始化所有信号号到信号集
int sigaddset(sigset_t *set, int signo);        
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);  //判断信号是否在信号集中

3.2 sigprocmask

#include <signal.h>

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

通过 sigprocmask 函数能够检测或者更改进程的信号屏蔽字,即 masks位图。

参数列表:

  • how:如何干扰位图。
含义
SIG_BLOCK将当前进程的信号屏蔽字和 set 信号集中的信号全部屏蔽,也就是将它们的 mask 位设置为 0
SIG_UNBLOCK将 set 信号集中与当前信号屏蔽字重叠的信号解除屏蔽,也就是将它们的 mask 位设置为 1
SIG_SETMASK将 set 信号集中的信号 mask 位设置为 0,其它的信号全部恢复为 1
  • set:需要被设置的 mask位图信号集;
  • oset:干扰前的 mask位图信号集;

需求:每行打印5个星号,打印前先屏蔽信号,打印完成后恢复信号;然后再屏蔽打印,如此往复

思考:打印前通过 sigprocmask 函数屏蔽指定信号集并保存旧的信号集,开始打印信号;打印完成后再恢复屏蔽。注意恢复信号屏蔽到pause之间需要是一个原子操作,否则可能恢复屏蔽前接收到了 SIGINT 信号,但是此时 masks 位图为0,只是将 pending位图置为1,程序不会响应信号;然后恢复信号屏蔽,接着执行pause操作等待信号打断,如果后续都没有 SIGINT 信号产生,程序就会一直阻塞。

sigsuspend 函数的作用就是在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。

如果有这样的一个使用场景:某件事情需要信号驱动,但是在事件还未处理完毕时又不希望再次被信号打断。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void int_handler(int s) {
    write(1, "!", 1);
}

int main() {
    sigset_t set, oset, saveset;
    int i, j;
    signal(SIGINT, int_handler);

    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    sigprocmask(SIG_UNBLOCK, &set, &saveset);

    sigprocmask(SIG_BLOCK,&set,&oset);
    for(j = 0 ; j < 10000; j++)
    {
        for(i = 0 ; i < 5; i++)
        {
            write(1,"*",1);
            sleep(1);
        }
        write(1,"\n",1);
        sigsuspend(&oset);     //注意这里需要使用 sigsuspend,原因后续会讲解
        /*
        sigset_t tmpset;
        sigprocmask(SIG_SETMASK, &oset, &tmpset);
        pause();
        sigprocmask(SIG_SETMASK, &tmpset, NULL);
        */
    }
    sigprocmask(SIG_SETMASK, &saveset, NULL);
    exit(0);
}

3.3 sigpending

#include <signal.h>

int sigpending(sigset_t *set);

用于获取当前收到但是还未处理的信号,当进程从内核返回用户态时会将 masks位图和 pending位图做与计算,然后执行信号处理函数,最后返回用户态时重新设置信号位图,所以它的结果不准确了。

3.4 sigaction

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction* oact);

类似于signal函数,但是弥补了缺陷,比如该函数在执行信号处理时会屏蔽掉其它信号。

参数列表:

  • signo:要设定信号处理函数的信号;
  • act:信号处理函数;
  • oact:函数回填之前的信号处理函数,不用为NULL。
struct sigaction {
     // 前两个是信号处理函数,二选一,在某些平台上是一个共用体。
     void     (*sa_handler)(int); // 为了兼容 signal(2) 函数
     void     (*sa_sigaction)(int, siginfo_t *, void *); // 第二个参数可以获得信号的来源和属性。第三个参数最原始时是 ucontext_t* 而不是 void*,与 setcontext(3) 有关,目前该参数已经禁止使用。
     sigset_t   sa_mask; // 信号集位图,指定要处理的信号集,并且信号集中的任何一个信号被触发时,信号集中的其它成员同时会被 block,避免像 signal(2) 的信号处理函数一样当多个信号同时到来时发生重入。
     int        sa_flags; // 特殊要求。如果使用三参的信号处理函数,需要指定为 SA_SIGINFO
     void     (*sa_restorer)(void); // 基本被废弃了,不用管
     };

运用实例,下面的代码如果使用 signal 函数来注册信号处理函数,在一个信号到来时不会屏蔽掉其它信号,那么就会造成内存泄漏;如果使用 sigaction 函数,则当一个信号到来时会屏蔽掉其它信号,更加安全:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <errno.h>
#include <signal.h>
#include <string.h>

#define FNAME "/home/root/cppProject/lession10/res.txt"

static FILE *fp;

static int daemonize() {
    pid_t pid;
    int fd;
    pid = fork();
    if (pid < 0) {
        return -1;
    }
    if (pid > 0)
        exit(0);
    fd = open("/dev/null", O_RDWR);
    if (fd < 0) {
        return -2;
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2) {
        close(fd);
    }
    setsid();
    chdir("/");
    umask(0);
    return 0;
}

static void daemon_exit(int s) {
    fclose(fp);
    closelog();
    syslog(LOG_INFO, "daemonize_exit");
    exit(0);
}

int main() {
    int i;
    struct sigaction sa;
    //使用signal函数
    // signal(SIGINT, daemon_exit);
    // signal(SIGTERM, daemon_exit);
    // signal(SIGQUIT, daemon_exit);

    //使用sigaction函数
    sa.sa_handler = daemon_exit;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGINT);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);

    openlog("mydaemon", LOG_PID, LOG_DAEMON);

    //启动守护进程
    if (daemonize()) {
        syslog(LOG_ERR, "daemonize() failed.");
        exit(1);
    } else {
        syslog(LOG_INFO, "daemonize() successed.");
    }
    fp = fopen(FNAME, "w");
    if (fp == NULL) {
        syslog(LOG_ERR, "fopen():%s", strerror(errno));
        exit(1);
    }
    for (i = 0; ;i++) {
        fprintf(fp, "%d\n", i);
        fflush(fp);
        syslog(LOG_DEBUG, "%d was printed.", i);
        sleep(1);
    }
    exit(0);
}

3.5 sigsuspend

看这样一个场景,如果希望对一个信号解除阻塞,然后pause等待以前被阻塞的信号发生,有如下代码实现:

sigset_t  newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/* 阻塞SIGINT,并保存阻塞前的信号集 */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    err_sys("SIG_BLOCK error");
/* 解除对SIGINT信号的阻塞 */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_sys("SIG_SETMASK error");
/* 等待信号发生 */
pause();

如果在信号阻塞时产生了信号,那么信号的传递就推迟到它解除阻塞时发生。在阻塞时接收到信号将pending位置为1,此时mask位为0;等到阻塞解除此时mask位置为1,然后执行pause(),但是此时没有新的信号来中断pause() 操作,之前阻塞时的信号就永远不会被处理,看起来就好像丢失了一样。

为了解决这个问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。sigsuspend函数指明了进程的信号屏蔽字为 sigmask 所指向的值,如果捕捉到一个信号或者发生了一个会终止该进程的信号之前,该进程会被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

实例1,sigsuspend 函数的应用,观察调用 sigsuspend函数前后进程阻塞信号集的变化:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include "../err.h"

/* 获取调用进程中的信号屏蔽字的信号名 */
void pr_mask(const char* str) {
    sigset_t sigset;
    int errno_save;
    errno_save = errno;
    if (sigprocmask(0, NULL, &sigset) < 0) {
        err_ret("sigprocmask error");
    } else {
        printf("%s", str);
        if (sigismember(&sigset, SIGINT)) {
            printf(" SIGINT");
        }
        if (sigismember(&sigset, SIGQUIT)) {
            printf(" SIGQUIT");
        }
        if (sigismember(&sigset, SIGUSR1)) {
            printf(" SIGUSR1");
        }
        if (sigismember(&sigset, SIGALRM)) {
            printf(" SIGALRM");
        }
        printf("\n");
    }
    errno = errno_save;
}

void sig_int(int signo) {
    pr_mask("\nin sig_int: ");
}

int main(void) {
    sigset_t newmask, oldmask, waitmask;

    /* 设置SIGINT信号处理函数 */
    struct sigaction sa;
    sa.sa_handler = sig_int;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sa.sa_flags = 0;

    pr_mask("program start: ");
    sigaction(SIGINT, &sa, NULL);
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGUSR1);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);

    /* 阻塞SIGINT信号 */
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        err_sys("SIG_BLOCK error");
    }
    pr_mask("in critical region: ");

    /* 屏蔽等待 */
    if(sigsuspend(&waitmask) != -1) {
        err_sys("sigsuspend error");
    }
    pr_mask("after return from sigsuspend:");

    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        err_sys("SIG_SETMASK error");
    }
    pr_mask("program exit: ");
    exit(0);
}

实例2,等待一个信号处理函数设置一个全局变量,只有当全局变量改变时才继续执行否则pause:

volatile sig_atomic_t quitflag;

static void sig_int(int signo) {
    if (signo == SIGINT) {
        printf("\ninterrupt\n");
    } else if (signo == SIGQUIT) {
        quitflag = 1;
    }
}

int main(void) {
    sigset_t newmask, oldmask, zeromask;
    if (signal(SIGINT, sig_int) == SIG_ERR) {
        err_sys("signal (SIGINT) error");
    }
    if (signal(SIGQUIT, sig_int) == SIG_ERR) {
        err_sys("signal (SIGQUIT) error");
    }
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        err_sys("SIG_BLOCK error");
    }
    /* 等待信号去设置一个全局变量,否则pause()等待 */
    while(quitflag == 0) {
        sigsuspend(&zeromask);
    }
    quitflag = 0;
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        err_sys("SIG_SETMASK error");
    }
    exit(0);
}

3.6 sigqueue

大部分 UNIX 系统不对信号排队。实际可以通过 sigqueue 函数实现对信号的排队。但是使用信号排队必须做以下几个操作:

(1) 使用 sigaction 函数安装信号处理程序时指定 SA_SIGINFO 标志。如果没有给出这个标志,信号会延迟,但信号是否进入队列要取决于具体实现。

(2) 在 sigaction 结构的 sa_sigaction 成员中(不使用通常的 sa_handler 字段)提供信号处理程序。实现可能允许用户使用 sa_handler 字段,但不能获取 sigqueue函数发送出来的额外信息。

(3) 使用 sigqueue 函数发送信号:

#include <signal.h>

int sigqueue(pid_t pid, int signo, const union sigval value);