UNIX环境高级编程——信号1

来源:互联网 发布:淘宝买mycard 编辑:程序博客网 时间:2024/05/03 20:00
1、信号基本概述和signal函数
 
一、为了理解信号,先从我们最熟悉的场景说起:
1. 用户输入命令,在Shell下启动一个前台进程。
2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。

kill -l命令可以察看系统定义的信号列表:
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2。编号34以上的是实时信号,这些信号各自在什么条件下产生,默认的处理动作是什么(Term表示终止当前进程,Core表示终止当前进程并且Core Dump,Ign表示忽略该信号,Stop表示停止当前进程,Cont表示继续执行先前停止的进程),在signal(7)中都有详细说明。

[1,31]  不可靠信号,多个信号不会排队只保留一个,即信号可能丢失。

[34,64]  可靠(实时信号),支持排队信号不会丢失,可使用sigqueue发送信号,不像0~31有缺省的定义。


二、产生信号的条件主要有:

1、用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号。

2、硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。

3、再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

4、一个进程调用kill(2)函数可以发送信号给另一个进程。

5、可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。

6、raise:给自己发送信号。raise(sig)等价于kill(getpid(), sig);
7、killpg:给进程组发送信号。killpg(pgrp, sig)等价于kill(-pgrp, sig);
8、sigqueue:给进程发送信号,支持排队,可以附带信息。

9、当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。

 

三、用户程序可以调用signal(2) / sigaction(2)函数告诉内核如何处理某种信号(若未注册则按缺省处理),可选的处理动作有三种:

1. 忽略此信号( SIG_IGN) 。有两个信号不能被忽略: SIGKILL和SIGSTOP 。
2. 执行该信号的默认处理动作( SIG_DFL) 。
3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(catch)一个信号。


四、信号与中断的区别

信号与中断的相似点:
(1)采用了相同的异步通信方式;
(2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
(3)都在处理完毕后返回到原来的断点;
(4)对信号或中断都可进行屏蔽。
信号与中断的区别:
(1) 中断有优先级,而信号没有优先级,所有的信号都是平等的 ;
(2)信号处理程序是在 用户态 下运行的,而中断处理程序是在 核心态 下运行;
(3)中断响应是及时的,而信号响应通常都有较大的时间延迟。


五、signal(2) 信号注册函数

typedef void (*__sighandler_t) (int);
#define SIG_ERR ((__sighandler_t) -1)
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

函数原型:
__sighandler_t signal(int signum, __sighandler_t handler);
参数
signal是一个带signum和handler两个参数的函数,准备捕捉或屏蔽的信号由参数signum给出,接收到指定信号时将要调用的函数由handler给出,handler这个函数必须有 一个int类型的参数 (即 接收到的信号代码 ),它本身的类型是void
handler也可以是两个特殊值: SIG_IGN 屏蔽该信号 ; SIG_DFL 恢复默认行为。

返回值:返回 先前 的信号处理函数指针(注意不是 handler) ,如果有 错误 则返回 SIG_ERR (-1)。
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

int main(int argc,char*argv[])
{
    __sighandler_t oldhandler;
    oldhandler = signal(SIGINT, handler); // 返回值是先前的信号处理程序函数指针
//    if (oldhandler == SIG_ERR)
//        ERR_EXIT("signal error");
      if (oldhandler == SIG_DFL)  //注意oldhandler应该等于SIG_DFL
          printf("huang\n");

    while (getchar() != '\n') ;

    /* signal(SIGINT, SIG_DFL) */
//    if (signal(SIGINT, oldhandler) == SIG_ERR)
//        ERR_EXIT("signal error");
      if (signal(SIGINT, oldhandler)== handler)   //注意:signal(SIGINT, oldhandler)应该为handler
          printf("cheng\n");

    for (; ;) ;

    return 0;
}

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
}

测试输出如下:
huangcheng@ubuntu:~$ ./a.out
huang
^Crecv a sig=2
^Crecv a sig=2

cheng
^C

 程序执行开始注册了SIGINT信号的处理函数,故我们按下ctrl+c 并不会像往常一样终止程序,只是打印了recv a  sig = 2。接着按下回车,重新注册了SIGINT的默认处理,此时再ctrl+c 程序就被终止了。

将程序中的  for (; ;)换成如下的表述:

for (; ;)
{
    pause(); //使进程挂起直到一个信号被捕获(信号处理函数完成后返回)
    //且调用schedule()使系统调度其他程序运行,
    //这样比完全的死循环的好处是让出cpu
    printf("pause return\n");
}

 调用pause函数:将进程置为可中断睡眠状态。然后它调用schedule(),使linux进程调度器找到另一个进程来运行。pause使调用者进程挂起,直到一个信号被捕获处理后函数才返回。调用pause 的好处是在等待信号的时候让出cpu,让系统调度其他进程运行,而不是完全的死循环,当然这样ctrl+c 就是始终终止不了程序,我们可以使用 ctrl+\ 产生SIGQUIT信号终止程序。

事实上根据man手册,signal 函数可移植性并不是很好,最好只是用在SIG_DFL, SIG_IGN 上,注册信号处理函数用sigaction 比较好。
注意:当一个进程调用fork时,其子进程继承父进程的信号处理方式。

 

 

2、信号(API)

一、信号在内核中的表示

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


每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,
1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


二、信号集处理函数

sigset_t类型(64bit)对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset (sigset_t *set); 若成功则为 0,若出错则为-1
int sigfillset(sigset_t *set); 若成功则为 0,若出错则为-1
int sigaddset(sigset_t *set, int signo); 若成功则为0,若出错则为-1
int sigdelset(sigset_t *set, int signo); 若成功则为0,若出错则为-1
int sigismember(const sigset_t*set,int signo); 若真则为1,若假则为0

各个函数的含义为:
sigemptyset : 用来初始化一个信号集,使其不包括任何信号。
sigfillset : 用来初始化由set指向的信号集,使其保留所有信号。
sigaddset : 用来向 set 指定的信号集中添加由 signo 指定的信号。
sigdelset : 用来从 set 指定的信号集中删除由 signo 指定的信号。
sigismember : 用来测试信号 signo 是否包括在 set 指定的信号集中。
注意:在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

 

三、sigprocmask 和 sigpending 函数

1、调用函数sigprocmask可以读取或更改进程的信号屏蔽字

一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集

#include <signal.h>
int sigprocmask(int how,const sigset_t*set, sigset_t *oset); 若成功则为0,若出错则为- 1

若oset是非空指针, 进程的当前信号屏蔽字通过oset返回, 注意:oset返回的不是对set参数操作之后的,是对set参数操作之前的 。 若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。how的取值及其意义:

SIG_BLOCK   :该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。 set包含了我们希望阻塞的附加信号 。
SIG_UNBLOCK :该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的差集(在当前信号屏蔽字中但是不在set所指向的信号集中)。
              set包含了我们希望解除阻塞的信号
SIG_SETMASK :该进程新的信号屏蔽字将被set指向的信号集的值代替 ,如果set是个空指针,则不改变该进程的信号屏蔽字, how的值也无意义。


2、sigpending 读取当前进程的未决信号集,通过set参数返回。调用成功则返回0,出错则返回-1

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

示例程序:  

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
 
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
 
void handler(int 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 flag = 0;
 
int main(int argc,char*argv[])
{
    if (signal(SIGINT, handler)== SIG_ERR)
        ERR_EXIT("signal error");

    if (signal(SIGQUIT, handler)== SIG_ERR)
        ERR_EXIT("signal error");
 
    sigset_t pset; // 64bit
    sigset_t bset;

    sigemptyset(&bset);
    sigaddset(&bset, SIGINT);
    sigprocmask(SIG_BLOCK, &bset, NULL);
 
    for (; ;)
    {
        sigpending(&pset);
        printsigset(&pset);
        sleep(1);
        if (flag == 1)
            sigprocmask(SIG_UNBLOCK, &bset, NULL);
    }
 
    return 0;
}
 
void handler(int sig)
{
    if (sig == SIGINT)
        printf("recv a sig=%d\n", sig);
    else if (sig == SIGQUIT)
    {
        printf("rev a sig=%d\n", sig);
        sigset_t uset;
        sigemptyset(&uset);
        sigaddset(&uset, SIGINT);
        sigprocmask(SIG_UNBLOCK, &uset, NULL);
        flag = 1;
    }
 
}
如果将程序中的37,57,58,75关于flag变量的语句注释掉,则输出如下:

huangcheng@ubuntu:~$./sigprocmask
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
...................................................................................
^C0100000000000000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000000
...................................................................................................................................................
^\rev a sig=3
recv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
............................................................................................................................................
^C0100000000000000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000000
...........................................................................................................................................
 
在程序的一开始将SIGINT信号添加进阻塞信号集(即信号屏蔽字),死循环中一直在打印进程的信号未决集,当我们按下ctrl+c,因为信号被阻塞,故处于未决状态,所以输出的第二位为1(SIGINT是2号信号),接着当我们按下ctrl+\,即发送SIGQUIT信号,我们在handler中解除了对SIGINT的阻塞,故2号信号被递达,打印两行recv语句,此时信号未决集又变成全0。比较让人疑惑的是我们貌似已经解除了对SIGINT的屏蔽,但当我们再次ctrl+c 时,信号还是处于未决状态。后来我写了个测试程序,发现解除阻塞时只是将未决标志pending位清0,而block位一直为1,但还是觉得很不解,难道一个进程运行期间只要阻塞了一个信号,只能每次靠清除pending位让其递达,即治标不治本?后来觉得会不会是因为在handler里进行解除才会这样呢?于是设置了一个标志位flag,即把前面说的4行代码补上,则前面的输出是一样的,但在主函数中再次解除阻塞后,按下ctrl+c,让人惊喜的是2号信号顺利递达,如下:
huangcheng@ubuntu:~$./sigprocmask
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
...................................................................................
^C0100000000000000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000000
...................................................................................................................................................
^\rev a sig=3
recv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
............................................................................................................................................
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
...........................................................................................................................................

我查遍了sigprocmask 的 man手册,也没发现说明这一点,但实际测试是这样的,即如果在信号处理函数中对某个信号进行解除阻塞时,则只是将pending位清0,让此信号递达一次,但不会将block位清0,即再次产生此信号时还是会被阻塞,处于未决状态。

经过下面的验证发现:在信号处理函数中对某个信号进行解除阻塞时,则只是将pending位清0,让此信号递达一次,但不会将block位清0,即再次产生此信号时还是会被阻塞,处于未决状态,而在main函数中对某个信号进行解除阻塞时,则会将block位清0,即再次产生此信号时不会被阻塞。产生如此区别的原因:信号处理函数和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
 
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
 
void handler(int 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 flag = 0;
 
int main(int argc,char*argv[])
{
    if (signal(SIGINT, handler)== SIG_ERR)
        ERR_EXIT("signal error");
    if (signal(SIGQUIT, handler)== SIG_ERR)
        ERR_EXIT("signal error");
 
    sigset_t pset; // 64bit
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, SIGINT);
    sigprocmask(SIG_BLOCK, &bset, NULL);
 
    for (; ;)
    {
        sigpending(&pset);
        printsigset(&pset);
        sleep(1);
        if (flag == 1)
        {
            sigprocmask(SIG_UNBLOCK, &bset, NULL);
            flag = 0;
        }
    }
 
    return 0;
}
 
void handler(int sig)
{
    if (sig == SIGINT)
        printf("recv a sig=%d\n", sig);
    else if (sig == SIGQUIT)
    {
        printf("rev a sig=%d\n", sig);
//      sigset_t uset;
//      sigemptyset(&uset);
//      sigaddset(&uset, SIGINT);
//      sigprocmask(SIG_UNBLOCK, &uset, NULL);
        flag = 1;
    }
 
}
运行结果:

huangcheng@ubuntu:~$./sigprocmask
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
...................................................................................
^C0100000000000000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000000
...................................................................................................................................................
^\rev a sig=3
recv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
............................................................................................................................................
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
^Crecv a sig=2
0000000000000000000000000000000000000000000000000000000000000000
...........................................................................................................................................

现在使用ctrl+c , ctrl+\ 都终止不了程序了,可以另开个终端kill -9 pid 杀死进程。


示例程序:

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
void my_err(constchar*err_string, int line)
{
     fprintf(stderr, "line:%d", line);
     perror(err_string);
     exit(1);
}
void hander_sigint(int signo)
{
     printf("recv SIGINT\n");
}
int main()
{
     sigset_t newmask, oldmask, pendmask;    //定义信号集

     /*安装信号处理函数*/
     if (signal(SIGINT, hander_sigint)== SIG_ERR) {
         my_err("signal", __LINE__);  /*__LINE__ 表示当前__LINE__ 所在的行数*/
     }

     printf("进入睡眠10秒\n");
     sleep(10);
     printf("睡眠醒来\n");

     /*初始化信号集 newmask 并将 SIGINT 添加进去*/
     sigemptyset(&newmask);
     sigaddset(&newmask, SIGINT);
     /*屏蔽信号 SIGINT*/
     if (sigprocmask(SIG_BLOCK,&newmask,&oldmask)< 0) {
         my_err("sigprocmask", __LINE__);
     } else {
         printf("SIGINT blocked\n");
     }

     printf("huangcheng\n");
     sleep(5);

     /*获取未决信号队列*/
     if (sigpending(&pendmask)<0) {
         my_err("sigpending", __LINE__);
     }

     /*检查未决信号队列里是否有 SIGINT*/
     switch (sigismember(&pendmask, SIGINT)) {
         case 0:
         printf("SIGINT is not in pending queue\n");
         case 1:
         printf("SIGINT is in pending queue\n");
         break;
         case -1:
         my_err("sigismember", __LINE__);
         break;
         default:
         break;
     }

     /*解除对 SIGINT 的屏蔽*/
     if (sigprocmask(SIG_SETMASK,&oldmask, NULL)<0) {
         my_err("sigprocmask", __LINE__);
     } else {
         printf("SIGINT unblocked\n");
     }
     while(1)
     ;
    return 0;
}

运行结果:

huangcheng@ubuntu:~$ ./a.out
进入睡眠10秒
^Crecv SIGINT
睡眠醒来
SIGINT blocked
huangcheng
^C^C^C^CSIGINT is in pending queue
 
recv SIGINT
SIGINT unblocked
^Crecv SIGINT
^Crecv SIGINT
^\退出
在输出结果中,^C 表示按下了一次 Ctrl + C 的组合键。由输出结果可以分析得:


1、在程序进入睡眠时(10s),只要接收到信号,它就会马上醒过来,然后进行接收到信号处理,如上面的输出:recv SIGINT。注意 到,“睡眠醒来“ 醒来这句话是在 “recv SIGINT” 之后的,这是因为程序醒来后,第一件事是要去处理信号,而并不着急要输出睡眠 函数的下面的语句。

2、接着,我们阻塞了 SIGINT 信号,然后又进入 5 秒的睡眠。这时,连续多次输入 ctrl+c 后程序并不会就立马 响应这些信号。所以,当用  sigismember() 函数来测试 SIGINT 是否在未决信号队列中,由输出  SIGINT blocked 可见,SIGINT 信 号确实已经被阻塞。

3、再接着,我们解除了 SIGINT 信号的屏蔽,用的是  sigprocmask ( SIG_SETMASK ,   & oldmask ,   NULL ),这里  oldmask 是原来屏蔽的信号集(相当于在设置新的信号集前我们对之前的信号集做了一个备份)。

4、当 SIGINT 信号的屏蔽被解除后, 程序马上去处理这个信号。再注意到,SIGINT unblocked 提示后于 recv SIGINT 输出,也是因为一旦解除了被屏蔽的信号且未决 信号队列中有这个信号,那么程序会立即处理信号函数,而不着急输出提示。为什么只有一次 recv SIGINT  输出呢?这是因为  SIGINT 信号是不可靠信号,当有多个这样的信号时,信号处理函数往往只会被调用一次。

 

四、内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数, sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。



五、sigaction函数

#include <signal.h>
int sigaction(int signo,conststruct sigaction *act,struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作 。调用成功则返回0,出错则返回-1
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:


struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t*,void*);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

需要注意的是sa_restorer 参数已经废弃不用,sa_handler主要用于不可靠信号(实时信号当然也可以,只是不能带信息),sa_sigaction用于实时信号(同时也支持非实时信号)可以带信息(siginfo_t),两者不能同时出现sa_flags有几个选项,比较重要的有两个:SA_NODEFER 和 SA_SIGINFO。

当SA_NODEFER设置时在信号处理函数执行期间不会屏蔽当前信号;

当SA_SIGINFO设置时与sa_sigaction 搭配出现,sa_sigaction函数的第一个参数与sa_handler一样表示当前信号的编号,第二个参数是一个siginfo_t 结构体,第三个参数一般不用。当使用sa_handler时sa_flags设置为0即可。

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 kernel 2.6.32) */

}

需要注意的是并不是所有成员都在所有信号中存在定义,有些成员是共用体,读取的时候需要读取对某个信号来说恰当的有定义的部分。


下面用sigaction函数举个小例子:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
 
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
 
void handler(int sig);
 
 
int main(int argc,char*argv[])
{
    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");
 
    for (; ;)
        pause();
 
    return 0;
 
}
 
void handler(int sig)
{
    printf("rev sig=%d\n", sig);
}
 

huangcheng@ubuntu:~$ ./a.out
^Crev sig=2
^Crev sig=2
^Crev sig=2
^Crev sig=2
...........................

即按下ctrl+c 会一直产生信号而被处理打印recv语句。

其实我们在前面文章说过的signal 函数是调用sigaction 实现的,而sigaction函数底层是调用 do_sigaction() 函数实现的。可以自己实现一个my_signal 函数,如下:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
 
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
 
void handler(int sig);
/* 系统调用signal()实际上调用了sigaction() */
__sighandler_t my_signal(int sig, __sighandler_t handler);
 
int main(int argc,char*argv[])
{
    my_signal(SIGINT, handler);
 
    for (; ;)
        pause();
 
    return 0;
 
}
 
__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("rev sig=%d\n", sig);
}

输出测试的一样的,需要注意的是 signal函数成功返回先前的handler,失败返回SIG_ERR。而sigaction 是通过oact 参数返回先前的handler,成功返回0,失败返回-1。


下面再举个小例子说明sa_mask 的作用:

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
 
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
 
void handler(int sig);
 
 
int main(int argc,char*argv[])
{
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT); // 在信号处理函数执行期间屏蔽SIGQUIT信号,完毕后会抵达
    /* 注意sigprocmask中屏蔽的信号是一直不能抵达的,除非解除了阻塞*/
    act.sa_flags = 0;
 
    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error");
 
    for (; ;)
        pause();
 
    return 0;
 
}
 
void handler(int sig)
{
    printf("rev sig=%d\n", sig);
    sleep(5);
}

先按下ctrl+c ,然后连按ctrl+c 三下,接着马上连按ctrl+\ 两下,程序是不会马上终止的,即等到handler处理完毕下一个SIGINT信号才会抵达。在handler处理的过程中SIGINT(自己)和SIGQUIT信号会被屏蔽,当handler处理完成后,SIGINT和SIGQUIT解除屏蔽。然后再处理SIGINT一次(因为是不可靠信号,所以三次触发,只处理一次),等SIGINT处理完成后,才再处理SIGQUIT信号,即程序退出。

huangcheng@ubuntu:~$ ./a.out
^Crev sig=2
^C^C^C^\^\^\rev sig=2
退出
 
先处理SIGINT打印rev sig=2,等待5S后。接着再处理SIGINT打印rev sig=2,等待5S后,最后处理Quit (core dumped),即在信号处理函数执行期间sa_mask集合中的信号(SIGQUIT)和自身(SIGINT)信号被阻塞直到运行完毕。
原创粉丝点击