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