首页 > 编程语言 > 浅谈Linux信号机制
2021
09-22

浅谈Linux信号机制

一、信号列表

root@ubuntu:# kill -l

 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP

 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

63) SIGRTMAX-1 64) SIGRTMAX

其中最常见的:

  • Ctrl + C 触发的是 SIGINT;
  • Ctrl + \ 触发的是SIGQUIT;
  • 但是特别说明下 Ctrl + D 并不是触发信号,而是产生一个 EOF,这也是为什么在 Python 交互模式按下这个组合会退出 Python 的原因。

1.1、实时信号非实时信号

如上,kill列举出所有信号。实时信号与非实时信号又叫做可靠信号与不可靠信号。SIGRTMIN 及其以后的是实时信号,之前的是非实时信号。区别是实时信号支持重复排队,但是非实时信号不支持。非实时信号在排队时候会默认只出现一次,意思就是即使多次发送也终将只收到一个。在队列的取出顺序上也有区别,即最先取出的信号一定是实时信号。

PS:

  • kill、killall 默认发送SIGTERM 信号。
  • linux下 SIGKILL不能被阻塞、或忽略。
  • 默认情况下 SIGCHLD 不被忽略,编程时候需要注意这个(要么设置 SIG_IGN 或者主动 wait)。
  • 所有未定义处理函数的信号,默认退出进程。
  • 信号被设置block后仍存在于队列中只是不被处理,如果放开屏蔽将会被处理。
  • 信号可以中断sleep调用引起睡眠的进程。

1.2、信号状态

信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间;信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

例如在sleep前用 sigprocmask 阻塞了退出信号,然后sleep,然后在sleep的过程中产生一个退出信号,但是此时退出信号被阻塞过,(中文的”阻塞”在这里容易被误解为一种状态,实际上是一种类似于开关的动作,所以说“被阻塞过”,而不是“被阻塞”)所以处于“未决”状态,在 sleep后又用sigprocmask关掉退出信号的阻塞开关,因为之前产生的退出信号一直处于未决状态,当关上阻塞开关后,马上退出“未决”状态,得到处理,这一切发生在sigprocmask返回之前。

1.3、信号生命周期

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:

1.信号诞生;

2. 信号在进程中注册完毕;

3.信号在进程中的注销完毕;

4.信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。

下面阐述四个事件的实际意义:

  • 信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。
  • 信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:
struct sigpending pending;
struct sigpending
{
    struct sigqueue *head, **tail;
    sigset_t signal;
};

信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

1.信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用gqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不在进程的未决信号集中删除该信号(信号注销完毕)。进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

2.信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

1.4、信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

eg:

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

void myHandler(int num)
{
    int ret = 0;
    
    if (SIGUSR1 == num)
    {
        sigset_t set;
        ret = sigemptyset(&set);
        assert(!(-1 == ret));
        ret = sigaddset(&set, SIGINT);
        assert(!(-1 == ret));
        ret = sigaddset(&set, SIGRTMIN);
        assert(!(-1 == ret));
        ret = sigprocmask(SIG_UNBLOCK, &set, NULL);
        assert(!(-1 == ret));
        printf("解除阻塞 recv sig num: %d\n", num);
    }
    else if (num == SIGINT || num == SIGRTMIN)
    {
        printf("recv sig num: %d\n", num);
    }
    else
    {
        printf(" 其他信号recv sig num: %d\n", num);
    }
}

int main(void)
{
    pid_t pid;
    int ret = 0;
    // 设置回调函数
    struct sigaction act;
    act.sa_handler = myHandler;
    act.sa_flags = SA_SIGINFO;
    // 注册非实时信号的处理函数
    ret = sigaction(SIGINT, &act, NULL);
    assert(!(-1 == ret));
    // 注册实时信号的处理函数
    ret = sigaction(SIGRTMIN, &act, NULL);
    assert(!(-1 == ret));
    // 注册用户自定义信号
    ret = sigaction(SIGUSR1, &act, NULL);
    assert(!(-1 == ret));
    
    // 把 SIGINT  SIGRTMIN 军添加到阻塞状态字中
    sigset_t set;
    ret = sigemptyset(&set);
    assert(!(-1 == ret));
    ret = sigaddset(&set, SIGINT);
    assert(!(-1 == ret));
    ret = sigaddset(&set, SIGRTMIN);
    assert(!(-1 == ret));
    ret = sigprocmask(SIG_BLOCK, &set, NULL);
    assert(!(-1 == ret));
    
    pid = fork();
    assert(!(-1 == ret));
    if (0 == pid)
    {
        union sigval value;
        value.sival_int = 10;
        int i = 0;
        // 发三次不稳定信号
        for (i = 0; i < 3; i++)
        {
            ret = sigqueue(getppid(), SIGINT, value);
            assert(!(-1 == ret));
            printf("发送不可靠信号 ok\n");
        }
        
        // 发三次稳定信号
        value.sival_int = 20;
        for (i = 0; i < 3; i++)
        {
            ret = sigqueue(getppid(), SIGRTMIN, value);
            assert(!(-1 == ret));
            printf("发送可靠信号ok\n");
        }
        // 向父进程发送 SIGUSR1 解除阻塞
        ret = kill(getppid(), SIGUSR1);
        assert(!(-1 == ret));
    }
    while (1)
    {
        sleep(1);
    }
    return 0;
}

二、信号掩码和信号处理函数的继承

2.1、信号处理函数的继承

信号处理函数是进程属性,所以进程里的每个线程的信号处理函数是相同的。通过fork创建的子进程会继承父进程的信号处理函数。execve 后设置为处理的信号处理函数会被重置为默认函数,设置为忽略的信号保持不变。意思是如果父进程里信号设置处理为SIG_IGN,那么等到子进程被exec了,这个信号的处理还是被忽略,不会重置为默认函数。

eg:

// test.c --> test
#include <stdlib.h>
  
typedef void (*sighandler_t)(int);
static sighandler_t old_int_handler;
  
static sighandler_t old_handlers[SIGSYS + 1];
  
void sig_handler(int signo)
{
    printf("receive signo %d\n",signo);
    old_handlers[signo](signo);
}
  
int main(int argc, char **argv)
{
    old_handlers[SIGINT] = signal(SIGINT, SIG_IGN);
    old_handlers[SIGTERM] = signal(SIGTERM, sig_handler);
  
    int ret;
  
    ret = fork();
    if (ret == 0) {
        //child
        // 这里execlp将运行 test2 作为子进程。
        execlp("/tmp/test2", "/tmp/test2",(char*)NULL);
    }else if (ret > 0) {
        //parent
        while(1) {
            sleep(1);
        }
    }else{
        perror("");
        abort();
    }
  
}
  
================================================
test2.c --> test2
#include <stdio.h>
int main(int argc, char **argv)
{
    while(1) {
        sleep(1);
    }
    return 0;
}

结论:test换成test2后,SIGINT的处理方式还是忽略,SIGTERM被重置为默认的方式。

2.2、信号掩码的继承

信号掩码有以下规则:

1.每个线程可以有自己信号掩码。

2.fork出来的子进程会继承父进程的信号掩码,exec后信号掩码保持不变。如果父进程是多线程,那么子进程只继承主线程的掩码。

3.针对进程发送的信号,会被任意的没有屏蔽该信号的线程接收,注意只有一个线程会随机收到。linux下如果都可以所有线程都可以接收信号,那么信号将默认发送到主线程,posix系统是随机发送。

4.fork之后子进程里pending的信号集初始化为空,exec会保持pending信号集。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
  
typedef void (*sighandler_t)(int);
  
static void *thread1(void *arg)
{
    sigset_t set;
      
    printf("in thread1\n");
  
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &set, NULL);

    while(1) {
        sleep(1);
    }
}
  
static void sigset_print(sigset_t *set)
{
    int i;
  
    for (i = 1; i <= SIGSYS; i++) {
        if (sigismember(set, i)) {
            printf("signal %d is in set\n",i);
        }
    }
}
  
int main(int argc, char **argv)
{
    int ret;
    sigset_t set;
    pthread_t pid;
  
    pthread_create(&pid, NULL, thread1, NULL);
    sleep(1);
  
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
  
    ret = fork();
    if (ret == 0) {
        //child
        pthread_sigmask(SIG_BLOCK, NULL, &set);
        sigset_print(&set);
  
        while(1) {
            sleep(1);
        }
    }else if (ret > 0) {
        //parent
        while(1) {
            sleep(1);
        }
    }else{
        perror("");
        abort();
    }
  
}

结论:只有在主线程里设置的掩码才被子进程继承了。这里面的原因在于linux里的fork只是复制了调用fork()的那个线程,因此在子进程里只有父进程的主线程被拷贝了,当然信号掩码就是父进程的主线程的信号掩码的复制了。再次验证证明,如果是在thread1里调用fork,那么子进程的信号掩码就会是thread1的拷贝了。

2.3、sigwait 与多线程

sigwait函数:sigwait等一个或者多个指定信号发生。

它所做的工作只有两个:

第一,监听被阻塞的信号;

第二,如果所监听的信号产生了,则将其从未决队列中移出来。sigwait并不改变信号掩码的阻塞与非阻塞状态。

在POSIX标准中,当进程收到信号时,如果是多线程的情况,我们是无法确定是哪一个线程处理这个信号。而sigwait是从进程中pending的信号中,取走指定的信号。这样的话,如果要确保sigwait这个线程收到该信号,那么所有线程含主线程以及这个sigwait线程则必须block住这个信号,因为如果自己不阻塞就没有未决状态(阻塞状态)信号,别的所有线程不阻塞就有可能当信号过来时,被其他的线程处理掉。

PS:

在多线程代码中,总是使用sigwait或者sigwaitinfo或者sigtimedwait等函数来处理信号。而不是signal或者sigaction等函数。因为在一个线程中调用signal或者sigaction等函数会改变所以线程中的信号处理函数,而不是仅仅改变调用signal/sigaction的那个线程的信号处理函数。

2.4、多进程下的信号

多进程下键盘触发的信号会同时发送到当前进程组的所有进程。如果一个程序在执行时 fork 了多个子进程,那么按键触发的信号将会被这个程序的所有进程收到。

但是与多线程不一样,多进程下的信号掩码和信号处理函数是独立的。每个进程都可以选择处理或者不处理,也可以设置自己的信号掩码。

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

int main(int argc, char **argv)
{
    pid_t pid = fork();

    signal(SIGCHLD, SIG_IGN);
    if (pid < 0)
        printf("error fork\n");
    else if (pid == 0)
    {
        signal(SIGINT, SIG_IGN); // 忽略 SIGINT,这样 ctrl+c 后子进程能活下来; 不设置的话,收到信号将退出
        printf("child gid = %ld\n", getpgid(getpid()));
        do
        {
            sleep(1);
        } while (1);
    }
    else
    {
        printf("parent gid = %ld\n", getpgid(getpid()));
        do
        {
            sleep(1);
        } while (1);
    }

    return 0;
}

如上图,可以看到,收到SIGINT 后父进程退出,子进程因为设置了忽略 SIGINT 所以子进程没有受到影响。

三、apis

3.1、信号发生函数

1.kill(pid_t pid, int signum);

2.int sigqueue(pid_t pid, int sig, const union sigval value);

3.pthread_kill(pthread_t tid, int signum);

4.raise(int signum);// 发送信号到自己

5.void alarm(void);

6.void abort(void);

7.int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

PS:

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

3.2、信号处理函数

1.signal(int signum, void (*handler)(int signum))

2.sigaction(int signum, struct sigaction* newact, sigaction* oldact)

sigaction act;
act.sa_handler = handler;
act.sa_flags = SA_SIGINFO;
// 注册信号的处理函数
sigaction(SIGINT, act, NULL);

3.3、信号掩码函数

1.sigprocmask(int how, struct sigaction* set,struct sigaction* oldset)

2.pthread_sigmask(int how, struct sigaction* set,struct sigaction* oldset)

sigprocmask用于设置进程的信号掩码,pthread_sigmask用于设置线程的信号掩码,二者参数相同。第一个参数有SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK

3.4、信号集合变量

sigset_t set

sigemptyset(&set) //清空阻塞信号集合变量

sigfillset(&set)  //添加所有的信号到阻塞集合变量里

sigaddset(&set,SIGINT) //添加单一信号到阻塞信号集合变量

sigdelset(&set,SIGINT) //从阻塞信号集合变量中删除单一信号

sigismember(&set,int signum) //测试信号signum是否包含在信号集合set中,如果包含返回1,不包含返回0,出错返回-1。错误代码也只有一个EINVAL,表示signum不是有效的信号代码。

3.5、信号屏蔽函数

1.int sigpending(sigset_t *set); // 返回阻塞的信号集

2.int sigsuspend(const sigset_t *mask);

sigsuspend表示临时将信号屏蔽字设为mask,并挂起进程直到有信号产生(非屏蔽信号才能唤醒或终止进程),如果信号处理函数返回,那么siguspend将恢复之前的信号屏蔽字(temporarily)

假设sisuspend阻塞进程时产生了信号A,且A不是mask内的屏蔽信号,那么A的信号处理函数有两种情形,

一:直接终止进程,此时进程都不存在了,那么sigsuspend当然无须返回了(不存在进程了sigsuspend也不存在了,函数栈嘛);

二:如果信号A的处理函数返回,那么信号屏蔽字恢复到sigsuspend之前的(sigsuspend调用时将信号屏蔽字设为mask,所以要恢复到sigsuspend调用之前的),然后sigsuspend返回-1并将error置为EINTR.

以上就是浅谈Linux信号机制的详细内容,更多关于Linux信号机制的资料请关注自学编程网其它相关文章!

编程技巧