什么是信号?
信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
“中断”在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断”了,我去签收快递( 处理中断> ),处理完成后,再继续玩我的游戏。这里我们学习的“信号”就是属于这么一种“中断”。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。
信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。
一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:
注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。
信号的常用操作
发送信号
所需头文件:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signum);
功能:
给指定进程发送信号。
注意:使用 kill() 函数发送信号,接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者是超级用户。
参数:
pid: 取值有 4 种情况:
pid > 0: 将信号传送给进程 ID 为pid的进程。
pid = 0: 将信号传送给当前进程所在进程组中的所有进程。
pid = -1: 将信号传送给系统内所有的进程。
pid < -1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
signum: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l (“l” 为字母)进行相应查看。
返回值:
成功:0
失败:-1
下面为测试代码,本来父子进程各自每隔一秒打印一句话,3 秒后,父进程通过 kill() 函数给子进程发送一个中断信号 SIGINT( 2 号信号),最终,子进程结束,剩下父进程在打印信息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char *argv[])
{
pid_t pid;
int i = 0;
pid = fork(); // 创建进程
if( pid < 0 ){ // 出错
perror("fork");
}
if(pid == 0){ // 子进程
while(1){
printf("I am son\n");
sleep(1);
}
}else if(pid > 0){ // 父进程
while(1){
printf("I am father\n");
sleep(1);
i++;
if(3 == i){// 3秒后
kill(pid, SIGINT); // 给子进程 pid ,发送中断信号 SIGINT
// kill(pid, 2); // 等级于kill(pid, SIGINT);
}
}
}
return 0;
}
等待信号
所需头文件:
#include <unistd.h>
int pause(void);
功能:
等待信号的到来(此函数会阻塞)。将调用进程挂起直至捕捉到信号为止,此函数通常用于判断信号是否已到。
返回值:
直到捕获到信号才返回 -1,且 errno 被设置成 EINTR。
处理信号
一个进程收到一个信号的时候,可以用如下方法进行处理:
1)执行系统默认动作 对大多数信号来说,系统默认动作是用来终止该进程。 2)忽略此信号
3)执行自定义信号处理函数
用用户定义的信号处理函数处理该信号。
注意:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。
产生一个信号,我们可以让其执行自定义信号处理函数。假如有函数 A, B, C,我们如何确定信号产生后只调用函数 A,而不是函数 B 或 C。这时候,我们需要一种规则规定,信号产生后就调用函数 A,就像交通规则一样,红灯停绿灯行,信号注册函数 signal() 就是做这样的事情。
所需头文件:
#include <signal.h>
typedef void (*sighandler_t)(int);// 回调函数的声明
sighandler_t signal(int signum,sighandler_t handler);
功能:
注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l (“l” 为字母)进行相应查看。
handler: 取值有 3 种情况:
SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数。
返回值:
成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回 SIG_ERR
示例一:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void signal_handler(int signo)
{
if(signo == SIGINT){
printf("recv SIGINT\n");
}else if(signo == SIGQUIT){
printf("recv SIGQUIT\n");
}
}
int main(int argc, char *argv[])
{
printf("wait for SIGINT OR SIGQUIT\n");
/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
// 信号注册函数
signal(SIGINT, signal_handler);
signal(SIGQUIT, signal_handler);
// 等待信号
pause();
pause();
return 0;
}
信号集与信号阻塞集
信号集
为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集(信号的集合)。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友。
信号集是用来表示多个信号的数据类型(sigset_t),其定义路径为:/usr/include/i386-linux-gnu/bits/sigset.h。
信号集相关的操作主要有如下几个函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigismember(const sigset_t *set, int signum);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
以上几个函数的用法都是比较简单,这里就不一一介绍,我们通过一个例子来学习其用法:
#include <signal.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
sigset_t set; // 定义一个信号集变量
int ret = 0;
sigemptyset(&set); // 清空信号集的内容
// 判断 SIGINT 是否在信号集 set 里
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGINT);
if(ret == 0){
printf("SIGINT is not a member of set \nret = %d\n", ret);
}
sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set
// 判断 SIGINT 是否在信号集 set 里
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGINT);
if(ret == 1){
printf("SIGINT is a member of set \nret = %d\n", ret);
}
sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除
// 判断 SIGQUIT 是否在信号集 set 里
// 在返回 1, 不在返回 0
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
printf("SIGQUIT is not a member of set \nret = %d\n", ret);
}
return 0;
}
运行结果如下:
(学习复习资料)本文内容来自:https://blog.csdn.net/tennysonsky/article/details/45847107