什么是信号(signal)
信号是一种软件中断,一种向进程传递有关其他进程,操作系统和硬件状态的信息的方法。信号是一种中断,因为它可以改变程序的流程。当信号传递给进程时,进程将停止其执行的操作,处理或忽略信号,或者在某些情况下终止,取决于信号。
由于信号可能源自当前正在执行的过程之外的事实,信号也可能以不可预测的方式传递,与程序不一致。查看信号的另一种方法是一种处理异步事件的机制。与同步事件相反,同步事件是标准程序执行迭代,即一行代码跟随另一行。当程序的某些部分按顺序执行时,就会发生异步事件。异步事件通常由于源自硬件或操作系统的外部事件而发生;信号本身是操作系统将这些事件传递给进程的方式,以便进程可以采取适当的操作。
如何使用它们
信号在Unix编程中用于各种各样的目的,我们已经在较小的上下文中使用它们。例如,当我们在shell中工作并希望“杀死所有cat程序”时,我们输入命令:
#> killall cat
killall命令将向所有名为cat的进程发送一个信号,表示“终止”。发送的实际信号是SIGTERM,其目的是将终止请求传送给给定进程,但该进程实际上不必终止…稍后将详细说明。
我们还在终端信令的上下文中使用和查看信号,这是程序停止,启动和终止的方式。我们输入Ctrl-c与发送SIGINT信号相同,输入Ctrl-z与发送SIGTSTP信号相同,我们输入fg或bg与发送SIGCONT信号相同。
这些信号中的每一个都描述了该过程应该采取的响应动作。此操作超出了程序的正常控制流程,事件异步到达,要求进程中断其当前操作以响应事件。对于上述信号,响应是明确的 - SIGTERM终止,SIGSTOP停止,SIGCONT继续 - 但对于其他信号,程序员可以选择正确的响应,这可能只是简单地忽略信号。
常用的Signals
每个信号都有一个名称,它以SIG开头,以描述结束。我们可以在手册页的第7节中查看所有信号,下面是您可能与之交互的标准Linux信号:
Signal | Value | Action | Comment |
---|---|---|---|
SIGHUP | 1 | Term | Hangup detected on controlling terminal or death of controlling process |
SIGINT | 2 | Term | Interrupt from keyboard |
SIGQUIT | 3 | Core | Quit from keyboard |
SIGILL | 4 | Core | Illegal Instruction |
SIGABRT | 6 | Core | Abort signal from abort(3) |
SIGFPE | 8 | Core | Floating point exception |
SIGKILL | 9 | Term | Kill signal |
SIGSEGV | 11 | Core | Invalid memory reference |
SIGPIPE | 13 | Term | Broken pipe: write to pipe with no readers |
SIGALRM | 14 | Term | Timer signal from alarm(2) |
SIGTERM | 15 | Term | Termination signal |
SIGUSR1 | 30,10,16 | Term | User-defined signal 1 |
SIGUSR2 | 31,12,17 | Term | User-defined signal 2 |
SIGCHLD | 20,17,18 | Ign | Child stopped or terminated |
SIGCONT | 19,18,25 | Cont | Continue if stopped |
SIGSTOP | 17,19,23 | Stop | Stop process |
SIGTSTP | 18,20,24 | Stop | Stop typed at tty |
SIGTTIN | 21,21,26 | Stop | tty input for background process |
SIGTTOU | 22,22,27 | Stop | tty output for background process |
信号的name和value关联
每个信号都有名称,值和默认操作。信号名称应该开始变得更加熟悉,信号的值实际上与信号本身相同。实际上,信号名称只是一个#defined值,我们可以通过查看sys / signal.h头文件来看到:
#define SIGHUP 1 /* hangup */
#define SIGINT 2 /* interrupt */
#define SIGQUIT 3 /* quit */
#define SIGILL 4 /* illegal instruction (not reset when caught) */
#define SIGTRAP 5 /* trace trap (not reset when caught) */
#define SIGABRT 6 /* abort() */
#define SIGPOLL 7 /* pollable event ([XSR] generated, not supported) */
#define SIGFPE 8 /* floating point exception */
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
信号的 Action
每个信号都有一个默认动作。表中描述了四种:
Term : The process will terminate
Core : The process will terminate and produce a core dump file that traces the process state at the time of termination.
Ign : The process will ignore the signal
Stop : The process will stop, like with a Ctrl-Z
Cont : The process will continue from being stopped
对于某些信号,我们可以更改默认操作。一些信号,即控制信号,不能改变它们的默认动作,包括SIGKILL和SIGABRT,这就是为什么“kill 9”是最终的kill语句。
处理和生成信号
1. Hello信号处理世界
信号处理的主要系统调用是signal(),它给出信号和功能,只要信号被传送就会执行该功能。此函数称为信号处理程序,因为它处理信号。 signal()函数有一个奇怪的声明:
int signal(int signum, void (*handler)(int))
也就是说,signal有两个参数:第一个参数是信号编号,例如SIGSTOP或SIGINT,第二个参数是对第一个参数为int并返回void的处理函数的引用。
#include <stdlib.h>
#include <stdio.h>
#include <signal.h> /*for signal() and raise()*/
void hello(int signum){
printf("Hello World!\n");
}
int main(){
//execute hello() when receiving signal SIGUSR1
signal(SIGUSR1, hello);
//send SIGUSR1 to the calling process
raise(SIGUSR1);
}
上述程序首先为用户信号SIGUSR1建立信号处理程序。信号处理函数hello()按预期执行:打印“Hello World!”到stdout。程序然后发送SIGUSR1信号,这是通过raise()完成的,执行程序的结果是漂亮的短语:
#> ./hello_signal
Hello World!
2.异步执行
从hello程序中取消的一些关键点是signal()的第二个参数是一个函数指针,一个对要调用的函数的引用。这告诉操作系统无论何时将此信号发送到此进程,都要将此函数作为信号处理程序运行。
此外,信号处理程序的执行是异步的,这意味着程序的当前状态将在信号处理程序执行时暂停,然后执行将从暂停点恢复,就像上下文切换一样。
让我们看看另一个示例hello world程序:
/* hello_loop.c*/
void hello(int signum){
printf("Hello World!\n");
}
int main(){
//Handle SIGINT with hello
signal(SIGINT, hello);
//loop forever!
while(1);
}
上面的程序将为SIGINT设置一个信号处理程序,键入Ctrl-C时生成的信号。问题是,当我们执行这个程序时,键入Ctrl-C会发生什么?
首先,让我们考虑一下程序的执行情况。它将注册信号处理程序,然后进入无限循环。当我们按下Ctrl-C时,我们都同意信号处理程序hello()应该执行并且“Hello World!”打印到屏幕上,但程序处于无限循环中。为了打印“Hello World!”一定是它打破循环执行信号处理程序的情况,对吗?所以它应该退出循环以及程序。让我们来看看:
#> ./hello_loop
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^CHello World!
^\Quit: 3
如输出所示,每次我们发出Ctrl-C“Hello World!”打印,但程序返回无限循环。只有在用Ctrl- \发出SIGQUIT信号后,程序才真正退出。
虽然循环将退出的解释是合理的,但它没有考虑信号处理的主要原因,即异步事件处理。这意味着信号处理程序的行为超出了程序控制的标准流程;事实上,整个程序都保存在一个上下文中,并且只为信号处理程序创建了一个新的上下文。如果你再考虑一下,你会发现这很酷,也是一种全新的方式。查看编程。
3.进程间通信
信号也是进程间通信的关键手段。一个进程可以向另一个进程发送信号,指示应该采取措施。要向特定进程发送信号,我们使用kill()系统调用。功能声明如下。
int kill(pid_t pid, int signum);
与命令行版本非常相似,kill()接受一个进程标识符和一个信号,在这种情况下,信号值为int,但值为#defined,因此您可以使用该名称。让我们看看它在使用中。
/*ipc_signal.c*/
void hello(){
printf("Hello World!\n");
}
int main(){
pid_t cpid;
pid_t ppid;
//set handler for SIGUSR1 to hello()
signal(SIGUSR1, hello);
if ( (cpid = fork()) == 0){
/*CHILD*/
//get parent's pid
ppid = getppid();
//send SIGUSR1 signal to parrent
kill(ppid, SIGUSR1);
exit(0);
}else{
/*PARENT*/
//just wait for child to terminate
wait(NULL);
}
}
在这个程序中,首先为SIGUSR1建立一个信号处理程序,即hello()函数。在fork之后,父进程调用wait(),并且子进程将通过SIGUSR1信号“杀死”它来与父进行通信。结果是在父级和“Hello World!”中调用了处理程序。从父级打印到stdout。
虽然这只是一个小例子,但信号对于进程间通信是不可或缺的。在前面的课程中,我们讨论了如何使用pipe()在进程之间传递数据,信号是进程传递状态更改和其他异步事件的方式。也许最相关的是儿童过程中的状态变化。 SIGCHLD信号是孩子终止时传递给父母的信号。到目前为止,我们一直在通过wait()隐式处理这个信号,但您可以选择处理SIGCHLD并在子进程终止时采取不同的操作。
4.忽略信号
到目前为止,我们的处理程序一直在打印“Hello World!” - 但我们可能只是希望我们的处理程序什么也不做,基本上是忽略了信号。这很容易写入代码,例如,这是一个程序,它将通过处理信号忽略SIGINT并且什么都不做:
/*ingore_sigint.c*/
#include <signal.h>
#include <sys/signal.h>
void nothing(int signum){ /*DO NOTHING*/ }
int main(){
signal(SIGINT, nothing);
while(1);
}
如果我们运行这个程序,我们会看到,是的,Ctrl-c无效,我们必须使用Ctrl- \来退出程序:
>./ignore_sigint
^C^C^C^C^C^C^C^C^C^C^\Quit: 3
signal.h标头定义了一组可用于代替处理程序的操作:
- SIG_IGN:忽略信号
- SIG_DFL:用默认处理程序替换当前信号处理程序
使用这些关键字,我们可以简单地将程序重写为:
int main(){
// using SIG_IGN
signal(SIGINT, SIG_IGN);
while(1);
}
5.更改并恢复默认处理程序
设置信号处理程序不是一个单一事件。您始终可以更改处理程序,还可以将处理程序恢复为默认状态。例如,请考虑以下程序:
/*you_shot_me.c*/
void handler_3(int signum){
printf("Don't you dare shoot me one more time!\n");
//Revert to default handler, will exit on next SIGINT
signal(SIGINT, SIG_DFL);
}
void handler_2(int signum){
printf("Hey, you shot me again!\n");
//switch handler to handler_3
signal(SIGINT, handler_3);
}
void handler_1(int signum){
printf("You shot me!\n");
//switch handler to handler_2
signal(SIGINT, handler_2);
}
int main(){
//Handle SIGINT with handler_1
signal(SIGINT, handler_1);
//loop forever!
while(1);
}
#> ./you_shout_me
^CYou shot me!
^CHey, you shot me again!
^CDon't you dare shoot me one more time!
^C
程序首先启动handler_1()作为SIGINT的信号处理程序。在第一个Ctrl-c之后,在信号处理程序中,处理程序更改为handler_2(),在第二个Ctrl-c之后,它再次从handler_2()更改为handler_3()。最后,在handler_3()中重新建立默认信号处理程序,即在SIGINT上终止,这就是我们在输出中看到的:
#> ./you_shout_me
^CYou shot me!
^CHey, you shot me again!
^CDon't you dare shoot me one more time!
^C
6.有些信号比其他信号更平等
关于信号处理的最后一点是,并非所有信号都是相同的。这意味着,您无法处理所有信号,因为它可能会使系统处于不可恢复的状态。
永远不会被忽略或处理的两个信号是:SIGKILL和SIGSTOP。我们来看一个例子:
/* ignore_stop.c */
int main(){
//ignore SIGSTOP ?
signal(SIGSTOP, SIG_IGN);
//infinite loop
while(1);
}
上面的程序试图为SIGSTOP设置忽略信号处理程序,然后进入无限循环。如果我们执行该计划,我们发现这些努力没有结果:
#>./ignore_stop
^Z
[1]+ Stopped ./ignore_stop
对于忽略SIGKILL的程序,我们可以看到相同的内容。
int main(){
//ignore SIGSTOP ?
signal(SIGKILL, SIG_IGN);
//infinite loop
while(1);
}
#>./ignore_kill &
[1] 13129
#>kill -SIGKILL 13129
[1]+ Killed: 9 ./ignore_kill
7.检查信号错误()
signal()函数返回一个指向前一个信号处理程序的指针,这意味着这里再次是一个系统调用,我们不能通过检查返回值是否小于0来以典型的方式进行错误检查。这是因为指针类型是无符号的,没有负指针这样的东西。
相反,使用特殊值SIG_ERR,我们可以比较signal()的返回值。这里再次是我们尝试忽略SIGKILL的程序,但这次正确的错误检查:
/*signal_errorcheck.c*/
int main(){
//ignore SIGSTOP ?
if( signal(SIGKILL, SIG_IGN) == SIG_ERR){
perror("signal");;
exit(1);
}
//infinite loop
while(1);
}
输出
#>./signal_errorcheck
signal: Invalid argument