Linux信号详解

每个信号都有一个编号和宏定义,在signal.h中可以找到

可通过kill -l 命令查看所有信号  1-31为普通信号


信号的产生

  1. 通过终端按键产生

    用户通过键盘按键,如ctrl+c给前台进程发送2号信号SIGINT,该信号的默认动作为终止进程,当进程收到此信号时,执行默认动作终止该进程。

  2. 调用系统函数

    int kill(pid_t pid, int signo);//这两个函数都是成功返回0,错误返-1

    int raise(int signo);

    void abort(void);//无返回值

    kill命令通过调用kill函数也可以给指定的进程发送指定的信号

    raise()给当前进程发送指定的信号(自己给自己发送信号)

    abort()使当前进程收到SIGABRT信号而异常终止

  3. 软件行为

    用户可通过调用一些函数产生信号。

    unsigned int alarm(unsigned int seconds);//这个函数的返回值是0或者是以前设定的闹钟时间       m(unsigned int seconds);//这个函数的返回值是0或者是以前设定的闹钟时间                                      还余下的秒数

    通过函数设定了一个闹钟,seconds秒之后给进程发送一个SIGALRM信号,该信号的默认动作为终止进程。

  如果seconds的值i为0则取消之前设定的闹钟


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

int main()
{
    int count=5;
    while(1){
        printf("i am runing\n");
        sleep(1);
        while(--count<=0){
            //kill(getpid(),SIGTERM);
            //kill(getpid(),SIGINT);
            //kill(getpid(),SIGQUIT);

            //raise(SIGTERM);

            abort();
        }
     }
    return 0;
}

while循环里五个函数分别运行结果:

Linux信号详解_第1张图片Linux信号详解_第2张图片

Linux信号详解_第3张图片Linux信号详解_第4张图片

Linux信号详解_第5张图片

可看出该进程收到的信号不同



信号的阻塞和递达

信号在收到的时候不是立马被处理,而是在一个合适的时刻才会被处理。这个合适的时刻是当进程产生异常、中断或者有系统调用的时候才会处理。


先介绍两个概念,未决和递达

未决信号在收到之后,处理之前处于未决状态,称为信号的未决(Pending)。

递达:信号被处理的动作叫做信号的递达(Delivery)。


每个进程的PCB中都保存了一个pending表、一个block表和一个handler表,每个信号都对应两个标志位(pending和block)和一个函数指针(指向信号处理函数)

信号在内核中的表示如下图:

Linux信号详解_第6张图片

当进程收到某种信号时,先保存到Pending信号集中,如果Block信号集中对应的bit位为1,说明该信号被阻塞,信号不会被递达,如果对应bit位为1,表明该信号没有被阻塞,则到一个合适的时刻该信号会被递达,递达即为信号的处理,根据Handler表做出该信号相应的动作(3种),如果为SIG_IGN表示忽略此信号(注意,忽略和阻塞是不一样的,SIG_DFL为执行默认动作,第三种是处理自定义的动作(事先对该信号注册新号处理函数)。当信号被递达,Pending信号集对应的bit位会被清除(注意,由于每个信号对应一个bit位,所以在信号处于未决状态时内无论该信号产生多少次都只会被递达一次


未决和阻塞信号集用sigset_t类型来存储


下面是对信号集操作的函数

 #include <signal.h>

  1. int sigemptyset(sigset_t *set);//成功返回0,失败返回-1

    int sigfillset(sigset_t *set);

    初始化set所指向的信号集,分别使信号集对应的比特位全部置0(sigemptyset),全部置1(sigfillset)。

    注意:在使用sigset_t类型的变量的时候一定要初始化

  2. int sigaddset(sigset_t *set,int signo);//成功返回0,失败返回-1

    int sigdelset(sigset_t *set,int signo);

    往set所指向的信号集中添加(删除)signo所对应的信号(signo是某个信号所对应的值,如#define SIGINT 2)

  3. int sigismumber(const sigset_t *set,int signo);

    判断signo该有效信号是否为set所指向信号集的成员,若包含则返回1,不包含则返回0,出错返回-1。

  4. int sigprocmask(int how,const sigset_t *set,sigset_t *oset);////成功返回0,失败返回-1

    how可取三种值  SIG_BLOCK  set所指向信号集添加到阻塞信号集中,mask=mask|set

               SIG_UNBLOCK 从阻塞信号集中取消set所指定的信号集中bit位为1的信                                              号,mask=mask&~set

               SIG_SETMASK 设置当前信号屏蔽字为set所指向的信号集的值,mask=set

    oset若非空,保存进程当前阻塞信号集中的值

  5. int sigsending(sigset_t *set);//调用成功则返回0,出错则返回-1

    获取当前进程的pending信号集,并通过set传出


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

void show_member(sigset_t *sig_set)
{
    int count=1;
    while(count<32){
        int ret=sigismember(sig_set,count);
        if(ret==1){
            printf("1");
        }
        else if(ret==0){
            printf("0");
        }
        else
            printf("error\n");
        count++;
    }
    printf("\n");
}
int main()
{
    sigset_t sig_set;
    sigemptyset(&sig_set);
    sigaddset(&sig_set,2);
    sigaddset(&sig_set,6);
    //  sigfillset(&sig_set);
    //  sigdelset(&sig_set,2);
    //  sigdelset(&sig_set,5);
    //  sigdelset(&sig_set,9);
    
    sigset_t old_sig_set;
    sigemptyset(&old_sig_set);

    sigprocmask(SIG_BLOCK,&sig_set,&old_sig_set);//设置信号屏蔽字,编号为2和6的信号被添加到阻塞信号集中
    show_member(&sig_set);//显示当前的阻塞信号集
    show_member(&old_sig_set);//显示旧的阻塞信号集
    printf("*********************************\n");
    sleep(5);

    sigaddset(&sig_set,31);
    sigaddset(&sig_set,30);
    sigaddset(&sig_set,29);
    sigdelset(&sig_set,6);
    sigprocmask(SIG_SETMASK,&sig_set,&old_sig_set);//同上
    show_member(&sig_set);
    show_member(&old_sig_set);
    printf("*********************************\n");
    sleep(3);
    
    sigset_t pending_set;
    sigpending(&pending_set);//获取当前的pending信号集
    show_member(&pending_set);
    printf("this is pending_set\n");
    
    return 0;
}

运行结果:

Linux信号详解_第7张图片

可看到当前的pending信号集中编号为2的信号对应bit位为1,那是因为在获取pending信号集之前我通过键盘给进程发送了2号信号(ctrl+c),但该信号被阻塞了,所以一直保存在pending信号集中,如果我不发送该信号,则会产生如下运行结果(pending信号集对应的bit位全部为0)

Linux信号详解_第8张图片


信号的捕捉

信号的处理动作可以有三种(忽略、执行默认动作、用户自定义动作)

如果信号的处理动作是用户自定义的,则该动作称为信号的捕捉,信号的捕捉过程如下:

  1. 当程序执行某条指令时发生中断、异常或系统调用的时候系统会切换到内核模式执行相应的处理

  2. 当处理完中断或异常后不会立即切换回用户态,而是先检查时候有信号可被递达

  3. 如果有信号可递达(收到该信号且未被阻塞),且该信号的处理动作为用户自定义动作,切换到用户态执行该信号事先注册的函数

  4. 自定义函数执行完并返回后自动进入内核态执行特殊的系统调用函数sigreturn

  5. 如果没有其它信号可递达,切换回用户模式,恢复上下文,执行被中断处的下一条指令

Linux信号详解_第9张图片


信号捕捉有关函数

#include <signal.h>

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

    把signo所对应的信号原来的执行动作保存到oact中,并执行新的动作act

    Linux信号详解_第10张图片

    sa_handler是一个函数指针,若为常数SIG_IGN则忽略该信号,若为SIG_DFL则执行默认动作,若指向一个函数则执行该函数定义的动作(该函数的返回值为void 参数为int 可通过该参数获得该信号所对应的编号 该函数是个回调函数,由系统调用,而不是main函数,参照上面的图可知)


  sa_mask设置需要额外屏蔽的信号,本次线程执行时会屏蔽,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

  当某个信号的处理函数被执行时,该信号会被自动添加到该进程的信号屏蔽字,若有该信号再次到产生时,它会被阻塞知道本次函数处理完。

2.#include <unistd.h>

int pause(void);

 该函数会使当前进程挂起,直到有一个信号递达。

 若信号的处理动作为忽略时,进程会继续挂起,若为默认(一般为终止进程),则终止该进程,pause函数没机会返回,若为动作为捕捉,则执行该自定义函数,函数返回-1,error设为ENITR,表示“信号被中断”。

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

 void hander(int arg)
  {
    printf("this is hander!\n");
    sleep(2);
  }

  int main()
  {
    sigset_t sig_set;
    sigemptyset(&sig_set);

    struct sigaction act;
    struct sigaction oact;
    
    act.sa_handler=hander;
    act.sa_mask=sig_set;
    //act.sa_mask=sig_set;
    act.sa_flags=0;

    sigaction(2,&act,&oact);

    while(1){
        printf("####################\n");
        sleep(1);
    }

    return 0;
  }

运行结果:

Linux信号详解_第11张图片


当我发送2号信号(ctrl+c)会执行注册的handler函数,而不会终止进程

  #include <stdio.h>
  #include <signal.h>
  #include <unistd.h>
  
  void hander(int sig)
  {
    //do nothing
  }
  
  void mysleep(size_t times)
  {
    sigset_t sig_set;
    sigemptyset(&sig_set);
 
    struct sigaction act;
    struct sigaction oact;
    //act.sa_handler=SIG_IGN;
    act.sa_handler=hander;
    act.sa_mask=sig_set;
    act.sa_flags=0;

    sigaction(SIGALRM,&act,&oact);
    alarm(times);//times秒之后发送一个SIGALRM信号
    pause();
    alarm(0);
    sigaction(SIGALRM,&oact,&act);
    printf("******************\n");
    //alarm(1);
    //pause();
    //alarm(0);
  }

  int main()
  {
    mysleep(5);
    printf("I have sleep 5 seconds...\n");

    return 0;
  }

运行结果:

wKiom1csgWvzBRe5AAAHR449cBU851.png

如果加上注释的3行,运行结果如下:

wKiom1csghCCiJR3AAABjeav8hM597.png

上面代码用alarm()和pause()实现了sleep()函数的功能,pause()把进程挂起,times秒之后alarm()给进程发送信号SIGALRM,进程被唤醒,执行自定义动作(此时函数内部什么都没做,但这里不能忽略或使该信号执行默认动作),alarm(0)取消闹钟


但上面代码在多线程环境下是有问题的,我们假设该进程的优先级非常非常的低,当它执行完alarm(times)后就被中断,系统执行别的进程,当再次回来执行该线程的时候,有可能时间已经超时了(即大于times),所以在SIGALRM信号已经递达,pause()此时才把进程挂起。

这中错误是因为时序而产生的错误,叫做竞态条件


如果我们按如下步骤是不是就解决该问题了:

  1. 屏蔽信号SIGALRM

  2. 执行alarm(times)

  3. 解除信号的屏蔽

  4. 执行pause()挂起进程

    如果把上述的第3步和第4步合为一步,即要不都执行要不都不执行,就不会出现进程被一直挂起的情况了


#include <signal.h>

int sigsuspend(const sigset_t *sigmask);


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

 void handler(int sig)
  {
    //do nothing
  }

  size_t mysleep(size_t times)
  {
    sigset_t new_sig_set,old_sig_set;
    sigemptyset(&new_sig_set);
    sigemptyset(&old_sig_set);
    sigaddset(&new_sig_set,SIGALRM);
    sigprocmask(SIG_BLOCK,&new_sig_set,&old_sig_set);//把信号SIGALRM屏蔽
    
    struct sigaction act,oact;
    act.sa_handler=handler;
    act.sa_flags=0;
    //    sigemptyset(&act.sa_mask);
    act.sa_mask=old_sig_set;
    sigaction(14,&act,&oact);
    
    sigset_t sig_mask=old_sig_set;
    sigdelset(&sig_mask,14);//make sure SIGALRM isn't block 
    alarm(times);
    sigsuspend(&sig_mask);//解除信号SIGALRM的屏蔽并将进程挂起
    alarm(0);
    
    sigaction(SIGALRM,&act,&oact);//恢复信号SIGALM的处理动作
    sigprocmask(SIG_UNBLOCK,&old_sig_set,NULL);//恢复阻塞信号集
  }
  
  int main()
  {
    mysleep(3);
    printf("i have wake up...\n");

    return 0;
  }

运行结果:

3秒钟之后输出下面一句话

wKioL1csh8Dg14mWAAAFFAjm7VU788.png


你可能感兴趣的:(linux,Signal)