信号是软件中断,它提供了一种处理异步事件的方法,需要告诉内核在某个信号出现时按照下面三种方式之一进行处理:忽略此信号、捕捉信号、执行系统默认动作。 在利用信号进行进程间通信之前,先介绍下信号及其常用处理函数。
哪些情况会引发信号?
1.键盘事件 ctrl +c ctrl +\
2.非法内存 如果内存管理出错,系统就会发送一个信号进行处理
3.硬件故障 同样的,硬件出现故障系统也会产生一个信号
4.环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统
“kill -l”这个命令可以查看所有的信号,现在信号已经增加到65个了,其中从33-64这些信号一般不会采用,这是为了区分可靠信号(34-64重新设计的一套信号集合 ,不会出现信号丢失,支持排队,信号处理函数执行完毕,不会恢复成缺省处理方式,实时信号是可靠信号,非实时信号不可靠信号)和不可靠信号(1.信号处理函数执行完毕,信号恢复成默认处理方式(Linux已经改进) ;2.会出现信号丢失,信号不排队;1-31 都是不可靠的,会出现信号丢失现象 )而新增加的32个信号。
“kill -信号值 pid”发送信号给指定进程。
1、signal函数
声明:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
第一个参数signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler:描述了与信号关联的动作,它可以取以下三种值:
(1)SIG_IGN 表示忽略该信号
(2)SIG_DFL 表示恢复对信号的系统默认处理
(3)sighandler_t类型的函数指针
当接收到一个类型为sig的信号时,就执行handler 所指定的函数。(int)signum是传递给它的唯一参数。执行了signal()调用后,
进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行handler函数。
返回值:
成功返回函数地址,该地址为此信号上一次注册的信号处理函数地址,如果有错误则返回SIG_ERR(-1)。
注意:当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。
示例:
int main(void){
if(signal(SIGUSR1, sig_usr) == SIG_ERR){
...
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR){
...
}
for(;;)
pause();
}
void sig_usr(int signo){ /*argument is signal number*/
if(signo == SIGUSR1){
...
}
else if(signo == SIGUSR2){
...
}
else{
...
}
}
2、sigaction函数
signal 函数的使用方法简单,但并不属于 POSIX 标准,在各类 UNIX 平台上的实现不尽相同,因此其用途受到了一定的限制。而 POSIX 标准定义的信号处理接口是 sigaction 函数。
头文件:#include <signal.h>
定义: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum:要操作的信号。
act:要设置的对信号的新处理方式。struct sigaction类型用来描述对信号的处理,定义如下:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
成员 sa_handler 是一个函数指针,其含义与 signal 函数中的信号处理函数类似。
成员 sa_sigaction 则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
当 sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号
处理函数,否则使用 sa_handler 作为信号处理函数。在某些系统中,成员 sa_handler 与
sa_sigaction 被放在联合体中,因此使用时不要同时设置。
sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它
自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。
sa_flags 成员用于指定信号处理的行为,它可以是以下值的“按位或”组合。
SA_RESTART:使被信号打断的系统调用自动重新发起。
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
re_restorer 成员则是一个已经废弃的数据域,不要使用。
oldact:原来对信号的处理方式。
返回值:0表示成功,-1表示有错误发生。
示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
static void sig_usr(int signum)
{
if(signum == SIGUSR1)
{
printf("SIGUSR1 received\n");
}
else if(signum == SIGUSR2)
{
printf("SIGUSR2 received\n");
}
else
{
printf("signal %d received\n", signum);
}
}
int main(void)
{
char buf[512];
int n;
struct sigaction sa_usr;
sa_usr.sa_flags = 0;
sa_usr.sa_handler = sig_usr; //信号处理函数
sigaction(SIGUSR1, &sa_usr, NULL);
sigaction(SIGUSR2, &sa_usr, NULL);
printf("My PID is %d\n", getpid());
while(1)
{
if((n = read(STDIN_FILENO, buf, 511)) == -1)
{
if(errno == EINTR)
{
printf("read is interrupted by signal\n");
}
}
else
{
buf[n] = '\0';
printf("%d bytes read: %s\n", n, buf);
}
}
return 0;
}
输出:
My PID is 5904
SIGUSR1 received (从另外一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:kill -USR1 5904)
read is interrupted by signal
由上可见,sigaction 注册信号处理函数时,不会自动重新发起被信号打断的系统调用。如果需要自动重新发起,则要设置 SA_RESTART 标志,比如在上述例程中可以进行类似以下的设置:sa_usr.sa_flags = SA_RESTART;
3、sigsetjmp和siglongjmp函数
在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。但是,调用longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序(仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值)。如果用longjmp跳出信号处理程序,那么,对此进程的信号屏蔽字会发生什么呢?setjmp和longjmp保存和恢复信号屏蔽字,还是不保存和恢复,不同的实现各有不同。
POSIX.1并没有说明setjmp和longjmp对信号屏蔽字的作用,而是定义了两个新函数sigsetjmp和siglongjmp。在信号处理程序中进行非局部转移时使用这两个函数。
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask); 返回值:若直接调用则返回0,若从siglongjmp调用返回则返回非0值
void siglongjmp(sigjmp_buf env, int val);
如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0 savemask的sigsetjmp调
用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
示例(摘自APUE):
#include "apue.h"
#include <setjmp.h>
#include <time.h>
static void sig_usr1(int), sig_alrm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump;
//sig_atomic_t,这是由ISO C标准定义的变量类型,在写这种类型的变量时不会被中断。
//这种类型的变量总是包括ISO类型修饰符voaltile。
int main(void)
{
if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
pr_mask("starting main: ");
if (sigsetjmp(jmpbuf, 1))
{
pr_mask("ending main: "); // pr_mask打印调用进程的信号屏蔽字中信号的名称
exit(0);
}
canjump = 1; /* now sigsetjmp() is OK */
for(; ;)
pause();
}
static void sig_usr1(int signo)
{
time_t starttime;
if (canjump == 0)
return; /* unexpected signal, ignore */
pr_mask("starting sig_usr1: ");
alarm(3); /* SIGALRM in 3 seconds */
starttime = time(NULL);
for(; ;) /* busy wait for 5 seconds */
if (time(NULL) > starttime + 5)
break;
pr_mask("finishing sig_usr1: ");
canjump = 0;
siglongjmp(jmpbuf, 1); /* jump back to main, don't return */
}
static void sig_alrm(int signo)
{
pr_mask("in sig_alrm: ");
}
输出:
$ ./a.out &
starting main
[1] 531
$ kill -USR1 531 向该进程发送SIGUSR1
starting sig_usr1: SIGUSR1
$ in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
4、可重入函数
可重入函数是指函数可以由多个任务并发使用,而不必担心数据错误。
编写可重入函数:
a.不使用(返回)静态的数据、全局变量(除非用信号量互斥);
b.不调用动态分配、释放的函数;
c.不调用任何不可重入的函数(如标准I/O函数)。
d.进行了浮点运算,许多处理器/编译器中,浮点一般都是不可重入的,浮点运算大多使用协处理器或者软件模拟来实现。
注:即使信号处理函数使用的都是可重入函数,也要注意进入处理函数时,首先保存errno变量的值,结束时,再恢复原值。因为信号处理过程中,errno的值随时可能被改变。(这里有点不理解!改变就改变了呗,有什么问题?) 一种说法:因为每个线程只有一个errno变量,信号处理函数可能会修改其值,要了解经常被捕捉到的信号是SIGCHLD(在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号),其信号处理程序通常要调用一种wait函数,而各种wait函数都能改变errno。常见可重入函数如下:
5、信号集
一种能够表示多个信号——信号集的数据类型。通常信号种类数目可能超过一个整形量所包含的位数,所以不能用一个整形量的其中一位代表一种信号,也就不能用一个整形量表示信号集。POSIX.1定义了数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数。
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化当前的信号集,并将所有信号排除在外
int sigfillset(sigset_t *set); //将所有信号的信号集设置为满
int sigaddset(sigset_t *set, int signum);//往信号集中增添信号
int sigdelset(sigset_t *set, int signum);//从信号集中删除某个信号
以上成功返回0,出错返回-1
int sigismember(const sigset_t *set, int signum);//检测信号是否在信号集中
若真返回1,若假返回0,出错返回-1
6、信号阻塞集
每个进程都有一个阻塞集,它用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
比如,当前2号、4号、7号和11号信号在阻塞集中,则此时发送这4个信号过来,进程不会响应,但是这些发送过来的信号会排队。以4号信号为例,当4号信号从阻塞集中去除后,排队的信号就会被重新响应。但是需要注意的是,阻塞期间同一个信号多次到来只会有一次进行排队,即4号信号目前已经在排队了,那后续收到不管多少次该信号,都不会重复进入到排队序列中。因此,阻塞集合中的信号不重复。
(1)sigprocmask函数
#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
返回值:若成功则返回0,若出错则返回-1
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
如果set是空指针,则不改变该进程的信号屏蔽字,how的值也无意义。
注意:
a.不能阻塞SIGKILL和SIGSTOP信号。
b.如果调用sigprocmask解除了对当前若干个未决信号的阻塞,且这若干个信号中有信号在阻塞期间到达了,
则在sigprocmask返回前,至少会将其中一个信号递送给进程。
how 说明
SIG_BLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号
SIG_UNBLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。set包含了我希望解除阻塞的信号
SIG_SETMASK 该进程新的信号屏蔽字将被set指向的信号集的值代替
(2)sigpending函数
sigpending返回信号集,其中的各个信号对于调用进程是阻塞的而不能递送,因而也一定是当前未决的,该信号集通过set参数返回。
#include <signal.h>
int sigpending(sigset_t *set); //读取当前进程的未觉信号集
成功返回0,失败返回-1
示例:
void sig_quit(int signo){
printf("caught SIGQUIT\n");
if(signal(SIGQUIT, SIG_DFL) == SIG_ERR){
printf("can't reset SIGQUIT");
}
}
int main(void){
sigset_t newmask, oldmask, pendmask;
if(signal(SIGQUIT, sig_quit) == SIG_ERR)
printf("can't catch SIGQUIT");
//Block SIGQUIT and save current signal mask
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){
printf("SIG_BLOCK error");
}
sleep(5); //SIG_QUIT here will remain pending
if(sigpending(&pendmask) < 0){
printf("sigpending error");
}
if(sigismember(&pendmask, SIG_QUIT)){
printf("\nSIG_QUIT pending\n");
}
//Reset signal mask which unblocks SIG_QUIT
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0){
printf("SIG_SETMASK error");
}
printf("SIGQUIT unblocked\n");
sleep(5);
exit(0);
}
输出:
./a.out
^\ 产生信号一次(5秒钟之内)
SIG_QUIT pending 从sleep返回后
caught SIGQUIT 在信号处理程序中
SIGQUIT unblocked 从sigprocmask返回后
^\Quit(coredump) 再次产生信号
./a.out
^\^\^\^\^\^\^\^\ 产生信号多次(5秒钟之内)
SIG_QUIT pending
caught SIGQUIT 只递送一次(未排队)
SIGQUIT unblocked
^\Quit(coredump)
7、sigsuspend函数
考虑下面一段代码:
sigemptyset(&new);
sigaddset(&new, SIGINT);
sigprocmask(SIG_BLOCK, &new, &old); //将SIGINT信号阻塞,同时保存当前信号集
printf("Blocked");
sigprocmask(SIG_SETMASK, &old, NULL); //取消阻塞
pause();
return 0;
本来期望pause()之后,来SIGINT信号,可以结束程序;可是,如果当“取消阻塞”和“pause”之间,正好来了SIGINT信号,结果程序因为pause的原因会一直挂起。为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由sigsuspend函数提供的。
#include <signal.h>
int sigsuspend( const sigset_t *sigmask );
返回值:-1,并将errno设置为EINTR
将进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且将该进程的信号屏蔽字设置为调用sigsuspend之前的值。此函数没有成功返回值,如果它返回到调用者,则总是返回-1,并将errno设置为EINTR(表示一个被中断的系统调用)。
示例:
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void handler(int sig) //信号处理程序
{
if(sig == SIGINT)
printf("SIGINT sig");
else if(sig == SIGQUIT)
printf("SIGQUIT sig");
else
printf("SIGUSR1 sig");
}
int main()
{
sigset_t new,old,wait; //三个信号集
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0); //可以捕捉以下三个信号:SIGINT/SIGQUIT/SIGUSR1
sigaction(SIGQUIT, &act, 0);
sigaction(SIGUSR1, &act, 0);
sigemptyset(&new);
sigaddset(&new, SIGINT); //SIGINT信号加入到new信号集中
sigemptyset(&wait);
sigaddset(&wait, SIGUSR1); //SIGUSR1信号加入wait
sigprocmask(SIG_BLOCK, &new, &old); //将SIGINT阻塞,保存当前信号集到old中
//临界区代码执行
//程序在此处挂起;用wait信号集替换new信号集。即:过来SIGUSR1信号,阻塞掉,程序继续挂起;过来其他信号,
//例如SIGINT,则会唤醒程序,执行sigsuspend的原子操作。注意:如果“sigaddset(&wait, SIGUSR1);”这句没有,
//则此处不会阻塞任何信号,即过来任何信号均会唤醒程序。
if(sigsuspend(&wait) != -1)
printf("sigsuspend error");
printf("After sigsuspend");
sigprocmask(SIG_SETMASK, &old, NULL);
return 0;
}
sigsuspend的原子操作是:
(1)设置新的mask阻塞当前进程(上面是用wait替换new,即阻塞SIGUSR1信号);
(2)收到SIGUSR1信号,阻塞,程序继续挂起;收到其他信号,恢复原先的mask(即包含SIGINT信号的);
(3)调用该进程设置的信号处理函数;
(4)待信号处理函数返回,sigsuspend返回。(sigsuspend将捕捉信号和信号处理函数集成到一起了)
8、kill函数
头文件:
#include <sys/types.h>
#include <signal.h>
定义:
int kill(pid_t pid, int sig);
参数:
pid可能选择有以下四种:
1\. pid大于零时,pid是信号欲送往的进程的标识。
2\. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个进程组的进程,而且发送进程具有
向这些进程发送信号的权限。注意:这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数
UNIX系统,系统进程集包括内核进程和init(pid 1).
3\. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
4\. pid小于-1时,信号将送往以-pid为组标识的进程。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用
sig值为零来检验某个进程是否仍在执行。如果向一个并不存在的进程发送空信号,则kill返回-1,并将
errno设置为ESRCH。但是对于进程是否存在的这种测试并非原子操作。在kill向调用者返回测试结果时,
原来存在的被测试进程此时可能已经终止。反之亦然。
返回值:
成功执行,返回0
失败返回-1,errno被设为以下的某个值
- EINVAL:指定的信号码无效(参数 sig 不合法)
- EPERM;权限不够无法传送信号给指定进程
- ESRCH:参数 pid 所指定的进程或进程组不存在
注意:
使用kill函数发送信号给进程或者进程组,进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际或者有效用户ID必须等于接收者的实际或者有效用户ID。另外,“杀死”这个术语是不恰当的,kill只是将一个信号发送给一个进程或者进程组,进程是否终止取决于信号类型以及进程是否安排了捕捉该信号。
9、alarm函数
头文件: #include <unistd.h>
函数原型: unsigned int alarm(unsigned int seconds);
函数说明: 主要功能是设置信号传送闹钟,即用来设置信号SIGALRM(由内核产生)在经过参数seconds秒
数后发送给目前的进程。如果未设置信号SIGALRM的处理函数,那么alarm()默认处理终止进程。
返回值: 返回0或者以前设置的闹钟剩余秒数
注意:
每个进程只能有一个闹钟时钟。如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置,且前面闹钟的剩余秒数将作为本次ALARM调用的返回值。当参数seconds为0时,之前设置的定时器闹钟将被取消,并将剩下的时间返回。如果想要捕捉信号SIGALRM,则最好在调用alarm函数之前设置该信号的处理程序。
10、pause函数
头文件: #include <unistd.h>
函数原型: int pause(void);
函数说明: 只有执行了一个信号处理程序并从其返回时,pause才返回,也即将调用进程挂起直至捕捉到
信号为止。在这种情况下,pause返回-1,并将errno设置成EINTR。这个函数通常用于判断信号是否已到。
返回值: -1,并将errno设置成EINTR。
11、raise函数
头文件: #include <signal.h>
函数原型: int raise(int signo);
函数功能: 向进程本身发送一个信号,相当于 kill(getpid(), sig)
函数参数: signo:要发送的信号值
返回值: 成功返回0,出错返回-1
12、abort函数
头文件: #include <stdlib.h>
函数原型: void abort(void);
功能: 向进程发送一个SIGABRT信号,默认情况下进程会退出。
注意: 即使SIGABRT信号被加入阻塞集,一旦进程调用了abort函数,进程也还是会被终止,且在终止前会刷新缓冲区,关闭文件描述符。