一、信号的概念
使用信号进行进程间通信(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 }
测试方式与上一节可靠信号和不可靠信号示例程序测试方式相同,结果如下图:

下一章 第十一章:线程
来源:https://www.cnblogs.com/Lioker/p/10856752.html