1. 信号概述
在Linux这个多用户多进程的系统中,信号的存在是必然的。信号可以理解为一个软中断,在某个条件下,系统会发出某个信号给正在运行的进程,通知进程需要执行某一特定的事件。
1.1 在终端中查看常见的信号
在终端输入命令"kill -l",可以看出Linux系统中的所有信号。(每个信号类型前面都有一个正整数,这个正整数与信号代表相同的含义,称之为信号编号)。
信号的宏定义和编号都定义在signal.h的头文件中。在终端中可以通过输入命令"man 7 signal"查看Linux系统中支持的信号的详细含义。
下图为Linux支持的所有的信号:
1.2 信号处理
信号作为一种进程间通信的机制,主要用于处理异步事件。通常,如果没有信号发送到正在执行的进程中,进程会有如下3种处理信号的方法:
(1) 默认信号的处理方法。系统为每一个信号都设置了默认的处理方法,通常为终止进程。
(2) 捕捉信号,使进程执行指定的程序代码。
(3) 忽略信号,对该信号不做任何处理,进程继续执行。
这三种处理捕捉到的信号的方法只是比较基本的方法。在实际应用中,对信号的处理并不会这么单一。例如,有些进程在执行时不希望被信号突然打断,但是又不希望忽略此信号,此时进程会将该信号挂起,需要时再回头处理它。
2. 产生信号
信号的产生有多种多样,主要有如下几种:
(1) 可以通过键盘终端产生。例如:使用Ctrl+C可以产生SIGINT信号;使用Ctrl+\可以产生SIGQUIT信号;使用Ctrl+Z可以产生SIGSTP信号。
(2) 通过终端中的kill命令产生信号,使用格式如下:
kill -信号类型 进程号
//进程号可以通过命令"ps -aux"获取
信号类型可以输入信号的编号,也可以输入信号的宏定义,例如,命令“kill -SIGTERM 进程号”或者"kill -15 进程号",表示编号为15、宏定义为SIGTERM的信号用来结束指定的进程。
(3) 调用系统函数向进程发送信号
在Linux系统中,kill()、raise()和alarm()函数都可以产生信号。
2.1 kill()函数
前面提到的在终端中通过kill命令产生信号的方法,原理是主要通过kill命令调用了kill()函数实现了这个功能。
kill()函数主要用于向指定的进程或进程组发送信号,该函数的定义如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数pid为进程号或进程组号;参数sig为要发送的信号类型的编号。如果参数sig为0,就没有信号可以发送,但会进行错误检查。
2.2 raise()函数
raise()函数主要用于将信号发送给当前进程,该函数原型:
#include <signal.h>
int raise(int sig);
参数sig为发送的信号类型的编号。
如果函数调用成功,返回0;失败,返回非0.
由raise()函数的功能可以知道,使用kill(getpid(), sig)也可以实现该功能。
2.3 alarm()函数
该函数主要用于为发送的信号设定一个时间警告,使系统在设定的时间之后发送信号,函数原型为:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数seconds为设定的时间值。如果seconds为0,那么alarm()函数设置的警告时钟将无效。
alarm()函数安排在seconds时间后,发送一个信号SIGALRM给进程。在默认的情况下,进程收到SIGALRM
信号会终止运行。如果不希望终止,可以在进程捕捉到该信号后修改默认的处理函数。
调用alarm()函数后,之前设置的任何警告时钟都将取消。
3. 捕捉信号
从前面信号的介绍中了解到有3种对信号的处理方法,一种是系统对信号的默认处理方法;一种是忽略;一种是捕捉信号。其实,对于忽略信号和捕捉信号,都是修改系统默认信号的处理方式。在Linux系统中,可以使用signal()函数和sigaction()函数对默认信号处理方法进行修改。
3.1 signal()函数
signal()函数用于修改某个信号的处理方法,该函数的定义如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
如果signal()函数调用成功,返回先前的信号,并处理调用的函数指针;反之返回SIG_ERR.
参数signum代表信号类型的编号;参数handler代表指向信号新的处理方法的指针,如果指针指向一个函数,那么捕捉到signum信号时,会执行这个特殊函数处理信号;参数handler还可以设置为SIG_IGN或SIG_DFL, SIG_IGN代表忽略该信号,而SIG_DFL代表采取默认的处理方法。
使用一个自己定义的特殊函数作为信号的处理方法,这种处理信号的方法叫做“捕捉信号”。
在系统提供的信号类型中,SIGKILL和SIGSTOP信号不能被捕获或者忽略。
示例代码:signal()函数的使用
#include <stdio.h>
#include <signal.h>
#include <stdarg.h>
void sigint(int sig);
void sigcont(int sig);
int main()
{
char a[100];
if(signal(SIGINT, &sigint)==SIG_ERR)
{
//修改SIGINT信号的处理方法为sigint()函数
perror("sigint signal error!");
}
if(signal(SIGCONT,&sigcont)==SIG_ERR)//修改SIGCONT信号处理方法为sigcont函数
{
perror("sigcont error!");
}
if(signal(SIGQUIT, SIG_IGN)) //修改SIGQUIT信号的处理方法为SIG_IGN
{
perror("sigquit error!");
}
printf("current process is:%d\n\n", getpid()); //获取当前进程ID
while(1)
{
printf("input a:");
fgets(a, sizeof(a), stdin);//获取键盘输入的字符串
if(strcmp(a, "terminate\n")==0) //比较字符串a与terminate字符
{
raise(SIGINT); //若两个字符串相同,则将SIGINT信号发送给当前进程
}
else if(strcmp(a, "continue\n") == 0)
{
raise(SIGCONT);//获取的字符串若与比较的字符串相同,则产生SIGCONT信号给当前进程
}
else if(strcmp(a, "quit\n") == 0)
{
raise(SIGQUIT);
}
else if(strcmp(a,"game over\n") == 0)
{
raise(SIGTSTP);
}
else
{
printf("your input is: %s\n\n", a);
}
}
return 0;
}
void sigint(int sig) //SIGINT信号的新的处理方法
{
printf("SIGINT signal %d.;\n", sig);
}
void sigcont(int sig) //SIGCONT信号的新的处理方法
{
printf("SIGCONT signal %d.;\n", sig);
}
sigaction()函数
sigaction()函数主要用于读取和修改指定信号的处理动作,定义如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
参数signum表示要捕获信号类型的编号;参数act和oldact都是指向sigaction结构体的指针。参数act表示需要修改的指定的新的处理动作,而该信号的原有处理动作保存到参数oldact指向的缓冲区中。(如果act和oldact都指向空,则两个指针参数不会实现上述功能。)
示例代码:
调用sigaction()函数修改SIGINT信号的处理方法,修改为显示接收到的信号编号,并累加计时,直到收到下一个信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int i = 0;
void new_handler(int sig) /*SIGINT信号的新的处理方法*/
{
printf("receive signal number is: %d\n", sig);
for(; i<100; i++) /*每隔一秒累加计时*/
{
printf("sleep2 %d\n", i);
sleep(1);
}
}
int main()
{
struct sigaction newact, oldact;
newact.sa_handler = new_handler; //处理方法
sigaddset(&newact.sa_mask, SIGQUIT); //将SIGQUIT信号加到新的处理方法的屏蔽信号中
newact.sa_flags = SA_RESETHAND | SA_NODEFER;
printf("change SIGINT(2)signal__[ctrl+c]\n");
sigaction(SIGINT, &newact, &oldact);
while(1)
{
sleep(1);
printf("sleep1 %d\n", i);
i++;
}
return 0;
}
4. 信号的阻塞
信号的处理并没有那么简单,有时候进程并不希望被突如其来的信号中断当前的执行,也不希望此信号被忽略,而是希望一段时间后再去处理这个信号。在这里,就要用阻塞信号的方法来实现。
这里有三个系统调用函数,分别是sigprocmask(), sigsuspend(), sigpending()。
说明:信号屏蔽字就是进程中被阻塞的信号集,这些信号不能发送给该进程,它们在该进程中被“屏蔽”了,也就是被阻塞了。
4.1 sigprocmask()函数
可用于检测和改变进程的信号掩码,定义如下:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//该函数只为单线程定义的,在多线程中要使用pthread_sigmask变量,使用之前要声明和初始化。
参数how表示修改信号屏蔽字的方式;newset表示把这个信号集设为新的信号屏蔽字,如果为NULL,则不改变;oldset表示保存进程旧的信号屏蔽字,如果为NULL,则不保存。
how的取值有以下几个:
SIG_BLOCK:代表是将newset所指向的信号集中所包含的信号加到当前的信号掩码中作为新的信号屏蔽字;
SIG_UNBLOCK:将参数newset所指向的信号集中的信号从当前的信号掩码中移除;
SIG_SETMASK: 设置当前信号掩码为参数newset所指向的信号集中所包含的信号。
函数成功调用返回0,失败返回-1.
4.2 sigsuspend()函数
该函数实现的是等待一个信号的到来,即将当前进程挂起,定义如下:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
mask是一个sigset_t结构体类型的指针,指向一个信号集。当该函数被调用时,参数mask所指向的信号集中的信号被复制给信号掩码。随后,进程会被挂起,直到信号被捕捉到,执行信号相应的处理方法返回时,该函数才返回。此时,信号掩码恢复为调用前的值。
4.3 sigpending()函数
在调用信号屏蔽的相关函数后,被屏蔽的信号对于调用进程是阻塞的,不能发送给调用进程,因此是待定(pending)的,而调用该函数可以取得这些阻塞的信号集,定义如下:
#include <signal.h>
int sigpending(sigset_t *set);
set为指向一个信号集的sigset_t 类型的结构体指针。调用sigpending()函数成功时,参数set会取得被悬挂的信号集,返回值为0;若失败,返回-1.
示例代码:调用信号阻塞函数将SIGINT信号阻塞。
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
static void sig_handler(int signo) //自定义的信号SIGINT处理函数
{
printf("信号SIGINT被捕捉!\n");
}
int main()
{
sigset_t new, old, pend;
if(signal(SIGINT, sig_handler) == SIG_ERR) //注册一个信号处理函数sig_handler
{
perror("signal");
exit(1);
}
if(sigemptyset(&new) < 0) //清空信号集
perror("sigemptyset");
if(sigaddset(&new, SIGINT)<0) //向new信号集中添加SIGINT信号
perror("sigaddset");
if(sigprocmask(SIG_SETMASK, &new, &old) < 0) //将信号集new阻塞
{
perror("sigpromask");
exit(1);
}
printf("SIGQUIT被阻塞! \n");
printf("试着按下Ctrl+C,程序会暂停5秒等待处理事件! \n");
sleep(5);
if(sigpending(&pend) < 0) //获得未决的信号类型
perror("sigpending");
if(sigismember(&pend, SIGINT)) //检查信号SIGINT是否为未决的信号类型
printf("SIGINT信号未决!\n");
if(sigprocmask(SIG_SETMASK, &old, NULL) < 0) //恢复为原始的信号掩码,解开阻塞
{
perror("sigpromask");
exit(1);
}
printf("SIGINT已被解开阻塞 \n");
printf("再按下 Ctrl+C \n");
sleep(5);
return 0;
}
运行结果:
该程序中,首先注册了一个SIGINT的信号处理函数,改变了SIGINT信号的默认处理方法,然后阻塞了该信号的处理方法。因此,当在提示下按 Ctrl+C时,系统没有反应,没有捕捉信号的处理方法,通过sigpending()函数获取了未决信号的类型,在调用sigprocmask()函数解开阻塞时,才捕捉到该信号的处理方法,这时再在提示信息下按Ctrl+C时,系统会立即捕捉该信号的处理方法。