Fork me on GitHub

信号

引言

信号是软件中断。很多比较重要的应用程序都需要处理信号,信号提供了一种处理异步事件的方法,例如,终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道中的下一个程序。

信号概念

首先,每个信号都有一个名字。这些名字都以3个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号。SIGALRM是闹钟信号,由alarm函数设置的定时器超时后将产生此信号。

在头文件中,信号名都被定义为正整数常量(信号编号)。

不存在编号为0的信号。kill函数对信号编号0有特殊的应用。POSIX.1将此种信号编号值称为空信号。

很多条件可以产生信号:

  • 当用户按某些终端键时,引发终端产生的信号。在终端上按Delete键(或者很多系统中的Ctrl+C键)通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。
  • 硬件异常产生信号:除数为0、无效的内存引用等。这些条件通常由硬件检测到,并通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。
  • 进程调用kill函数可将任意信号发送给另一个进程或进程组。自然,对此有所限制,接受信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
  • 用户可用kill命令将信号发送给其他进程。此命令只是kill函数的接口。常用此命令终止一个失控的后台进程。
  • 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。这里指的不是硬件产生条件(如除以0),而是软件条件。例如SIGURG(在网络连接上传来带外的数据)、SIGPIPE(在管道的读进程已终止后,一个进程写此管道)以及SIGALRM(进程所设置的定时器已经超时)。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单的测试一个变量(如errno)来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。

在某个信号出现时,可以告诉内核按下列3中方式之一进行处理,我们称之为信号的处理或与信号相关的动作。

  1. 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却绝不能忽略,它们是SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用或除以0),则进程的运行行为是未定义的。
  2. 捕获信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。比如,如果捕获到SIGCHLD信号,则表示一个子进程已经终止,所以此信号的捕获函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。注意:不能捕获SIGKILL和SIGSTOP信号
  3. 执行系统默认动作,对大多数系统默认动作是终止该进程。

image

下面较详细地逐一说明这些信号:

参考UNIX环境高级编程。

函数signal

UNIX系统信号机制最简单的接口是signal函数。

1
2
3
#include <signal.h>
void (*signal(int signo ,void (*func)(int))) (int);
//若成功,返回以前的信号处理配置;若出错,返回SIG_ERR

通过typedef可以转换成这样:

1
2
typedef void Sigfunc(int);
Sigfunc *signal(int,Sigfunc *);

也就是说,signal有两个参数,一个是int,一个是Sigfunc ,返回值也是Sigfunc ,该指针指向一个参数为int,无返回值的函数。

signo参数是上图中的信号名。func的值是常量SIG_IGN、常量SIGDFL或当接到此信号后要调用的函数的地址。

  • SIG_IGN,则向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略)。
  • SIG_DFL,则表示接到此信号后的动作是系统默认动作。
  • 当指定函数地址时,则在信号发生时,调用该信号,我们称这种处理为捕获该信号,称此函数为信号处理程序(signal handler)或信号捕获函数(signal-catching function)。

当调用signal设置处理程序时,第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向在此之前的信号处理程序的指针

signal 的第1个参数signum表示要捕捉的信号,第2个参数是个函数指针,表示要对该信号进行捕捉的函数,该参数也可以是SIG_DEF(表示交由系统缺省处理,相当于白注册了)或SIG_IGN(表示忽略掉该信号而不做任何处理)。signal如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR。

1
2
3
#define SIG_ERR (void(*)())-1
#define SIG_DFL (void(*)())0
#define SIG_IGN (void(*)())1

这些常量可用于表示“指向函数的指针,该函数要求一个整形参数,并且无返回值”。signal的第二个参数及其返回值就可用它们表示。这些常量所使用的3个值不一定是-1、0和1,但它们必须是3个值而绝不能是任一函数的地址。

注:

为什么不是这样定义的呢??

1
2
3
#define SIG_ERR (void (*)(int))-1
#define SIG_DEL (void (*)(int))0
#define SIG_IGN (void (*)(int))1

在网上搜索之后找到答案,C语言中是可以这样定义的,C语言中前向声明是可以省略参数的,意味着可以有任意多个参数

1
2
3
4
5
6
7
8
9
void fun();
int main()
{
fun(1,2);
}
void fun(int i, int j)
{
printf("%d\n",i+j);
}

只是将-1强制转换为一个指针,通过编译。就像

1
#define NULL (void *)0

可以将SIG_ERR跟其他的信号理解的一样,是一个整数。

示例

下面给出了一个简单的信号处理程序,它捕获两个用户定义的信号并打印信号编号,pause函数,它使调用进程在接到一信号前挂起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "head.h"
static void sig_usr(int); //one handler for both signals
int main(void){
if(signal(SIGUSR1,sig_usr) == SIG_ERR){
printf("can't catch error\n");
}
if(signal(SIGUSR2,sig_usr) == SIG_ERR){
printf("can't catch error\n");
}
printf("we come here,catch no signal\n");
for(;;){
pause();
}
}
static void sig_usr(int signo){ //argument is signal number
if(signo == SIGUSR1){
printf("received SIGUSR1\n");
}else if(signo == SIGUSR2){
printf("received SIGUSR2\n");
}else{
printf("received signal %d\n",signo);
}
}

我们使程序在后台运行,并且用kill命令将信号发送给它。

注意,在UNIX系统中,杀死(kill)这个术语是不恰当的。kill命令和kill函数只是将一个信号发送给一个进程或进程组。该信号是否终止进程取决于该信号的类型,以及进程是否安排了捕获该信号。

1
2
3
4
5
6
7
8
[vrlive@iZ23chs2r19Z ten]$ ./signal.o & //后台启动进程
[4] 3243 //进程ID
[vrlive@iZ23chs2r19Z ten]$ kill -USR1 3243
received SIGUSR1
[vrlive@iZ23chs2r19Z ten]$ kill -USR2 3243
received SIGUSR2
[vrlive@iZ23chs2r19Z ten]$ kill 3243 //默认向该进程发送SIGTERM
[4]+ Terminated ./signal.o

程序启动

当执行一个程序时,所有信号的状态都是系统默认或者忽略。通常所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号。确切地讲,exec函数将原先设置为要捕获的信号都更改为默认状态,其他信号的状态不变(一个进程原先要捕获的信号,当其执行一个新程序后,就不再捕获了,因为信号捕获函数的地址很可能在所执行的新程序文件中已无意义)。

很多捕获这两个信号的交互程序具有下列形式的代码:

1
2
3
4
5
6
void sig_int(int);
void sig_quit(int);
if(signal(SIGINT,SIG_IGN) != SIG_IGN)
signal(SIGINT,sig_int);
if(signal(SIGQUIT,SIG_IGN) != SIG_IGN)
signal(SIGQUIT,sig_quit);

这样处理后,仅当SIGINT和SIGQUIT当前未被忽略时,进程才会捕获它们。

从signal的这两个调用中也可以看到这种函数的限制:不改变信号的处理方式就不能确定信号的当前处理方式。

进程创建

当一个进程调用fork时,其子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中时有意义的。

示例

首先注册一个信号处理函数,发送信号后,注册信号处理函数更新新的注册信号函数,代替之前的信号处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "head.h"
static void sig_usr(int);
static void sig_usr1(int);
int main(void){
if(signal(SIGUSR1,sig_usr) == SIG_ERR){
printf("can't catch SIGUSR1\n");
}
for(;;){
pause();
}
}
static void sig_usr(int signo){
signal(SIGUSR1,sig_usr1);
printf("now we reestablish handler for next time\n");
}
static void sig_usr1(int signo){
printf("this handler is new handler\n");
}
1
2
3
4
5
6
7
8
9
10
[vrlive@iZ23chs2r19Z ten]$ ps -auf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
vrlive 4420 0.0 0.0 115512 2136 pts/1 Ss 10:18 0:00 -bash
vrlive 4444 0.0 0.0 139496 1616 pts/1 R+ 10:18 0:00 \_ ps -auf
vrlive 4378 0.0 0.0 115516 2140 pts/0 Ss 10:03 0:00 -bash
vrlive 4415 0.0 0.0 4164 352 pts/0 S+ 10:17 0:00 \_ ./changesignal.o
root 473 0.0 0.0 110036 844 tty1 Ss+ 2017 0:00 /sbin/agetty --noclear tty1 linux
[vrlive@iZ23chs2r19Z ten]$ kill -USR1 4415
[vrlive@iZ23chs2r19Z ten]$ kill -USR1 4415
[vrlive@iZ23chs2r19Z ten]$ kill -USR1 4415
1
2
3
4
5
[vrlive@iZ23chs2r19Z ten]$ ./changesignal.o
now we reestablish handler for next time
this handler is new handler
this handler is new handler
-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2018/05/16/信号/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!