Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)

时间:2020-12-15 14:45:30

信号在内核中的表示

    实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以看作是这样的:

Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)

1)block集(阻塞集、屏蔽集):一个进程所要屏蔽的信号,在对应要屏蔽的信号位置1

2)pending集(未决信号集):如果某个信号在进程的阻塞集中,则也在未决集中对应位置1,表示该信号不能被递达,不会被处理

3)handler(信号处理函数集):表示每个信号所对应的信号处理函数,当信号不在未决集中时,将被调用。

4)block状态字、pending状态字均64位(bit);

5)block状态字用户可以读写,pending状态字用户只能读;这是信号设计机制

那么我们该如何对信号的屏蔽字状态进行改变和读取呢?接下来我们介绍一组信号集操作函数

#include <signal.h>  
int sigemptyset(sigset_t *set); //把信号集清零;(64bit/8=8字节)
int sigfillset(sigset_t *set); //把信号集64bit全部置为1
int sigaddset(sigset_t *set, int signo); //根据signo,把信号集中的对应位置成1
int sigdelset(sigset_t *set, int signo); //根据signo,把信号集中的对应位置成0
int sigismember(const sigset_t *set, int signo); //判断signo是否在信号集中

sigprocmask    功能:读取或者更改进程的信号屏蔽字(Block)

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

  返回值:若成功则为0,若出错则为-1

  读取:如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

  更改:如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)


sigpending获取信号未决状态字(pending)信息,保存至set态,NSIG信号的最大值=64。

#include <signal.h>  
int sigpending(sigset_t *set);

sigismember函数

    用来测试参数signum 代表的信号是否已加入至参数set信号集里。如果信号集里已有该信号则返回1,否则返回0。如果有错误则返回-1。出错的情况及其错误代码见下:

EFAULT 参数set指针地址无法存取
EINVAL 参数signum 非合法的信号编号

int sigismember(const sigset_t *set,int signum);
我们注册一个SIGINT信号,打印出pending的状态,结果如下:

void handler(int sig)
{
        printf("recv a sig=%d\n",sig);
}
void printsigset(sigset_t *set)
{
        int i;
        for(i=1;i<NSIG;i++)
        {
                if(sigismember(set,i))
                        putchar('1'); //打印出未决态
                else
                        putchar('0');
        }
        printf("\n");
}
int main()
{
        sigset_t pset;
        if(signal(SIGINT,handler)==SIG_ERR)
                ERR_EXIT("signal error!");
        while(1)
        {
                sigpending(&pset);
                printsigset(&pset);
                sleep(1);
        }
        return 0;
 
}
Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)
信号没有阻塞,不会发生未决状态,直接递达。

     在接下来的例子中,我们先屏蔽SIGINT信号, 但是如果该进程接收到了SIGQUIT信号, 则将对SIGINT信号的屏蔽节解除,当然,我们需要先注册SIGINT和SIGQUIT信号。

/*开始阻塞信号的程序,产生未决状态*/
void handler(int sig)
{
        if(sig==SIGINT)
                printf("recv a sig=%d\n",sig);
        else if(sig==SIGQUIT) //解除SIGINT的屏蔽
        {
                sigset_t uset;
                sigemptyset(&uset);
                sigaddset(&uset,SIGINT);
                sigprocmask(SIG_UNBLOCK,&uset,NULL);
 
        }
//        printf("recv a sig=%d\n",sig);
}
void printsigset(sigset_t *set)
{
        int i;
        for(i=1;i<NSIG;i++)
        {
                if(sigismember(set,i))
                        putchar('1');
                else
                        putchar('0');
        }
        printf("\n");
}
int main()
{
        sigset_t pset;
        sigset_t bset;
        sigemptyset(&bset);
        sigaddset(&bset,SIGINT);
        if(signal(SIGINT,handler)==SIG_ERR)
                ERR_EXIT("signal error!");
        if(signal(SIGQUIT,handler)==SIG_ERR)
                ERR_EXIT("signal error!");
        sigprocmask(SIG_BLOCK,&bset,NULL);//屏蔽SIGINT信号
        while(1)
        {
                sigpending(&pset);
                printsigset(&pset);
                sleep(1);
        }
        return 0;
 
}
Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)
    当我们按下ctrl+c产生信号时,信号被阻塞,处于未决状态。接收到SIGQUIT信号时,解除阻塞,进入递达状态,但是只会对信号做出一次反应,即使你按了很多次ctrl+c,原因就在于,SIGINT是不可靠信号,不支持排队,只保留了一个。

    如果我们采用实时信号的话,例如SIGRTMIN,那么对信号来说是支持排队的,不会发生丢失的情况,在解除阻塞后,会对每个信号做出处理。

Sigaction

前面我们讲过了使用signal安装不可靠信号,虽然signal不如sigaction功能丰富,但是也可以安装可靠信号;

#include <signal.h>  
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

功能:

   sigaction函数用于改变进程接收到特定信号后的行为。

Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)

简而言之参数就是(信号,指针,原行为)

关于sigaction结构体

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等

struct sigaction {  
//信号处理程序 不接受额外数据(比较过时)
void (*sa_handler)(int);

//信号处理程序能接受额外数据,和sigqueue配合使用(支持信号排队,信号传送其他信息),推荐使用
void (*sa_sigaction)(int, siginfo_t *, void *);

sigset_t sa_mask; //屏蔽
int sa_flags; //表示信号的行为:SA_SIGINFO表示能接受数据
void (*sa_restorer)(void); //废弃不用了
};

      sa_handler的原型是一个参数为int,返回类型为void的函数指针。参数即为信号值,所以信号不能传递除信号值之外的任何信息;

  sa_sigaction的原型是一个带三个参数,类型分别为int,struct siginfo *,void *,返回类型为void的函数指针。第一个参数为信号值;第二个参数是一个指向struct siginfo结构的指针,此结构中包含信号携带的数据值;第三个参数没有使用。

  sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。默认当前信号本身被阻塞

  sa_flags包含了许多标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中。即使sa_sigaction指定信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。

  sa_restorer已过时,POSIX不支持它,不应再使用。

注意:回调函数sa_handler和sa_sigaction只能选一个

  因此,当你的信号需要接收附加信息的时候,你必须给sa_sigaction赋信号处理函数指针,同时还要给sa_flags赋SA_SIGINFO,

实例1: 利用 sigaction 实现了 signal 函数的功能

__sighandler_t my_signal(int sig,__sighandler_t handler)
{
        struct sigaction act;
        struct sigaction oldact;
        act.sa_handler=handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags=0;
        if(sigaction(sig,&act,&oldact)<0)
                return SIG_ERR;
        return oldact.sa_handler;
}
void handler(int sig)
{
        printf("recv a sig=%d\n",sig);
}
int main()
{
  /*      struct sigaction act;
        act.sa_handler=handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags=0;
        if(sigaction(SIGINT,&act,NULL)<0)
                ERR_EXIT("sigaction error\n");
*/
        my_signal(SIGINT,handler);
 
        while(1)
                pause();
        return 0;
 
}
sa_mask选项

 在执行handler 的时候, 如果此时进程收到了sa_mask所包含的信号, 则这些信号将不会被响应, 直到handler函数执行完毕。

 sigprocmask使其即使发生了也不能递达,但是sa_mask 仅是在处理handler是屏蔽外来的信号;两者的区别还是要好好搞一搞的。

void handler(int sig)
{
        printf("recv a sig=%d\n",sig);
        sleep(5);
}
int main()
{
        struct sigaction act;
        act.sa_handler=handler;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask,SIGQUIT);//屏蔽SIGQUIT信号
        act.sa_flags=0;
        if(sigaction(SIGINT,&act,NULL)<0)
                ERR_EXIT("sigaction error\n");
 
        while(1)
                pause();
        return 0;
 
}
Linux信号编程实践(三) 信号在内核中的表示(sigaction&sigqueue)
在响应SIGINT信号即handler处理时,SIGQUIT暂时被屏蔽,但是一旦handler函数处理完后,立即对SIGQUIT进行响应。

siginfo_t结构:

siginfo_t{  
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
}

sigqueue

#include <signal.h>  
int sigqueue(pid_t pid, int sig, const union sigval value);

功能

   sigqueue是新的发送信号系统调用,主要是针对实时信号提出的支持信号带有参数,与函数sigaction()配合使用

   和kill函数相比多了一个参数:const union sigval value(int kill(pid_t pid, int sig)),因此sigqueue()可以比kill()传递更多的信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。

参数

   参数1是指定接收信号的进程id,参数2确定即将发送的信号;

   参数3是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

  注意:要想在进程间通信的话,sa_flags要置为 SA_SIGINFO

sigval联合体

typedef union sigval{  
int sival_int;
void *sival_ptr;
} sigval_t;
接下来我们模拟一下进程间通信的实例:

先运行hello开启接收,然后使用send发送信号;通过这种方式可以达到进程见通信的目的。

Hello.c
void handler(int sig,siginfo_t *info,void *ctx)
{
 
        printf("recv a sig=%d data=%d\n",sig,info->si_value.sival_int);
         
}
int main()
{
        struct sigaction act;
        act.sa_sigaction=handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags=SA_SIGINFO;
        if(sigaction(SIGINT,&act,NULL)<0)
                ERR_EXIT("sigaction error\n");
 
        while(1)
                pause();
        return 0;
 
}

Send
int main(int argc,char *argv[])
{
        if(argc!=2)
        {
                fprintf(stderr,"Usage %s pid\n",argv[0]);
                exit(EXIT_FAILURE);
        }
        pid_t pid=atoi(argv[1]);
        union sigval v;
        v.sival_int=100;
        sigqueue(pid,SIGINT,v);
        return 0;
}