当先锋百科网

首页 1 2 3 4 5 6 7

一,信号,信号集

1. 信号相关的概念

        信号递达:实际执行信号的处理动作

        信号未决:从信号产生到信号递达之间的状态

        信号阻塞:如果一个信号被阻塞,那它在产生时处于未决状态,不会被递达,只有解除该信号后,才被递达。

注意:信号的阻塞,是还未执行处理动作,而信号的忽略是正在处理信号,但信号的处理动作是忽略。这两者是不同的。

2. 信号在内核中的表示

        我们之前一直说操作系统向进程发送了一个信号,但操作系统是如何发送的,见下图:


        上图应该横向来看,每一行表示一种信号。上图表明,在每个进程的PCB中都有一个有关信号的指针,该指针指向一张表,该表中有三部分内容:

block位图:表示每种信号是否被阻塞,0表示没有被阻塞,1表示被阻塞。

pending位图:表示每种信号的未决状态,1表示该信号已经已经产生,但还未递达。0表示信号没有产生,或者产生后已经递达了,此时该标志位也变为0。

handler表:实际是一函数指针数组,数组每个元素对应处理该信号的函数指针。如果是SIG_DEL表示自行默认处理动作,如果是SIG_IGN表示忽略该信号,如果是用户自定义处理动作,则保存的是自定义函数的指针,该自定义函数位于用户空间中。

        因此,操作系统向进程发送信号是指将该表中的pending位图中的该信号对应状态位由0变为1。如果该信号的block标志位为1,表明该信号被阻塞,所以不会被递达,只有解除阻塞后才会递达。用户可以通过修改handler表中该信号对应的函数指针。当接收到该信号时,来执行自定义处理动作,这就叫信号的捕捉。

        常规信号在递达前产生多次时,只会被记一下,而实时信号可以将其全部放入一个队列中。这里只讨论常规信号。

二, 信号集及相关操作函数

        上面我们已经了解了操作系统是如何发送信号的。当我们使用触发或采取二中信号产生的条件时,操作系统会将pending表中对应的比特位由0变为1来产生信号。也就是说pending表是由操作系统来修改的。如果我们想阻塞某种信号,就必须人为的对block表中的比特位进行修改,以及想知道pending表和block表中的相关内容,这些又如何做到呢?

1. 信号集sigset_t

        我们要查看或修改block表,首先要知道它是如何存储的,即它是什么类型的。从上图中可以看到,pending表和block表的结构很相似,都是一个比特位表示一种信号的未决或阻塞状态。所以,所有信号的未决状态信息和阻塞状态信息,可以用同一种类型来存储,这个类型就叫做信号集sigset_t。阻塞信号集也叫作信号屏蔽字。

2. 信号集操作函数

        知道了block表和pending表的存储类型,便可以对其查询和修改了。sigset_t类型内部是如何存储这些比特位的,这些我们是不必关心的。如果是一个整型变量,可以直接对其赋值进行修改。而sigset_t类型的变量必须通过调用以下函数来实现:

        注意:对sigset_t类型的变量进行查看或修改只能通过调用以下函数,而直接printf打印该类型的变量是没有意义的

        以下函数的头文件均为<signal.h>

int sigemptyset(sigset_t* set);

        该函数的功能是使set所指向的信号集变量的所有比特位清零,比如set所指向的是当前进程的阻塞信号集,所以在该进程中的所有信号都处于未屏蔽状态。

int sigfillset(sigset_t* set);

      该函数的功能是使set所指向的信号集变量的所有比特位均变为1。比如set所指向的是当前进程的阻塞信号集,所以在该进程中的所有信号都处于屏蔽状态。

int sigaddset(sigset_t *set,int signo);

        该函数的作用是在set所指向的信号集中使signo信号变为有效信号。

int sigdelset(sigset_t* set,int signo);

        该函数的作用是在set所指向的信号集中使signo信号变为无效信号。

int sigismember(const sigset_t* set,int signo);

        该函数的作用是判断set所指向的信号集中signo信号是否有效。

        前四个函数都是成功返回0,失败返回-1。最后一个函数有效返回1,无效返回0,出错返回-1。

注意:在sigset_t变量之前,一定要用sigemptyset或sigfillset函数对该变量进行初始化,才可使用后面的函数对其进行操作。

三,sigprocmask

        上面已经知道如何对sigset_t类型的变量进行修改和查询。而阻塞信号集就是sigset_t类型的。所以下面具体说明如何对信号屏蔽字即阻塞信号集来实现查询和修改。

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

        该函数的功能是读取和修改进程的信号屏蔽字即阻塞信号集。

参数:

        oset:如果该参数非空,相当于保存该进程原本的信号屏蔽字。

        set:如果该参数非空,则将根据how和set修改该进程的信号屏蔽字

        how:有以下三种

            SIG_BLOCK:此时set中包含的是我们希望添加到当前信号屏蔽字中的信号,相当于mask = mask|set

            SIG_UNBLOCK:此时set中包含的是我们希望从当前信号屏蔽字中解除的信号,相当于mask = mask&~set

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

返回值:成功返回0,失败返回-1

四,sigpending

        sigprocmask告诉我们如何对阻塞信号字进行读取和修改,那sigpending将会使我们读取进程的未决信号集。

int sigpending(sigset_t* set);

        该函数的功能就是读取进程的未决信号字并将其保存在set所指向的空间中,所以set是一个输出型变量。

        如果函数执行成功,返回0,执行失败,返回-1。

        下面通过代码来演示如何对阻塞屏蔽字和未决信号集进行操作:

  #include<stdio.h>                                                                                                                   
  #include<signal.h>
  #include<unistd.h>
  
  //打印未决信号集函数
  void printpending(sigset_t pending)
  {
      int i = 1;
      for(;i < 32;i++)
      {
          if(sigismember(&pending,i))//如果第i号信号为有效信号,则打印1,否则打印0
          {
              putchar('1');
          }
          else
          {
              putchar('0');
          }
      }
      printf("\n");
  }
  int main()
  {
      sigset_t pending;//设置信号集变量用于保存未决信号集
      sigset_t set,oset;//定义信号集变量,用于设置新屏蔽信号字和保存原来的信号屏蔽字
      sigemptyset(&set);//初始化信号集
      sigaddset(&set,2);//将2号信号设置为有效信号
      sigprocmask(SIG_SETMASK,&set,&oset);//屏蔽2号信号
      int i = 1;
      while(1)
      {
          sigpending(&pending);//读取未决信号集,并保存在信号集变量pending中
          printpending(pending);//打印未决信号集pending
          sleep(1);                                                                                                                   
          if(i == 3)//3s后向本进程发送2号信号
          {
              raise(2);
          }
          if(i == 5)
          {
              sigprocmask(SIG_UNBLOCK,&set,NULL);//5s后解除对2号信号的屏蔽
          }
          i++;
      }
      return 0;
  }

        运行结果如下:

[admin@localhost sigset]$ ./a.out 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000

        上述打印的是pending表,因为前三秒没有任何信号产生,所以各普通信号的未决状态均为无效即0,3秒后,利用raise向本进程发送了2号信号,且2号信号一直都处于阻塞状态,所以信号产生后不会被递达的,因此3秒后2号信号为未决状态。在5秒后解除了对2号信号的屏蔽,此时2号信号被递达去执行默认处理动作:使进程退出。

五,捕捉信号

        上面描述了如何对block表和pending表进行读取和修改,下面研究如何对handler表进行修改。

        如果信号的处理动作是用户自定义处理动作,在信号递达时就去调用这个函数,这就叫信号的捕捉。

        我们之前有说过,在一个信号产生后操作系统会先向该进程发送该信号,上面已经知道,发送信号实际是将pending表中该信号对应的比特位由0改为1。但是并不会立即处理该信号,而是在合适的时机去处理。

        合适的时机其实是进程由内核态返回至用户态时,对信号进行检测发现有未决的信号并且该信号没有被阻塞就去处理它即递达。当该信号的处理动作是忽略时,直接忽略返回用户态即可,当处理动作是默认时,会直接结束进程(一般信号的默认处理动作是结束进程),不再返回。当信号的处理动作是自定义行为,而用户自定义函数代码是在用户空间的,则需要以下几个步骤:


        注意:main函数和sighandler函数使用不同的堆栈空间,他们之间不存在调用和被调用关系,是两个独立的控制流程

        下面通过两个函数来进行信号的捕捉:

1. signal函数

#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

        该函数的功能是修改signum信号的处理动作为handler指向的函数。

参数:

        signum:表示要捕捉的信号序号

        handler:是一回调函数,当信号signum产生并且没有被阻塞时,当该信号被递达时,就去执行该回调函数,该回调函数有一个整型参数,表示对哪个信号进行处理。

        代码演示:

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

//自定义的信号处理函数
void handler(int num)
{
    printf("catch signo %d\n",num);
}
int main()
{
    signal(2,handler);//捕捉2号信号                                                                                                   
    signal(3,handler);//捕捉3号信号
    while(1)
    {   
        printf("hehe\n");
        sleep(1);
    }   
    return 0;
}

        运行结果:

[admin@localhost signal]$ ./a.out 
hehe
^Ccatch signo 2
hehe
^\catch signo 3
hehe
^Z
[1]+  Stopped                 ./a.out

        程序运行开始后,在第2秒按Ctrl+C向该进程发送2号信号,该信号的处理动作是用户自定义函数handler,所以执行输出语句。第4秒按Ctrl+\向该进程发送3号信号,该信号的处理动作同样是用户自定义函数handler,所以执行输出语句。第6秒时按Ctrl+Z发送SIGTSTP信号,该信号的处理动作是默认行为即终止进程,所以进程退出了。

2. sigaction函数

int sigaction(int signo,const struct sigaction* act,struct sigaction* oact);//头文件<signal.h>

        该函数的功能是读取或修改信号signo的相关处理动作。如果正在执行该信号的处理动作时,又发来了该信号,系统会自动屏蔽该信号,直到执行结束才解除屏蔽。结构体struct sigaction为:

struct sigaction {
               void     (*sa_handler)(int);//信号的处理动作
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;//当正在执行信号处理动作时,希望屏蔽的信号。当处理结束后,自动解除屏蔽
               int        sa_flags;//一般为0
               void     (*sa_restorer)(void);
           };

参数:

        signo:信号的编号

        act:如果非空,则修改该信号的相关信息为act所指向的结构体

        oact:如果非空,保存该信号的原有信息到oact所指向的结构体中

返回值:成功返回0,失败返回-1

代码演示:

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

void handler(int signo)
{
    int i = 2;                                                                                                                        
    while(i--)
    {   
        printf("catch signo %d\n",signo);//输出2条该语句
        sleep(1);
    }   
}
int main()
{
    struct sigaction act,oact;//act用来设置信号的相关信息,oact用来保存信号原有的相关信息
    act.sa_handler = handler;//将信号的处理动作设置为handler
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);//对3号信号进行屏蔽 对比设置该语句前后,输入ctrl+c和ctrl+\的差别 
    sigaction(2,&act,&oact);//自定义2号信号的处理动作
    sigaction(3,&act,&oact);//自定义3号信号的处理动作
    int i = 1;
    while(1)
    {   
        printf("hehe\n");
        sleep(1);
        if(i == 5)
        {
            sigaction(2,&oact,NULL);//5s后恢复2号信号的原处理动作
            oact.sa_handler = SIG_IGN;//5s后将3号信号的处理动作设置为忽略
            sigaction(3,&oact,NULL);
        }
        i++;
    }
    return 0;
}           

运行结果如下:

[admin@localhost signal]$ ./a.out 
hehe
hehe
^Ccatch signo 2
^\catch signo 2    //当在捕捉2号信号时输入Ctrl+C时,因为3号信号被屏蔽,所以不会捕捉3号信号
catch signo 3      //当处理完2号信号时,自动解除3号信号的屏蔽,所以会捕捉2号信号
catch signo 3
hehe
hehe
hehe
hehe
^\hehe  //5s后3号信号的处理动作为忽略
hehe
hehe
^C    //5s后2号信号的处理动作为默认即终止进程