学科分类
目录
Linux编程

信号捕获

信号的产生是一个异步事件,进程不知道信号何时会递达,也不会等待信号到来,事实上,信号的接收并非由进程,而是由内核来完成的。当进程A向进程B发送一个信号时,内核极有可能正在运行其它进程,若非紧急信号,进程并不会立刻切换进程进行信号处理,而是将信号的相关信息写入B进程的PCB,在恰当的时机(大多会在内核态切换会用户态之前),才处理信号。

前面讲解了信号的默认处理动作,除此之外,进程也可以为信号设置自定义动作。若进程捕获某个信号后,想使其执行其它的函数处理,则需为该信号注册信号处理函数。进程的信号是在内核态处理的,内核为每个进程准备了一个信号向量表,其中记录了每个信号所对应的处理机制,若用户为信号自定义了处理方式,内核会使信号向量表中的指针指向新的信号处理函数。

Linux系统中为用户提供了两个捕获信号——signal()和sigaction(),用于自定义信号处理方法。

1、 signal()函数

signal()函数也能实现信号屏蔽,但其主要功能仍为捕获信号,修改信号向量表中该信号的信号处理函数指针。signal()函数存在于函数库signal.h中,其函数定义如下:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

其中“typedef void (*sighandler_t)(int);”是signal()函数的返回值以及传入参数handler的类型定义,表示将返回值为空、包含一个int型参数的函数定义为一个类型名为sighandler的指针。signal()函数的参数只有两个,第一个参数signum表示信号编号,第二个参数一般表示自定义信号处理函数的函数指针,除此之外,还有两种取值:SIG_IGN和SIG_DFL。当handler为SIG_IGN时,执行signal()函数后,进程会忽略信号signum;当handler为SIG_DEL时,进程会恢复系统对信号的默认处理。若函数调用成功,返回先前信号处理函数的指针,否则返回SIG_ERR。下面通过案例来演示signal()函数的用法。

案例5:为2号信号SIGINT设置自定义信号处理函数,并在信号处理函数中将函数恢复为默认值。

 1    #include <stdio.h>
 2    #include <stdlib.h>
 3    #include <unistd.h>
 4    #include <string.h>
 5    #include <signal.h>
 6    void sig_int(int signo)                //自定义信号处理函数
 7    {
 8        printf(".........catch you,SIGINT\n");
 9        signal(SIGINT,SIG_DFL);            //信号处理函数执行
 10    }
 11    int main()
 12    {
 13        signal(SIGINT,sig_int);            //捕获信号SIGINT,修改信号处理函数
 14        while(1);                        //等待信号递达
 15        return 0;
 16    }

编译案例,执行程序,进程会等待信号递达;使用组合按键Ctrl+C或kill命令发送信号到当前进程,终端会打印信号处理函数包含的printf()中的信息;因为第6~10行代码中将SIGINT的信号处理函数恢复了默认值,因此再次发送信号SIGINT,程序将终止运行。程序执行结果如下所示:

^C.........catch you,SIGINT
^C

分析程序,结合打印信息可知案例实现成功。

2、 sigaction()函数

sigaction()函数存在于函数库signal.h中,与signal()函数相比,它最大的优点在于支持信息传递。sigaction()函数的声明如下:

int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

当sigaction()函数调用成功时返回0,否则返回-1。sigaction()函数有3个参数,第一个参数signum指信号编号;第二个参数为传入参数,其中包含自定义信息处理函数和一些携带信息;第三个参数为传出参数,包含旧的信息处理函数等信息。第二个参数与第三个参数是自定义结构体类型的数据,其类型定义如下:

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

sigaction结构体的第一个成员与signal()函数的返回值类型相同,都返回类型为void、包含一个整型参数的函数指针。除此之外,比较重要的是参数sa_mask和sa_flags,sa_mask是一个位图,该位图可指定捕捉函数执行期间屏蔽的信号;sa_flags则用于设置是否使用默认值,默认情况下,该函数会屏蔽自己发送的信号,避免重新进入函数。下面通过案例来演示sigaction()函数的用法。

案例6:使用sigaction()函数修改2号信号SIGINT的默认动作。

 1    #include <stdio.h>
 2    #include <stdlib.h>
 3    #include <unistd.h>
 4    #include <string.h>
 5    #include <signal.h>
 6    void sig_int(int signo)
 7    {
 8        printf("...........catch you,SIGINT,signo=%d\n",signo);
 9        sleep(5);                            //模拟信号处理函数执行时间
 10    }
 11        
 12    int main()
 13    {
 14        struct sigaction act,oldact;
 15        act.sa_handler=sig_int;                //修改信号处理函数指针
 16        sigemptyset(&act.sa_mask);              //初始化位图,表示不屏蔽任何信号
 17        sigaddset(&act.sa_mask,SIGINT);        //更改信号SIGINT的信号处理函数
 18        act.sa_flags=0;                         //设置flags,屏蔽自身所发信号
 19        sigaction(SIGINT,&act,&oldact);
 20        while(1);
 21        return 0;
 22    }

编译案例,执行程序,进程会等待信号递达,当使用组合按键或kill发送信号SIGINT到进程后,进程调用自定义信号处理函数sig_int(),打印信息。终端打印的信息如下所示:

^C...........catch you,SIGINT,signo=2

案例中第9行代码调用了sleep()函数,延长了信号处理函数的执行时间,函数执行即表示信号正在被处理,此时再次向进程发送信号,处理函数没有被中断,仍继续执行。假设在信号处理函数第一次执行期间(约5秒内),又向进程发送了6次信号,终端打印的信息如下所示:

^C.........catch you,SIGINT,signo=2
^C^C^C^C^C^C.........catch you,SIGINT,signo=2

观察打印结果,进程共接收到SIGINT信号7次,但只调用信号处理函数2次,由此可知,信号处理函数执行期间,信号被屏蔽。

分析程序,结合打印信息可知案例实现成功。

多学一招:sleep()函数自实现

sleep()函数是一个系统调用,在程序中设置该函数,可以使进程在某一段时间内进入睡眠状态。sleep()函数存在于函数库unistd.h中,其函数声明如下:

unsigned int sleep(unsigned int seconds);

sleep()函数的参数与返回值都为一个正整数,参数用于指定进程沉睡的时长,返回值用于判断函数的调用情况:若请求超时则返回0;若被信号处理程序中断,则返回剩余秒数。

将自实现的sleep()函数命名为mysleep(),那么mysleep()的函数除了函数名外,其它结构应与sleep()函数相同;考虑到函数的功能,这里使用alarm()、pasue()来实现mysleep():alarm()用于计时;pasue()用于挂起进程。

案例7:mysleep()函数自实现。

 1    #include <stdio.h>
 2    #include <signal.h>
 3    #include <stdlib.h>
 4    #include <unistd.h>
 5    void sig_alrm(int signo)
 6    {
 7        //do something...   
 8    }
 9    
 10    unsigned int mysleep(unsigned int seconds)
 11    {
 12        struct sigaction newact,oldact;
 13        unsigned int unslept;
 14        newact.sa_handler=sig_alrm;
 15        sigemptyset(&newact.sa_mask);
 16        newact.sa_flags=0;
 17        sigaction(SIGALRM,&newact,&oldact);        //屏蔽信号SIGALRM
 18        alarm(seconds);                            //倒计时
 19        sigaction(SIGALRM,&oldact,NULL);        //解除信号屏蔽
 20        pause();                                    //挂起等待信号
 21        return alarm(0);                            //返回
 22    }
 23    int main()
 24    {
 25        while(1){
 26            mysleep(2);
 27            printf("two seconds passed.\n");
 28        }
 29        return 0;
 30    }

这里实现的mysleep()函数中使用计时器alarm()函数作为计时工具,进入睡眠状态的进程不应有其它操作,因此使用pasue()函数将程序挂起;另外为了保证进程在进入沉睡状态后不被由其它进程发送的SIGALRM信号干扰,计时器启动之前应先屏蔽SIGALRM信号;在计时器计时结束后,SIGALRM信号将进程唤醒,此时进程应能接收SIGALRM信号,因此在pasue()之前调用sigaction()函数,解除屏蔽;最后返回alarm(0),因为alarm(0)默认返回0或上一个计时器的剩余秒数,因此mysleep()函数直接返回alarm(0)的返回值即可,此外,alarm(0)也是取消计时的一个安全方法。

编译此段代码,执行程序,程序的执行结果如下所示:

two seconds passed.
two seconds passed.
^C

根据程序的执行结果进行可知,自实现的mysleep()函数实现了sleep()函数的功能,但其实这个函数仍是存在问题的。这就是我们接下来要讲解的,程序执行的时序问题——时序竞态。

点击此处
隐藏目录