第十章:信号

[亡魂溺海] 提交于 2020-02-18 07:13:54

 

一、信号的概念

使用信号进行进程间通信(IPC)是UNIX的一种传统机制,Linux也支持这种机制。

 

每一个信号都有一个名字,这些名字都以SIG开头。如SIGINT表示终端中断(Ctrl + C产生),SIGQUIT表示终端退出,SIGIO表示异步I/O。

 

我们可以使用kill -l命令查看所有信号

 

信号属于异步事件,它的发生对于进程是随机的。进行必须要告诉内核当信号发生时怎么处理。

对于信号的处理,我们可以有以下几种方式:

1. 忽略此信号,但是SIGKILL和SIGSTOP不能忽略,因为这两个信号向内核提供使进程终止的方法。

2. 捕捉并处理信号。

3. 执行系统默认动作,如Ctrl + C就是终端中断,程序中不做任何信号处理。

 

 

二、signal()函数

signal()函数的使用方法:

1. 包含头文件:#include <signal.h>。

2. 定义信号处理函数:typedef void (*sighandler_t)(int signum),其中的signum是信号名。

3. 注册信号:signal(int signum, sighandler_t handler);。

 

示例如下:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <signal.h>
 4 
 5 static void sig_handler(int signo)
 6 {
 7     printf("RECV SIGNO: %d\n", signo);
 8 }
 9 
10 int main()
11 {    
12     signal(SIGINT, sig_handler);
13     
14     while (1) {
15         sleep(1000);
16     }
17 
18     return 0;
19 }

执行此代码,当我们在命令行中按下Ctrl + C,程序不会退出,而是会打印:^CRECV SIGNO: 2

按下Ctrl + Z可退出程序。

 

若要忽略某信号,可使用SIG_IGN:

signal(SIGINT, SIG_IGN);

若要使用默认处理,可使用SIG_DFL:

signal(SIGINT, SIG_DFL);

 

前文提到signal()用于进程间通信,我们使用fork()函数测试一下,示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <signal.h>
 4 #include <unistd.h>
 5 
 6 void sig_handler(int signo)
 7 {
 8     printf("捕获了信号%d\n", signo);
 9 }
10 
11 int main()
12 {
13 //    signal(SIGINT, sig_handler);
14 
15     pid_t pid = fork();
16     if (!pid) {
17         printf("子进程%d开始运行\n", getpid());
18         signal(SIGINT, sig_handler);
19         while(1);
20         printf("子进程%d结束\n", getpid());
21     }
22     
23     sleep(1);            // 让子进程signal()注册
24     kill(pid, SIGINT);    // 父进程向子进程发送SIGINT信号
25     printf("父进程结束");
26 
27     return 0;
28 }

执行此代码,父进程在结束前会使用SIGINT杀掉子进程。

 

 

三、闹钟和定时器

Linux定时器就是在n秒以后,每个n秒产生一个信号。在信号定义中使用的是SIGALRM。

 

闹钟是在定义SIGALRM信号和信号处理函数后,设定n秒后产生SIGALRM信号发给本进程。函数声明和示例如下:

/* 声明 */
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

/* 示例 */
signal(SIGALRM, sig_handler);
alarm(3);    // 3秒后产生一个SIGLRM信号,发给本进程

 

对于定时器而言,函数setitimer()可以设置定时器,getitimer()可以获取定时器。函数声明如下:

#include <sys/time.h>

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
              struct itimerval *old_value);

which参数有以下三种:

ITIMER_REAL:以系统真实的时间来计算,发送的信号是SIGALRM。一般使用此选项。

ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,发送的信号是SIGVTALRM。

ITIMER_PROF:以该进程在用户态下和内核态下所费的时间来计算,发送的信号是SIGPROF。

 

声明中所使用struct itmerval结构体定义如下:

struct itimerval {
    struct timeval it_interval; /* 定时器间隔时间 */
    struct timeval it_value;    /* 定时器开始时间 */
};

struct timeval {
    time_t      tv_sec;         /* 秒 */
    suseconds_t tv_usec;        /* 微秒 */
};

 

示例代码如下:

 1 #include <stdio.h>
 2 #include <sys/time.h>
 3 #include <signal.h>
 4 
 5 static int count;
 6 
 7 void sig_handler(int signo)
 8 {
 9     printf("itmer count = %d\n", count++);
10 }
11 
12 int main()
13 {
14     signal(SIGALRM, sig_handler);
15     
16     struct itimerval it;
17     it.it_interval.tv_sec = 1;        // 间隔时间的秒数
18     it.it_interval.tv_usec = 1000;    // 微秒数
19     it.it_value.tv_sec = 3;            // 3秒后开始执行
20     it.it_value.tv_usec = 0;
21     
22     setitimer(ITIMER_REAL, &it, 0);
23     
24     while(1);
25 
26     return 0;
27 }

 

 

四、可靠信号和不可靠信号

信号分为可靠信号和不可靠信号。早期的Unix系统中定义了32个信号,但是都是不可靠信号。

不可靠在这里指的是,一个信号发生了,进程却可能不知道,也就不会进行处理。

随着时间的发展,Unix系统力图实现可靠信号,因此在不可靠信号的基础上添加了可靠信号。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。其中sigaction()定义如下:

#include<signal.h>

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

 

struct sigaction定义如下:

struct sigaction {
    void (*sa_handler)(int);    /* 信号处理函数,sa_sigaction和它任选其一,如果sa_flags设置有SA_SIGINFO则必须使用sa_sigaction */
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;    /* 信号集,后面会讲到 */
    int sa_flags;
    void (*sa_restorer)(void);
};

 

在Linux系统中,信号1号到31号是不可靠信号,不支持排队,有可能丢失。34号到64号是可靠信号,支持排队,不可能丢失。示例代码如下:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <signal.h>
 4 
 5 static void sig_handler(int signo)
 6 {
 7     printf("RECV SIGNO: %d\n", signo);
 8     sleep(3);
 9 }
10 
11 int main()
12 {
13 #if 1
14     signal(SIGINT, sig_handler);
15 #else
16     signal(SIGRTMIN, sig_handler);
17 #endif
18 
19     while (getchar() != 'q')
20         ;    /* NULL */
21 
22     return 0;
23 }

SIGINT是不可靠信号,因此当我们执行此代码后在信号处理函数的sleep(3)时间内按下Ctrl + C,会导致多次按键,只有一次响应。

 

在测试可靠性信号代码中,我以SIGRTMIN为例。读者可以将上面代码13行的#if 1改为#if 0。

重新编译执行后,打开另外一个命令窗口,使用ps -ef查看当前进程号,在我的电脑上PID为84337。

 

测试使用kill命令,kill使用方式如下:

kill -信号值 进程PID

如:

kill -34 84337

执行kill命令时可发现执行多少次命令,输出多少条信息,这就是可靠信号的支持排队特性。

 

 

五、信号集和信号屏蔽

信号集是一个能表示多个信号的数据类型,使用结构体sigset_t来表示。其本质是一个超大的整数,每个二进制位代表一个信号。比如:信号2用倒数第二位代表,倒数第二位是1,代表有信号2,是0代表没有。

既然是一个集合,就需要对集合进行添加/删除等操作,系列函数定义如下:

#include <signal.h>

int sigemptyset(sigset_t *set);        /* 清空信号集 */
int sigfillset(sigset_t *set);        /* 将所有信号加入信号集 */
int sigaddset(sigset_t *set, int signum);    /* 将signum加入信号集 */
int sigdelset(sigset_t *set, int signum);    /* 将signum移出信号集 */
int sigismember(const sigset_t *set, int signum);    /* 判断signum是否存在数据集中 */

除sigismember()存在返回1之外,其它函数成功均返回0。

 

信号屏蔽可以让信号被处理的时间延后。信号屏蔽主要用于关键代码的执行,关键代码执行完毕后一定要解除信号的屏蔽,让信号得到处理。

比如银行的存储和支出操作是使用信号实现的。在某人开始存储时,应该屏蔽支出信号;在存储结束后再执行等待的支出信号函数。

其函数定义如下:

#include <signal.h>

int sigprocmask(int how, const sigset_t *newset, sigset_t *oldset);

其中参数how的取值如下:

1. SIG_BLOCK:该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字。

2. SIG_UNBLOCK:将参数newset所指向的信号集中的信号从当前的信号掩码中移除。

3. SIG_SETMASK:设置当前信号掩码为参数newset所指向的信号集中所包含的信号。

 

需要注意的是,sigprocmask()函数只为单线程的进程定义的,在多线程中要使用pthread_sigmask变量,在使用之前需要声明和初始化。

 

在屏蔽过后,我们可以使用sigpending()函数查看哪个信号来过需要处理,其函数声明如下:

#include <signal.h>

int sigpending(sigset_t *set);

 

示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <signal.h>
 4 #include <unistd.h>
 5 
 6 void sig_handler(int signo)
 7 {
 8     printf("捕获了信号%d\n", signo);
 9 }
10 
11 int main()
12 {
13     signal(SIGINT, sig_handler);    // 不可靠信号
14     signal(SIGRTMIN, sig_handler);    // 34,可靠信号
15     
16     printf("%d执行普通代码,信号不屏蔽\n", getpid());
17     sleep(1);
18     
19     printf("执行关键代码信号屏蔽\n");
20     sigset_t set, old;
21     sigemptyset(&set);
22     sigaddset(&set, SIGINT);
23     sigaddset(&set, SIGRTMIN);
24     sigprocmask(SIG_SETMASK, &set, &old);    // 屏蔽SIGINT和SIGRTMIN
25     sleep(10);
26     
27     sigset_t pend;
28     sigpending(&pend);
29     if (sigismember(&pend, SIGINT) == 1)
30         printf("SIGRTMIN来过\n");
31     if (sigismember(&pend, SIGRTMIN) == 1)
32         printf("SIGRTMIN来过\n");
33     
34     printf("关键代码执行完毕,解除屏蔽\n");
35     sleep(1);
36     sigprocmask(SIG_SETMASK, &old, NULL);
37     printf("程序结束\n");
38 
39     return 0;
40 }

测试方式与上一节可靠信号和不可靠信号示例程序测试方式相同,结果如下图:

 

 

下一章  第十一章:线程

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!