10.2 信号基本概念
信号本质是在软件层次上对中断机制的一种模拟,即软件中断;在原理上,一个进程收到一个信号或处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。
信号事件的发生有两个来源:
(1)硬件来源(比如按下了键盘一些信号指令(比如ctrl + c)或者其它硬件故障);
(2)软件来源,最常用发送信号的系统函数是 kill,raise,alarm 和setitimer 以及sigqueue 函数,软件来源还包括一些非法运算等操作。
我们可以通过指令 kill -l 查看自己系统的信号,如下显示:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号的分类
不可靠信号:
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做"不可靠信号",信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是:信号的错误处理、信号可能丢失。因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
可靠信号(实时信号):
可靠信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction(),可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation() 以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
注意:信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。信号值小于SIGRTMIN的信号为不可靠信号,信号值在SIGRTMIN及SIGRTMAX之间的信号为可靠信号。
信号产生
产生信号的条件主要有:
(1)用户在终端按下某些键时,引发终端产生的信号,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-\产生 SIGQUIT 信号,Ctrl-Z 产生 SIGTSTP 信号。
(2)硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
(3)进程调用 kill(2) 函数可将信号发送给另一个进程或进程组。
用户调用 kill(1) 命令将信号发送给其他进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。
(4)当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。
信号的处理
进程可以通过三种方式来响应一个信号,采用下述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数:
(1)忽略信号,即对信号不做任何处理,其中SIGKILL及SIGSTOP两个信号不能忽略;
(2)捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数;
(3)执行缺省操作,Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是进程终止。
信号与中断
信号与中断的相似点:
1. 采用了相同的异步通信方式;
2. 当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
3. 都在处理完毕后返回到原来的断点;
4. 对信号或中断都可进行屏蔽。
信号与中断的区别:
1. 中断有优先级,而信号没有优先级,所有的信号都是平等的;
2. 信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
3. 中断响应是及时的,而信号响应通常都有较大的时间延迟。
10.3 signal 函数
/* signal 函数*/
/*
* 函数功能:信号处理机制;
* 返回值:若成功则返回信号以前的处理配置,若出错则返回SIG_ERR;
* 函数原型:
*/
#include <signal.h>
void (*signal (int signo, void(*func)(int))) (int);
/*
* 说明:
* signo是信号名;
* func是常量值SIG_IGN、SIG_DFL或当接到此信号后要调用的函数的地址,
* 若指定常量SIG_IGN,则向内核表示忽略此信号(SIGKILL和SIGSTOP这两个信号不能忽略);
* 若指定常量SIG_DFL,则表示接到此信号后的动作是系统默认动作;
* 当指定函数地址时,则在信号发生时,调用该函数;
*
* signal函数需要两个参数,返回一个函数指针,而该指针所指向的函数无返回值;
* 也就是说signal函数的返回值是一个函数地址,该函数有一个参数;
* 可以简化上面的函数形式表达:
*/
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc*);
测试程序:
#include "apue.h"
#include <signal.h>
static void sig_sur(int signo);
int main(void)
{
if(signal(SIGUSR1 , sig_sur) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if(signal(SIGUSR2 , sig_sur) == SIG_ERR)
err_sys("can't catch SIGUSR2");
for(;;)
{
pause();
printf("pause.\n");
}
}
static void sig_sur(int signo)
{
if(SIGUSR1 == signo)
printf("recevied SIGUSR1.\n");
else if(SIGUSR2 == signo)
printf("recevied SIGUSR2.\n");
else
err_dump("recevied signal %d.\n",signo);
}
输出结果:
[root@www chapter_10]# ./a.out & //&表示在后台启动进程
[1] 5504
[root@www chapter_10]# kill -USR1 5504 //向进程发送SIGUSR1信号
recevied SIGUSR1.
pause.
[root@www chapter_10]# kill -USR2 5504
recevied SIGUSR2.
pause.
[root@www chapter_10]# kill 5504 //向进程发送SIGTERM 信号(系统默认终止信号)
[root@www chapter_10]# jobs
[1]+ 已终止 ./a.out
[root@www chapter_10]#
该程序是对信号SIGUSR1 和SIGUSR2进行捕捉,其他信号采用默认的终止进程。
10.9 kill 函数和 raise 函数
kill 函数是将信号发送给进程或进程组,raise 函数是将信号发送给自身,他们定义及说明如下所示:
/* 信号发送与捕获 */
/*
* 函数功能:将信号发送给进程或进程组;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <signal.h>
int kill(pid_t pid, int signo);
/*说明:
* signo 是信号类型;
* pid有以下四种情况:
* (1)、pid >0,将该信号signo发送给进程ID为pid的进程;
* (2)、pid ==0,将该信号signo发送给与发送进程属于同一进程组的所有进程,
* 而且发送进程具有向这些进程发信号的权限;
* (3)、pid <0,将该信号signo发送给其进程组ID等于pid绝对值,而且发送进程具有
* 其所发送信号的权限;
* (4)、pid ==-1,将该信号signo发送给发送进程有权限向他们发送信号的系统上的所有进程;
*/
/*
* 函数功能:向进程自身发送信号;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
int raise(int signo);
/*
* raise(signo)等价于kill(getpid(),signo);
*/
超级用户可以将信号发送给任一进程,对于非超级用户,其基本规则是发送者的实际或者有效用户ID必须等于接受者的实际或者有效用户ID。但也有一个特例: 如果被发送的信号是 SIGCONT ,则进程可以将它发送给属于同一会话的任何进程。
如果 signo 参数为 0(POSIX.1将其定义为空信号),kill仍执行正常的错误检查,但不发送信号。这常被用来检测某一进程是否存在。如果向一个不存在的进程发送信号,则 kill 返回 -1,并将 errno 设置为 ESRCH。但是,UNIX系统在经过一段时间后会重新使用进程ID,所以一个现有的具有所给定进程ID的进程可能并不是你真正想检测的进程。
10.10 alarm 函数和pause 函数
使用 alarm 函数可以设置一个计时器,在将来某个指定的时间该计时器会超时。当计时器超时时,产生SIGALRM信号,如果不忽略或捕捉此信号,则其默认动作是终止调用该 alarm 函数的进程。每个进程只能有一个闹钟。如果在调用alarm 时,已经为该进程设置过闹钟,而且它还没有超时,则将该闹钟的剩余时间值作为本次alarm 调用的返回值。以前的闹钟则被新值代替。
pause 函数使调用进程挂起直到捕捉到一个信号。只有执行一个信号处理程序并从中返回时,pause 才返回。
/*
* 函数功能:设置进程的闹钟时间;
* 返回值:0或以前设置的闹钟时间余留的秒数;
* 函数原型:
*/
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
/*
* 说明:
* 参数seconds是秒数,经过指定的秒数seconds后会产生信号SIGALRM;
* 若进程之前登记过的闹钟尚未超过闹钟时间,而且本次seconds为0时,
* 则取消之前的闹钟时钟,其余留值仍作为alarm函数的返回值;
*/
int pause(void);//使调用进程挂起直到捕获到一个信号;
/*
* 只有执行了一个信号处理程序并从其返回时,pause才返回;
* 在这种情况下,pause返回-1,并将errno设置为EINTR;
*/
程序测试:
#include "apue.h"
#include <signal.h>
#include <unistd.h>
static void sig_alarm(int signo)
{
printf("Recevied alarm.\n");
}
static void sig_kill(int signo)
{
printf("Recevied kill.\n");
}
int main(void)
{
int nsecs = 5;
if(signal(SIGALRM, sig_alarm) == SIG_ERR)
return(nsecs);
signal(SIGHUP,sig_kill);
printf("kill()\n");
kill(getpid(),SIGHUP);
printf("alarm()\n");
alarm(nsecs);
printf("pause.\n");
pause();
printf("raise().\n");
raise(SIGHUP);
exit(0);
}
输出结果:
[root@www chapter_10]# ./10-2
kill()
Recevied kill.
alarm()
pause.
Recevied alarm.
raise().
挂起
[root@www chapter_10]#
10.11 信号集
信号集是表示多个信号的数据类型,这里的信号集数据类型是 sigset_t,包含五个处理信号集的函数:
/* 信号集 */
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化由set所指向的信号集,清空信号集;
int sigfillset(sigset_t *set);//初始化由set所指向的信号集,使其包括所有信号;
int sigaddset(sigset_t *set, int signo);//把指定的信号signo添加到由set所指的信号集中;
int sigdelset(sigset_t *set, int signo);//把指定的信号signo从由set所指定的信号集中删除;
//前面四个函数返回值:若成功则返回0,若出错则返回-1;
int sigismember(const sigset_t *set, int signo);//判断指定的信号signo是否在由set所指的信号集中;
//返回值:若为真则返回1,若为假则返回0,若出错则返回-1;
/*
* 说明:
* 所有应用程序使用信号集之前,要对该信号集调用sigemptyset或sigfillset一次;
*/
10.12 sigprocmask 函数
在前面我们提到,task_struct 结构有一个blocked 成员(我们称之为“信号屏蔽字”),它指定了进程阻塞的信号,被阻塞的信号将不能被递送给进程,直到进程解除阻塞。在信号被阻塞时,内核将其放置到待决列表上。如果同一个信号在阻塞期间被发送了多次,则在待决列表中只放置一次。也就是说,不管发送了多少相同的信号,在进程删除阻塞后,都只会接收到一个信号。调用函数sigprocmask 可以检测或更改其信号屏蔽字。在调用 sigprocmask 后如果有任何未决的、不再阻塞的信号,则在 sigprocmask 返回前,至少会将其中一个信号递送给该进程。
/* sigprocmask 函数 */
/*
* 函数功能:检查或更改信号屏蔽字,也可同时执行这两个操作;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
/*
* 说明:
* 若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回;
* 若set是非空指针,则参数how指示如何修改当前信号屏蔽字;
* 若set是空指针,则不改变该进程的信号屏蔽字,how的值就没有意义;
* 参数how可选以下值:
* (1)SIG_BLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号; (信号屏蔽值变为两个集合的并集)
* (2)SIG_UNBLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集补集的交集;set包含我们希望解除阻塞的附加信号; (信号屏蔽值变为oset – set 内的)
* (3)SIG_SETMASK 该进程新的信号屏蔽字将被set指向的信号集的值所代替; (此时与oset无关,即用set作为新的信号屏蔽值代替老的oset)
* SIG_BLOCK是"或"操作,而SIG_SETMASK则是赋值操作;
* 注意:SIGKILL 和 SIGSTOP 信号是不能阻塞的;
*/
10.13 sigpending 函数
sigpending 函数返回信号集,其中的各个信号对于调用进程是阻塞的而不能传递,该信号集通过set参数返回。(即把阻塞信号集放入set中)
/* sigpending函数 */
/*
* 函数功能:返回信号集;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <signal.h>
int sigpending(sigset_t *set);
测试程序:
#include "apue.h"
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
static void sig_quit(int signo);
int main()
{
sigset_t newmask,oldmask,pendmask;
if(signal(SIGQUIT,sig_quit) == SIG_ERR)
{
err_sys("signal() error");
exit(-1);
}
//初始化信号集
sigemptyset(&newmask);
//添加一个SIGQUIT信号
sigaddset(&newmask,SIGQUIT);
//将newmask信号集设置为阻塞,原信号集保存在oldmask中 ,新的信号集变为原来的加上SIGQUIT
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) == -1)
{
err_sys("SIG_BLOCK error");
exit(-1);
}
sleep(5);
//获取阻塞的信号集
if(sigpending(&pendmask) == -1)
{
err_sys("sigpending() error");
exit(-1);
}
//判断SIGQUIT是否是阻塞的
if(sigismember(&pendmask,SIGQUIT))
printf("\nSIGQUIT is pending.\n");
//恢复原始的信号集
if(sigprocmask(SIG_SETMASK,&oldmask,NULL) == -1)
{
err_sys("SIG_SETMASK error");
exit(-1);
}
printf("SITQUIT unblocked\n");
sleep(5);
exit(0);
}
static void sig_quit(int signo)
{
printf("caught SIGQUIT.\n");
if(signal(SIGQUIT,SIG_DFL) == SIG_ERR)
{
err_sys("can't reset SIGQUIT");
exit(-1);
}
}
输出结果:
$ ./sigset
^\ //键盘输入是 ctrl + \ ,中文状态下终端显示不出来
SIGQUIT is pending.
caught SIGQUIT.
SITQUIT unblocked
$ ./sigset
^\^\^\^\^\^\
SIGQUIT is pending.
caught SIGQUIT.
SITQUIT unblocked
在程序第二次 sleep 时,产生了多个SIGQUIT 信号,此时被 pending,解除了 mask 后,只产生了一次action,也说明了在同一时刻产生多次同一种信号,不会对信号排队。
10.14 sigaction 函数
sigaction 函数的功能与 signal 类似,用于检查或修改与指定信号相关联的处理动作,一般在应用中使用 sigaction 函数代替signal函数。
/* sigaction函数 */
/*
* 函数功能: 检查或修改与指定信号相关联的处理动作;此函数取代之前的signal函数;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
/*
* 说明:
* signo是要检查或修改其具体动作的信号编号;
* 若act非空,则要修改其动作;
* 若oact指针非空,则系统经由oact指针返回该信号上一个动作;
*
* struct sigaction 结构如下:
*/
struct sigaction
{
void (* sa_handler)(int); /* addr of signal handler, or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
/*
* 说明:
* 当更改动作时,若sa_handler 字段包含一个信号捕捉函数的地址,则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,
* 这一信号集要加到进程的信号屏蔽字中。仅当从信号捕获函数返回时再将进程的信号屏蔽字复位为原先值;
* 其中sa_flags标志如下:
* (1)SA_INTERRUPT 由此信号中断的系统调用不会自动重启;
* (2)SA_NOCLDSTOP 若signo是SIGCHLD,当子进程停止时,不产生此信号,当子进程终止时,仍然产生此信号;若已设置此标志,则当停止的进程继续运行时,不发送SIGCHLD信号;
* (3)SA_NOCLDWAIT 若signo是SIGCHLD,当子进程停止时,不创建僵死进程;若调用进程后面调用wait,则调用进程阻塞,直到其所有子进程都终止,此时返回-1,并将errno设置为ECHILD;
* (4)SA_NOEFER 当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号;
* (5)SA_NOSTACK 若用signaltstack(2)声明了一替换栈,则将此信号传递给替换栈上的进程;
* (6)SA_RESETHAND 在此信号捕捉函数入口处,将此信号的处理方式复位为SIG_DEL,并清除SA_SIGINFO标志;
* (7)SA_RESTART 由此信号中断的系统调用会自动重启动;
* (8)SA_SIGINFO 此选项对信号处理程序提供附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针;
*/
sa_sigaction 字段是一个替代的信号处理程序,当在 sigaction 结构中使用了 SA_SIGINFO 标志时,使用该信号处理程序。通常,按下列方式调用信号处理程序:
void handler(int signo);
如果设置了 SA_SIGINFO 标志,那么按照下列方式调用信号处理程序:
void handler(int signo, siginfo_t *info, void *context);
//siginfo 的结构如下:
struct siginfo{
int sig_signo; //信号编号
int sig_errno; //如果不是0,就是errno.h中的errno值
int sig_code; //附加信息(取决于信号)
pid_t si_pid; //发送信号的进程ID
uid_t sig_uid; //发送信号的进程实际用户ID
void *si_addr; //产生错误的地址
int si_status; //退出值或者信号值
long si_band; //SIGPOLL的band号
//可能还会有其他的值。
}
下面可以 sigaction 函数实现 signal 函数的功能:
#include "apue.h"
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
/* 设置信号处理函数 */
act.sa_handler = func;
/* 初始化信号集 */
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(signo == SIGALRM)
{/* 若是SIGALRM信号,则系统不会自动重启 */
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else
{/* 其余信号设置为系统会自动重启 */
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
/* 调用 sigaction 函数 */
if(sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
测试程序:
#include <sys/types.h>
#include "apue.h"
#include <signal.h>
static Sigfunc *Msignal(int signo, Sigfunc *func);
static void sig_func(int signo);
int main(void)
{
if(Msignal(SIGALRM,sig_func) == SIG_ERR)
err_sys("SIGALRM error");
if(Msignal(SIGHUP,sig_func) == SIG_ERR)
err_sys("SIGHUP error");
printf("kill...\n");
kill(getpid(),SIGHUP);
printf("alarm...\n");
alarm(5);
pause();
printf("exit.\n");
exit(0);
}
static void sig_func(int signo)
{
if(SIGHUP == signo)
printf("Recevied kill.\n");
else if(SIGALRM == signo)
printf("Recevied alarm.\n");
else
printf("Recevied others.\n");
}
static Sigfunc *Msignal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
/* 设置信号处理函数 */
act.sa_handler = func;
/* 初始化信号集 */
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(signo == SIGALRM)
{/* 若是SIGALRM信号,则系统不会自动重启 */
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else
{/* 其余信号设置为系统会自动重启 */
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
/* 调用 sigaction 函数 */
if(sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
输出结果:
[root@www chapter_10]# ./a.out
kill...
Recevied kill.
alarm...
Recevied alarm.
exit.
[root@www chapter_10]#