信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
10.1 Linux信号概述 10.1.1 发送信号Linux下,一个进程给其他进程或进程组发送信号的API是kill函数。
#include#include int kill(pid_t pid, int sig); 参数解释: pid:接收信号的目标进程ID sig:准备发送的信号代码,假如其值为0则没有任何信号送出,但是系统会执行错误检查, 通常会利用sig值为零来检验某个进程是否仍在执行。 返回值:成功时返回0,失败则返回-1并设置errno。
参数pid的取值及含义如下表所示:
Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。参数sig的取值及其含义如下表所示:
kill函数返回的errno取值及含义如下表所示:
#include10.1.2 中断系统调用#include #include #include #include #include #include void waitChild(int signo) { int status; if (signo == SIGCHLD) { pid_t pid = wait(&status); printf("回收子进程, pid: %dn", pid); } } //输入ctrl + c,会发送SIGINT信号 void testInt(int signo) { printf("键入: ctrl + c, pid: %dn", getpid()); } //输入ctrl + z,会发送SIGTSTP信号 void testSTP(int signo) { printf("键入: ctrl + z, pid: %dn", getpid()); } int main() { pid_t pid; if (0 > (pid = fork())) //创建子进程 { printf("fork errno is: %dn", errno); } else if (pid == 0) //子进程 { sleep(2); //睡眠2s,让父进程为SIGCHLD信号绑定好函数 printf("I am is child! pid is %dn", getpid()); // 利用kill函数 发信号给父进程 if (-1 == kill(getppid(), SIGINT)) { printf("send SIGINT to parent process fail, errno is: %dn", errno); } //结束进程,返回123,同时发送SIGCHLD信号 exit(123); } else //父进程 { printf("I am is parent! pid is %dn", getpid()); // 让SIGCHLD信号绑定waitChild函数 if (SIG_ERR == signal(SIGCHLD, waitChild)) { printf("bind SIGCHLD signal errno is: %dn", errno); exit(-1); } // 让SIGINT 信号绑定testInt if (SIG_ERR == signal(SIGINT, testInt)) { printf("bind SIGINT signal errno is: %dn", errno); exit(-1); } // 让SIGTSTP 信号绑定testSTP if (SIG_ERR == signal(SIGTSTP, testSTP)) { printf("bind SIGTSTP signal errno is: %dn", errno); exit(-1); } while(1); // 死循环,等待信号 } return 0; }
如果程序在执行执行处于阻塞状态的系统调用(select、poll)时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志以重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(SIGSTOP、SIGTTIN),如果没有为它们设置信号处理函数,则它们也可以中断某些系统调用(connect、epoll_wait)。
10.2 信号注册函数对于捕获到的信号,要使用信号注册函数为信号设置处理函数。
10.2.1 signal函数#includevoid (*signal(int signo, void (*func)(int)))(int); 参数解释: signo:信号。见10.1.1的表 void (*func)(int):返回值为void,参数为int的函数指针 返回值: signal系统调用出错时返回SIG_ERR,并设置errno 实际上,信号处理函数的原型如下: tyepdef void (*__sighandler_t)(int); 可知__sighandler_t是函数指针(void (*)(int))的一个别名。 此时再看:void (*signal(int signo, void (*func)(int)))(int); signal(int signo, void (*func)(int))就是一个函数指针,指向void (*)(int)型函数。
代码示例:
#include10.2.2 sigaction函数#include #include void procFun(int sig) { if(sig == SIGINT) { puts("CTRL + C pressed"); } } int main() { //注册信号处理函数 signal(SIGINT, procFun); sleep(10); return 0; }
由于signal函数在不同系统中存在兼容性问题,所以实际中很少使用。取而代之的是sigaction函数。
#includeint sigaction(int signo, const struct sigaction* act, struct sigaction* oldact); 参数解释: signo:信号 act:信号处理函数信息 oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0 返回值: 成功时返回0,失败时返回-1并设置errno struct sigaction { void (*sa_handler)(int); //信号处理函数指针 sigset_t sa_mask; //在进程原有信号掩码的基础上增加信号掩码,以指定哪些信号不能发送给本进程 int sa_flags; //设置程序收到信号时的行为 };
sigaction结构体的sa_flags成员的取值及含义如下表:
使用代码示例:
#include10.3 信号集#include #include void procFun(int sig) { if(sig == SIGINT) { puts("CTRL + C pressed"); } } int main() { struct sigaction act; act.sa_handler = procFun; sigemptyset(&act.sa_mask); //初始化sa_mask的所有成员为0 act.sa_flags = 0; sigaction(SIGINT, &act, 0); sleep(10); return 0; }
信号从产生到抵达目的地,叫作信号递达。而信号从产生到递达的中间状态,叫作信号的未决状态。产生未决状态的原因有可能是信号受到阻塞了,也就是信号屏蔽字(或称阻塞信号集,mask)的对应位被置1(非0即1)。
信号集可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞;而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
10.3.1 信号集函数Linux使用sigset_t来表示一组信号。sigaction结构体的sa_mask成员的类型就是sigset_t。其定义如下:
#include#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; }sigset_t; sigset_t实质上是一个数组,每个元素的每个bit位表示一个信号,这种定义方式类似于fd_set。
下面一组函数用来设置、修改、删除、查询信号集:
#includeint sigemptyset(sigset_t* _set); //将信号集清0 int sigfillset(sigset_t* _set); //将信号集置为1 int sigaddset(sigset_t* _set, int signo); //将信号signo加入信号集 int sigdelset(sigset_t* _set, int signo); //将信号signo从信号集中删除 int sigismember(_const sigset_t* _set, int signo); //判断signo是否在信号集中。在:1;不在0;错误:-1
sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。能用来可以用来屏蔽信号,也可以用来解除屏蔽信号。
#includeint sigprocmask(int how, const sigset_t* set, sigset_t* oldset); 参数解释: how:指定操作方式 set:传入参数。set中哪位置1,就表示当前进程屏蔽哪个信号 oldset:传出参数,保存旧的信号屏蔽集。 返回值: 成功时返回0,失败返回-1并设置errno
该函数的_how参数可取值及对应的含义如下表:
| how参数 | 表示意义 |
| SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号(往里加) |
| SIG_BLOCK | set包含了我们希望从当前信号屏蔽字中解除屏蔽的信号(往外减) |
| SIG_SETMASK | 设置当前信号屏蔽字为set(重新设置) |
信号被屏蔽后将无法被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置位进程的一个挂起的信号(未决信号)。如果取消对被挂起信号的屏蔽,则它能立即被进程接收到。 sigpending函数能获取当前进程的未决信号集,通过set参数保存。
#includeint sigpending(sigset_t *set); 返回值:调用成功则返回0,出错则返回-1并设置errno
使用代码示例:
#include#include #include void handler(int signo) //信号2的处理函数 { printf("get a signo is:%dn",signo); } void show(sigset_t *pending) //输出pending信号集 { int i=1; for(; i<=31; i++) { if(sigismember(pending, i)) //判断信号i是否在pending信号集中 { printf("1"); //在则输出1 } else { printf("0"); //不在则输出0 } } printf("n"); } int main() { sigset_t set, oldset; //定义两个阻塞信号集 sigemptyset(&set); //初始化这两个信号集 sigemptyset(&oldset); sigaddset(&set, 2); //将信号2添加到阻塞信号集 sigprocmask(SIG_SETMASK, &set, &oldset); //将当前阻塞信号集设为set printf("show set begin!n"); show(&set); signal(2, handler); //设置信号2的处理函数为handler sigset_t pending; //定义未决信号集pending int i=3; while(1) { sigpending(&pending); //获取当前进程的未决信号集 printf("show pending begin!n"); show(&pending); //打印未决信号集 sleep(1); if(i-- ==0) { //3秒之后将阻塞信号集设为oldset sigprocmask(SIG_SETMASK, &oldset, NULL); } } return 0; }
执行结果分析:
10.4 统一事件源事件源:定时器的超时事件、信号、数据读写、网络异常。
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。当信号来临时,主逻辑会被打断去执行信号处理函数。而信号到来的时机是不确定,如果此时信号处理函数会去访问一个已经被锁住的资源,那么这个线程就会被阻塞。这一点可通过下面一个例子看出:
#include#include #include void procFun(int sig) { if(sig == SIGINT) { puts("CTRL + C pressed"); sleep(6); printf("procFun has already sleep 6sn"); } } int main() { struct sigaction act; act.sa_handler = procFun; //为信号设置处理函数 sigemptyset(&act.sa_mask); //初始化sa_mask的所有成员为0 act.sa_flags = 0; sigaction(SIGINT, &act, 0); //在这3s内按ctrl+c,发送信号,信号处理函数会被立即执行, //信号处理函数执行结束后,继续执行main函数。 //可见当信号来临时,主逻辑会被打断去执行信号处理函数 for(int i=1; i<=3; i++) { sleep(1); printf("main functon sleep %dsn", i); } printf("main function finishedn"); return 0; } 程序执行结果: GF@GF:~/VscodeProject/test1$ ./signal main functon sleep 1s main functon sleep 2s ^CCTRL + C pressed procFun has already sleep 6s main functon sleep 3s main function finished
此外,一般信号处理时会将一些信号屏蔽。为了不屏蔽这些信号太久,同时也不至于主逻辑被冲散,一种解决方案是:
用一种简单的信号处理函数,这种函数只是简单的通知主循环并告诉信号值,而真正的信号处理函数才会被主循环调用,根据信号值做出相应的处理。这种简单的信号处理函数和主循环之间通常用管道做通信,这种函数从管道的写端写入信号值,主循环从管道的读端读取信号值。因为主循环本来就要使用I/O复用函数监听连接socket和监听socket,所以,不妨将这个管道一并注册到I/O复用函数,这样就能在主循环中及时收到信号并作出处理。这样一来,只要有信号发出,就可以被及时接收,同时主逻辑也不会被中断。这种方法可以简写为如下示例:
int pipefd[2];
...
void sig_handler(int sig) //简单的信号处理函数
{
int msg = sig;
send(pipefd[1], (char*)&msg, 1, 0);//将信号按字节写入管道,以通知主循环
}
void addsig(int sig) //为信号设置处理函数
{
struct sigaction sa;
memset(&sa, ' ', sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART; //信号如果打断了慢速系统调用,中断处理完成之后继续恢复系统调用
sigfillset(&sa.sa_mask); //在信号处理函数中屏蔽所有信号
assert(sigaction(sig, &sa, NULL) != -1);
}
void handle_sig(int sig) //真正的信号处理函数
{
switch(sig)
{
case SIGCHLD:
...
case SIGHUP:
...
case SIGTERM:
...
case SIGINT:
...
...
}
}
...
int main(int argc, char **argv)
{
...
//创建管道
ret=socketpair(PF_UNIX,SOCK_STREAM,0,pipefd);
setnonblocking(pipefd[1]);
addfd(epollfd,pipefd[0]);
while(true)
{
int ret = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for (int i = 0; i < ret; i++)
{
int sockfd = events[i].fd;
if (sockfd == listenfd)
{
int connfd = accept(sockfd, ...);
...
}
else if(sockfd == pipefd[0] && events[i].events & EPOLLIN) //接收到信号
{
char signals[1024];
int num = recv(pipefd[0], signals, sizeof(signals), 0);
//每个信号值占1字节,所以按字节来逐个接收信号
//可能处理的时候收到了多个信号
for (int i = 0; i < num; i++)
{
handle_sig(signals[i]);
}
}
else
{
//读写socket
}
}
}
...
}



