1. 什么是信号
简而言之,信号是事件发生时用于给进程发送各种通知的机制,以便通知进程发生何种事件。
站在操作系统的角度来说,信号也称为软件中断,因为信号可以改变程序的执行流程,大多数情况下无法预测信号到达的精确时间。当信号传递给进程时,它会中断进程当前正在进行的任何操作,并强制进程处理或忽略信号,又或者在某些情况下终止进程。
2. 向进程发送信号
比如写一个A程序每隔1秒打印一次hello world,然后在终端按Ctrl + C时,系统就会产生一个SIGINT信号并发送给A程序,我们无法预测SIGINT信号何时到达A程序,但是当A程序收到SIGINT信号时,无论A程序此时执行到哪里了,SIGINT信号都会中断A程序的执行流程,同时强制A程序处理SIGINT信号,由于SIGINT信号的默认处理动作是终止一个进程,那么A程序会立即终止结束,而不是按正常的执行流程终止结束。
3. 产生信号相关的事件
虽然信号是进程间通信(IPC)的一种方式,进程也可以向自身发送信号,但是通常是由内核来产生信号的,一般产生信号的事件大概有以下几种:
1 . 按键产生,如Ctrl+c,Ctrl+z,Ctrl+\等
2 . 系统调用产生,如kill、raise、abort等系统调用函数,在程序中通过调用某些系统函数给进程发送信号
3 . 硬件异常产生,如非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
4 . 在终端上输入命令产生信号,如kill命令
拿按键产生信号来说,比如想要终止一个进程就可以通过按键Ctrl+c的方式使系统内核产生一个信号并发送给这个进程,而这个进程接收到这个信号并执行这个信号的默认处理动作:终止一个进程。
Ctrl+\也是终止一个进程,区别在于Ctrl+c是发送信号终止进程的执行,相当于直接把进程干掉,而Ctrl+\是发送信号让进程尽快的结束运行,没有那么直接。
Ctrl+z会向正在运行的进程发送SIGTSTP信号,默认情况下,此信号会导致进程暂停执行。
4. linux支持的信号
有同学可能会问,linux系统中到底有多少信号呢?如果我们想要查看当前系统支持的信号可以使用kill –l命令查看当前系统可使用的信号:
linux中的信号的编号是从1开始的
,其中编号是1 - 31的称之为常规信号(也叫普通信号或标准信号),34-64号称之为实时信号,驱动编程与硬件相关。另外32和33编号的信号作为保留并未使用,不同的linux系统有些信号可能会不太一样,如果你想要详细了解关于信号更多的资料可以参考《linux/unix系统编程手册(上册)》,这本书对于信号的介绍非常详细。
5 . 使用系统调用kill发送信号
前面我们讲过shell命令也可以产生信号,比如kill命令,而kill函数实现一样的功能,给指定进程发送指定信号。
函数原型:
int kill(pid_t pid, int sig);
返回值:成功0;失败-1并设置errno
参数sig:
指定要发送的信号,如果sig为0,通常用来测试是否有权限(权限保护)向进程发信号。
参数pid:
pid > 0表示发送信号给pid指定的进程。
pid = 0表示发送信号给与调用kill函数的进程属于同一进程组的所有进程
pid < -1表示取|pid|发给对应进程组
pid = -1表示向系统中所有进程发送信号,前提是有权限
6. 实验
循环创建5个子进程,任一子进程用kill函数终止其父进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#define N 5
int main(void) {
int i;
//默认创建5个子进程
for(i = 0; i < N; i++){
if(fork() == 0){
break;
}
}
if (i == 3) {
sleep(1);
printf("-----------child ---pid = %d, ppid = %d\n", getpid(), getppid());
//子进程终止父进程
kill(getppid(), SIGKILL);
//父进程
} else if (i == N) {
printf("I am parent, pid = %d\n", getpid());
while(1);
}
return 0;
}
程序执行结果:
7. raise函数的用法
raise 函数是给当前进程发送指定信号(自己给自己发),注意raise函数和kill函数的区别。
当进程调用raise函数向自身发送信号时,信号将会立即递送,即在raise返回前。另外raise出错不一定返回-1,调用raise唯一的错误就是EINVAL,即参数isg无效。
函数原型:
int raise(int sig); 成功:0,失败非0值
代码实验:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
int main(void){
int ret;
while(1){
sleep(1);
//打印进程id
printf("pid = %d\n", getpid());
ret = raise(SIGINT);
//失败直接break
if(ret != 0){
break;
}
}
return 0;
}
程序执行结果:
8. 信号的处理方式
信号的处理方式一般有以下几种:
-
执行默认动作,对于大多数信号的系统默认动作是终止该进程。
-
忽略(丢弃),大多数信号都可使用这种方式来处理
-
捕捉(调用自定义信号处理函数)
9. 信号捕捉函数——signal
根据信号的处理方式可知,如果我们不想信号执行默认的处理动作时,就要进行捕捉信号,并注册自定义信号处理函数,而signal函数就是用来做这件事的。
需要注意的是,signal函数由ANSI定义,由于历史原因在不同版本的Unix实现中可能存在着差异,这意味着如果程序需要考虑可移植性的话,那么应该尽量避免使用它,取而代之使用sigaction函数。
函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数signum:表示要捕捉的信号
参数handler:捕捉后默认处理动作函数指针
另外系统还为handler参数提供了两个宏,分别是 SIG_DFL和 SIG_IGN :
1. 如果handler指定为SIG_DFL,系统将为该信号指定默认的信号处理函数。2. 如果 handler指定为SIG_IGN,系统将忽略该信号即内核会将信号丢弃,而进程不会知道曾经产生了该信号。
返回值说明:
成功返回函数指针,即先前的信号处理函数,也有可能是SIG_DFL或SIG_IGN;失败则返回SIG_ERR说明注册失败,设置errno
signal函数实现捕捉信号实验:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
void do_sig(int a)
{
printf("hello, SIGINT\n");
}
int main(void)
{
if (signal(SIGINT, do_sig) == SIG_ERR) {
perror("signal");
exit(1);
}
while (1) {
printf("---------------------\n");
sleep(1);
}
return 0;
}
程序执行结果:
当使用signal函数捕捉了SIGINT信号后,此时在终端输入Ctrl-C发送SIGINT信号不再是让进程退出了,而是调用注册的信号处理函数,打印hello SIGINT。