目录
引言
信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法,例如,终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道中的下一个程序。
1.信号概念
1.首先,每个信号都有一个名字。这些名字都以3个字符SIG开头。例如,SIGABRT是天折信号,当进程调用abort 函数时产生这种信号。SIGALRM是闹钟信号,由alarm 函数设置的定时器超时后将产生此信号。
2.很多条件可以产生信号。
- 当用户按某些终端键时,引发终端产生的信号。在终端上按Delete键(或者很多系统中的Ctrl+C键)通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。
- 硬件异常产生信号:除数为0、无效的内存引用等。这些条件通常由硬件检测到,并通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。
- 进程调用kil1(2)函数可将任意信号发送给另一个进程或进程组。自然,对此有所限制:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
- 用户可用ki11(1)命令将信号发送给其他进程。此命令只是kil1函数的接口。常用此命令终止一个失控的后台进程。
- 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。这里指的不是硬件产生条件(如除以0),而是软件条件。例如SIGURG(在网络连接上传来带外的数据)、SIGPIPE(在管道的读进程已终止后,一个进程写此管道)以及STGALRM(进程所设置的定时器已经超时)
3.在某个信号出现时,可以告诉内核按下列3种方式之一进行处理,我们称之为信号的处理或与信号相关的动作。
(1)忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用或除以0),则进程的运行行为是未定义的。
(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若正在编写一个命令解释器,它将用户的输入解释为命令并执行之,当用户用键盘产生中断信号时,很可能希望该命令解释器返回到主循环,终止正在为该用户执行的命令。如果捕捉到SIGCHLD 信号,则表示一个子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要为SIGTERM信号编写一个信号捕捉函数以清除临时文(SIGTERM是终止信号,kill命令传送的系统默认信号是终止信号)。注意,不能捕捉SIGKILL和SIGsTOP信号。
(3)执行系统默认动作。注意,对大多数信号的系统默认动作是终止该进程。
4.系统信号
了解了信号的概述,那么,信号是如何来使用呢?
其实对于常用的 kill 命令就是一个发送信号的工具,
kill 9 PID
来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9)SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。杀死进程
2.信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版
- 入门版:函数
signal
- 高级版:函数
sigaction
3.信号处理发送函数
信号发送函数也不止一个,同样分为入门版和高级版
1.入门版:kill
2.高级版:sigqueue
4.信号注册函数——(入门版)
4.1 signal函数
函数原型:
根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。
对于sighandler_t signal(int signum, sighandler_t handler);
函数来说,signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int);
中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。我们先来看看简单一个信号注册的代码示例吧。(signal函数的入门理解参考:4.typedef void (*sighandler_t)(int);_单片机爱好者之家的博客-CSDN博客)
4.2 信号注册的代码
#include <signal.h>
#include <stdio.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)
{
printf("get signum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
printf("never quit\n");
}
int main()
{
signal(SIGINT,SIG_IGN);
signal(SIGKILL,SIG_IGN);
signal(SIGUSR1,handler);
while(1);
return 0;
}
4.3 信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为 SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。
#include <signal.h>
#include <stdio.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)
{
printf("get signum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
printf("never quit\n");
}
int main()
{
signal(SIGINT,SIG_IGN);//参数1:注册(接收)crtl+c这个信号,参数2:宏(忽略)
signal(SIGKILL,SIG_IGN);
signal(SIGUSR1,SIG_IGN);
while(1);
return 0;
}
4.3 kill函数
函数原型
kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。
信号的处理需要有接受者,显然发送者必须要知道发给谁,根据 kill 函数的远行可以看到,pid 就是接受者的 pid,sig 则是发送的信号的类型。从原型来看,发送信号要比接受信号还要简单些。
//发送信号
#include <signal.h>
#include <stdio.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
void handler(int signum)
{
printf("get signum = %d\n",signum);
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); //参数1:注册(接收)crtl+c这个信号,参数2:调用handler函数处理信号
signal(SIGKILL,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
接收信号
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
//int kill(pid_t pid, int sig);
int main(int argc,char **argv)
{
int signum;
int pid;
signum = atoi(argv[1]); //atoi() 将argv[i]的字符串,转换为 整型数
pid = atoi(argv[2]);
printf("signum = %d , pid = %d \n",signum,pid);
//int kill(pid_t pid, int sig);
kill(pid,signum);
printf("send siganal ok\n");
return 0;
}
4.4 除了 kill()函数 还可以用 sprintf() 配合 system() 函数 来做到同样的信号处理发送
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
int main(int argc ,char **argv)
{
int signum;
int pid;
char cmd[128]={0};
signum = atoi(argv[1]);
pid = atoi(argv[2]);
printf("num=%d,pid=%d\n",signum,pid);
// kill(pid, signum);
sprintf(cmd,"kill -%d %d",signum,pid);
system(cmd);
printf("send signal ok");
return 0;
}
5.信号注册函数——高级版
5.1 信号注册函数 :sigaction
函数原型:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
这个函数的原版帮助信息,可以通过man sigaction
来查看。
sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum
应该就是注册的信号的编号;第二个参数act
如果不为空说明需要对该信号有新的配置;第三个参数oldact
如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。
在这里额外说一下struct sigaction
结构体中的 sa_mask 成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。
sigaction 的时效性:当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用 sigaction并改变动作之前都会一直有效。
关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用 sa_sigaction 这个函数指针所指向的信号处理函数。否则,系统会默认使用 sa_handler 所指向的信号处理函数。在此,还要特别说明一下,sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。
关于void (*sa_sigaction)(int, siginfo_t *, void *);
处理函数来说还需要有一些说明。void*
是接收到信号所携带的额外数据;而struct siginfo
这个结构体主要适用于记录接收信号的一些相关信息。
其中的成员很多,si_signo 和 si_code 是必须实现的两个成员。可以通过这个结构体获取到信号的相关信息。
关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据。
那么,kill 函数发送的信号是无法携带数据的,我们现在还无法验证发送收的部分,那么,我们先来看看发送信号的高级用法后,我们再来看看如何通过信号来携带数据吧。
5.2 信号发送函数 :sigqueue
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
- 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
- sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。
sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。
但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX
的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。
那么我们来尝试一下,发送信号如何携带信息
send.c
#include <stdio.h>
#include <signal.h>
//int sigqueue(pid_t pid, int sig, const union sigval value);
int main(int argc,char **argv)
{
int signum;
int pid;
printf("%d\n",getpid());
signum = atoi(argv[1]); //atoi() 将argv[i]的字符串,转换为 整型数
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 100;
sigqueue(pid,signum,value); //参数1:进程pid,参数2:信号值,参数3:你要发送的信息
printf("done\n");
return 0;
}
get.c
#include <signal.h>
#include <stdio.h>
// int sigaction(int signum, const struct sigaction *act,
// struct sigaction *oldact);
//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum,siginfo_t *info,void *context) //参数1:信号值,参数2:内容,参数3:是否为NULL决定有没有内容
{
printf("get signum %d\n",signum);
if(context != NULL){ //非空,代表有内容
printf("get data = %d\n",info->si_int);
printf("get data = %d\n",info->si_value.sival_int);
printf("from = %d\n",info->si_pid);
}
}
int main()
{
struct sigaction act;
act.sa_sigaction = handler; //调用handler函数处理信号
act.sa_flags = SA_SIGINFO; //要想接收信号数据,必须指定这个flag 是 SA_SIGINFO
printf("%d\n",getpid());
sigaction(SIGUSR1,&act,NULL); //参数1:接收哪个信号,参数2:收到信号后想怎么处理它,参数3:备份原信号的操作。一般设置为NULL,不关心
while(1);
return 0;
}