信号是由用户、系统或者进程发送给目标进程的信息,用来通知目标进程的状态改变或系统异常。
发送信号
kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
该函数可以把sig信号发送给pid指定的目标进程
pid 参数 | 含义 |
---|---|
pid > 0 | 信号发送给PID为pid的进程 |
pid = 0 | 信号发送给本进程组内的其他进程 |
pid = -1 | 信号发送给除init进程外的所有进程,但发送者需要拥有对目标发送信号的权限 |
pid < -1 | 信号发送给组ID为-pid的进程组中的所有成员 |
特别的如果sig为0的时候,kill函数不发送任何信号。
kill函数成功时返回0,失败返回-1,并设置errno
errno | 含义 |
---|---|
EINVAL | 无效的信号 |
EPERM | 该进程没有权限发送信号给任何一个目标进程 |
ESRCH | 目标进程或进程组不存在 |
alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds表示设定的秒数,经过seconds秒后,内核将给调用函数的进程发送SIGALRM信号。
成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0
出错:-1
raise
#include<signal.h>
int raise(int sig);
sig为要发送的信号,成功返回0,失败返回非0值。
sigqueue
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
该函数可以给指定进程
对信号的处理
进程在收到信号时,需要定义一个接受函数来处理。信号处理函数的原型为
#include<signal.h>
typedef void (*sighandler_t)(int);
唯一带有的int参数用来指示信号类型。用户可以按照该模板自定义信号处理函数。
除了用户自定义的信号处理函数,还有两种其他处理方式——SIG_IGN和SIG_DEL
#include<bits/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
SIG_IGN表示忽略目标信号,SIG_DEL表示使用信号的默认处理方式:结束进程、忽略信号、结束进程并生成核心转储文件、暂停进程,以及继续进程
信号函数
signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum为信号类型,handler为函数指针。返回值为前一次调用signal函数时传入的函数指针,如果是第一次调用则为SIG_DEF。
signal系统调用出错时返回SIG_ERR并设置errno。
sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
signum为信号类型,act为新的信号处理方式,oact为之前的信号处理方式。
struct sigaction
{
#ifdef __USE_POSIX199309
union
{
_sighandler_t sa_handler;
void (*sa_sigaction)(int, siginfo_t*, void*);
}
_sigaction_handler;
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
#else
_sighandler_t sa_handler;
#endif
_sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void)
};
sa_handler和sa_sigaction为共用体,这两个值只有一个生效。
sa_handler为函数指针,指向用户自定义的信号处理函数,也可以是SIG_DFL或SIG_IGN
sa_sigaction也是用来指定signum的处理函数,但是它有三个参数买第一个是信号编号,第二个是指向siginfo_t结构的指针,第三个是指向任何类型的指针,一般不使用。
sa_mask成员声明一个信号集。当某个信号被加入到信号集中后这个信号将不会被程序接收到
例:
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<signal.h>
void handler(int signo)
{
if (signo == SIGINT)
printf("Get SIGINT!\n");
else
printf("Get SIGQUIT!\n");
}
int main()
{
sigset_t sigset, oset; /*sigset存放屏蔽信号,oset保存当前屏蔽信号*/
sigemptyset(&sigset); /*清空信号集*/
sigaddset(&sigset, SIGINT); /*添加SIGINT信号,信号集中仅有SIGINT*/
sigprocmask(SIG_BLOCK, &sigset, &oset); /*加入屏蔽信号*/
signal(SIGINT, handler);
signal(SIGQUIT, handler);
sleep(2);
raise(SIGINT); /*发送SIGINT信号*/
raise(SIGQUIT);
}
我们将SIGINT加入到了信号集当中,我们在给程序发送SIGINT信号的时候程序是接收不到的。这个操作其实为设置进程的信号掩码,我们后面还会再讲。
sa_flags用来说明信号的一些其他操作
选项 | 含义 |
---|---|
SA_BOCLDSTOP | 如果sig的参数为SIGCHLD,则设置该标志表示子进程暂时不生成SIGCHLD信号 |
SA_NOCLDWAIT | 如果sig的参数为SIGCHLD,则设置该标志表示子进程结束时不产生僵尸进程 |
SA_SIGINFO | 如果使用sa_sigaction作为信号处理函数(而不是sa_handler),它给进程提供更多相关的信息(可以传参) |
SA_ONSTACK | 调用由siglstack函数设置的可选信号栈上的信号处理函数 |
SA_RESTART | 重新调用被该信号终止的系统调用 |
SA_NODEFER | 当接收到信号并进入其信号处理函数时,不屏蔽该信号。 |
SA_RESETHAND | 信号处理函数执行完以后恢复信号的默认处理方式 |
SA_INTERRUPT | 中断系统调用 |
SA_NOMASK | 同SA_NODEFER |
SA_ONESHOT | 同SA_RESETGAND |
SA_STACK | 同SA_ONSTACK |
sa_restorer已经作废,一些标准已经不支持该数据成员。
中断系统调用
一些IO系统调用执行时, 如 read 等待输入期间, 如果收到一个信号,系统将中断read, 转而执行信号处理函数. 当信号处理函数返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败?早期UNIX系统的做法是, 中断系统调用, 并让系统调用失败, 比如read返回 -1, 同时设置 errno 为 EINTR中断了的系统调用是没有完成的调用, 它的失败是临时性的, 如果再次调用则可能成功, 这并不是真正的失败, 所以要对这种情况进行处理。
比如:
while (1) {
n = read(fd, buf, BUFSIZ);
if (n == -1 && errno != EINTR) {
printf("read error\n");
break;
}
if (n == 0) {
printf("read done\n");
break;
}
}
也可使用sigaction函数设置SA_RESTART标志以自动重启被该信号中断的系统调用。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <unistd.h>
void sig_handler(int signum)
{
printf("in handler\n");
sleep(1);
printf("==========handler========== return\n");
}
int main(int argc, char **argv)
{
char buf[100];
int ret;
struct sigaction action, old_action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 版本1:不设置SA_RESTART属性
* 版本2:设置SA_RESTART属性 */
//action.sa_flags |= SA_RESTART;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
bzero(buf, 100);
ret = read(0, buf, 100);
if (ret == -1) {
perror("read");
}
printf("read %d bytes:\n", ret);==
printf("%s\n", buf);
return 0;
}
信号集
在之前我们在介绍sigaction的时候,sa_mask的类型为sigset_t。
在Linux中数据结构sigset_t来表示一组信号。其定义如下:
#include<asm-generic/bitsperlong.h>
#define __BITS_PER_LONG 32
#include<signal.h>
#define _NSIG 64
#define _NSIG_BPW __BITS_PER_LONG
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
由定义可知,sigset_t实际上是一个长整形数组,数组的每个元素的每一位表示一个信号。Linux提供如下一组函数来设置、修改、删除和查询信号集。
include<signal.h>
int sigemptyset(sigset* _set) //清空信号集
int sigfillset(sigset* _set) //在信号集设置所有信号
int sigaddset(sigset* _set, int _signo) //将信号_signo添加到信号集中
int sigdelset(sigset* _set, int _signo) //将信号_signo从信号集删除
int sigismember(_const sigset_t* _set, int _signo) //测试_signo是否在信号集中
进程信号掩码
我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码,当设置好信号掩码之后我们可以使用sigprocmask来使得设置生效
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
set参数指定新的信号掩码,oldset参数输入原来的信号掩码(如果不为NULL的话)。如果set参数不为NULL,则how参数指定设置进程信号掩码的方式为:
how参数 | 含义 |
---|---|
SIG_BLOCK | 按照参数 set 提供的屏蔽字,屏蔽信号。并将原信号屏蔽保存到oldset中。 |
SIG_UNBLOCK | 按照参数 set 提供的屏蔽字进行信号的解除屏蔽。针对Set中的信号进行解屏 |
SIG_SETMASK | 按照参数 set 提供的信号设置重新设置系统信号设置。 |
设置进程号掩码之后,被屏蔽的信号将不能被进程接收。
sigprocmask成功时返回0,失败返回-1并设置errno。
被挂起的信号
设置进程信号掩码后,如果给进程发送一个被屏蔽的信号,则该信号会被设置进程的一个被挂起的信号。若我们在后面取消对挂起信号的屏蔽,则它能立即被进程接收到。
例:
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<signal.h>
/*sigprocmsk的使用*/
void msg(int signo)
{
if (signo == SIGINT)
printf("Get SIGINT!\n");
else
printf("Get SIGQUIT!\n");
}
int main()
{
sigset_t sigset, oset; /*sigset存放屏蔽信号,oset保存当前屏蔽信号*/
sigemptyset(&sigset); /*清空信号集*/
sigaddset(&sigset, SIGINT); /*添加SIGINT信号,信号集中仅有SIGINT*/
sigprocmask(SIG_BLOCK, &sigset, &oset); /*加入屏蔽信号*/
signal(SIGINT, msg);
signal(SIGQUIT, msg);
sleep(2);
raise(SIGINT); /*发送SIGINT信号*/
sigprocmask(SIG_UNBLOCK, &sigset, &oset);
raise(SIGQUIT);
}
执行结果为
^C^C^C^C^CGet SIGINT!
Get SIGINT!
Get SIGQUIT!
可知在取消屏蔽后果然生效了。
#include <signal.h>
int sigpending(sigset_t *set);
该函数可以获得进程当前被挂起的信号。set参数用于保存被挂起的信号集。根据上面的例子我们可知:即使进程多次接收到同一个被挂起的信号,该信号的信号处理函数只触发一次,sigpending函数也只反映一次。
sigpending函数成功时返回0,失败时返回-1,并设置errno。
特别的:fork调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起的信号集。
可以传参数的信号处理函数
之前我们有提到过sigaction函数sa_flags的SA_SIGINFO选项,在我们设置了该选项之后,并且使用sa_sigaction来指定信号处理函数时我们便可以给信号处理函数传递参数。其信号处理函数的模板为:
void (*sa_sigaction)(int, siginfo_t*, void*);
其中siginfo_t中的si_int和si_ptr可以用来传递参数给信号处理函数
typedef struct siginfo_t{
int si_signo;//信号编号
int si_errno;//如果为非零值则错误代码与之关联
int si_code;//说明进程如何接收信号以及从何处收到
pid_t si_pid;//适用于SIGCHLD,代表被终止进程的PID
pid_t si_uid;//适用于SIGCHLD,代表被终止进程所拥有进程的UID
int si_status;//适用于SIGCHLD,代表被终止进程的状态
clock_t si_utime;//适用于SIGCHLD,代表被终止进程所消耗的用户时间
clock_t si_stime;//适用于SIGCHLD,代表被终止进程所消耗系统的时间
sigval_t si_value;
int si_int;
void * si_ptr;
void* si_addr;
int si_band;
int si_fd;
};
例:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
void handler(int sig,siginfo_t *s_t,void *p)//能够接受额外数据的信号处理函数签名
{
int tmp = 0;
tmp = s_t->si_int; //si_int和si_value.sival_int是一样的--针对额外数据是int的时候。
printf("Aloha recv a sig=%d\tvar :%d\n and var is also: %d", sig,tmp,s_t->si_value.sival_int);
}
int main(int argc, char *argv[])
{
pid_t pid;
int ret = 0;
int i = 0;
union sigval mysigval;//用来存放额外数据
struct sigaction act;//用来注册信号
/*使用sigaction必须要初始化的三个成员*/
act.sa_sigaction = handler;//指定回调函数
act.sa_flags = SA_SIGINFO;//尤其重要--只有等于SA_SIGINFO,信号处理函数才能接受额外数据
sigemptyset(&act.sa_mask);//清空屏蔽字
if(sigaction(SIGINT,&act,NULL) < 0)//注册信号--指定毁掉函数
{
perror("sigaction error!\n");
exit(-1);
}
pid = fork();//创建子进程
if(-1 == pid)
{
perror("fork");
exit(-1);
}
else if(0 == pid)
{
mysigval.sival_int = 125;//设置要随着信号发送的额外数据
for(i = 0;i < 10;i++)//子进程发送十次信号--SIGINT是不可靠信号--传送有点慢
{
ret = sigqueue(getpid(),SIGINT,mysigval);//开始发送信号
if(ret != 0)//发送失败
{
perror("sigqueue");
exit(-1);
}
else{//返回0表示信号发送成功
printf("send ok!\n");
sleep(1);
}
}
}
else if(pid > 0)
{
while(1);//父进程死循环
}
return 0;
}
系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。
统一事件源
信号处理函数往管道的写端写入信号值,主循环从管道的读端读出该信号值。使用I/O多路复用来监听管道的读端文件描述符上的可读事件。从而将信号事件转化为一般的I/O事件,即统一事件源。