原理
对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。
信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了ctrl+c来中断程序,会通过信号机制停止一个程序。
概述
信号的名字和编号
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGUP(挂起) ”、“SIGINT(中断)、SIGQUIT(退出)”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以 使用kill -l来查看信号的名字以及序号,
信号是从1开始编号的,不存在0号信号。kill对于信号0有特殊的应用。
信号的处理
信号的处理方式有三种,分别是忽略、捕捉和默认动作。
忽略信号
大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。
系统自带的忽略宏函数
signal(SIGINT,SIG_IGN);//将SIGINT信号(ctrl+C、2)忽略
捕捉信号
需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。( 在main函数外定义一个函数,用signal函数中的参数调用该函数并执行函数中的功能)
系统默认动作
对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。
例子如下:
其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID 来杀死进程。比如,我在后台运行了一个 a.out 工具,通过 ps 命令可以查看他的 PID,通过 kill 9来发送了一个终止进程的信号来结束了 a.out 进程。如果查看信号编号和名称,可以发现9对应的是 SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。
kill -9 进程PID
kill -SIGKILL 进程PID
可见,两者的执行结果相同。说明kill命令是发送信号的工具。
signal函数
功能
设置某一信号的对应动作
头文件
#include <signal.h>
原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数解读
第一行是真实处理信号的函数:中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号,即注册函数的第二个参数可以调用信号处理函数并执行其中的功能。
第二行是信号处理注册的函数:
signum | 信号的编号,如SIGKILL的编号是9 |
handler | 中断函数的指针,写入后可以调用编写的真实处理信号函数并执行功能 |
signal()会依参数signum指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。
返回值
成功则返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
代码示例
信号处理函数的注册
signal1.c
#include <stdio.h>
#include <signal.h>
void handler(int signum)
{
printf("get signum is %d\n",signum);
printf("not quit\n");
switch(signum)
{
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
代码编译后查看运行a.out工具,通过ps查看其编号
运用kill指令分别对信号进行处理
注:第一种按下crtl+c执行结果相同。
可见调用signal函数后匹配的正确编号后会执行handler中的功能(将函数编号打印出来)。第三个与前两个结果不一样是因为SIGKILL指令无法被忽略,这里的kill -9发出的是指令,由于代码为死循环,若SIGKILL被忽视,则会导致代码无法终止循环,所以一旦SIGKILL指令发出,程序立刻停止(被杀死)。
发送信号处理函数
signal2.c
#include <stdio.h>
#include <signal.h>
int main(int argc,char **argv)//由于需要此代码发送指令另一部分代码才会执行,所以需要进行传参,参数为kill参数,格式为./a.out pid signum
{
int signum;
int pid;
signum = atoi(argv[1]);
pid = atoi(argv[2]);
kill(pid,signum);//调用kill函数,将信号处理编号和工具的pid值输入即可
printf("send signal success\n");
return 0;
}
先编译signal1.c(上一模块的代码)并运行
调用ps指令查看该程序的信号值
编译运行signal2.c中的代码传参即可运行signal1.c代码中的功能
将signum与pid输入后即可实现signal1.c中的功能,实现信号捕捉处理。
功能与signal2.c一样的代码:
#include <stdio.h>
#include <signal.h>
int main(int argc,char **argv)
{
int signum;
int pid;
char cmd[128] = {0};
signum = atoi(argv[1]);
pid = atoi(argv[2]);
sprintf(cmd,"kill -%d %d",pid,signum);//cmd的指令格式为“”里的格式,即调用kill指令
system(cmd);//调用cmd指令
printf("send signal success\n");
return 0;
}
注:
1、atoi (表示 ascii to integer)是把字符串转换成整型数的一个函数。
2、sprintf指的是字符串格式化命令,函数原型为
int sprintf(char *string, char *format [,argument,…]);
主要功能是把格式化的数据写入某个字符串中,即发送格式化输出到 string 所指向的字符串。
信号忽略函数的补充
代码展示
#include<stdio.h>
#include <signal.h>
void handler(int signum)
{
printf("get signum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
}
int main()
{
signal(SIGINT,SIG_IGN);//将SIGINT信号(ctrl+C、2)忽略
signal(SIGUSR1,SIG_IGN);//将SIGUSR1信号(10)忽略
while(1);
return 0;
}
可见crtl+c和kill -10和kill -2都被忽略了,只有kill -9才能使该程序终止,印证的信号处理中的忽略部分不能忽略SIGKILL。