一、信号的概念
使用信号进行进程间通信(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