- 信号:用户、系统或进程发送给目标进程的信息,通知目标进程某个状态的改变或系统异常。
10.1 Linux信号概述
10.1.1 发送信号
一个进程给其他进程发送信号的API是kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- pid是目标进程的id,含义如下:
- sig是信号值
- 成功返回0,失败返回-1并设置errno
10.1.2 信号处理方式
- 目标进程收到信号时,需要定义一个接收函数来处理
#incldue <signal.h>
typedef void(* _sighandler_t) (int);
- 信号处理函数必须是可重入的
- 定义了两种处理方式,SIG_IGN(忽略目标信号),SIG_DFL(使用信号默认处理方式)
#include <bits/signum.h>
#define SIG_DFL ((_sighandler_t) 0)
#define SIG_IGN ((_sighandler_t) 1)
Linux信号
- linux可用信号都定义在
bits/signum.h
头文件中。 - 主要关注的几个信号
SIGHUP
,SIGPIPE
,SIGURG
,SIGALRM
,SIGCHLD
。
10.1.4 中断系统调用:errno == EINTR
- 程序处于阻塞态的系统调用的时候,接收到信号,并且,为该信号设置了信号处理函数,那么系统调用会被中断,且errno设置为
EINTR
,可以使用sigaction为信号设置SA_RESTART标志,以重新启动被改信号中断的系统调用, - 对于默认行为是暂停进程的信号(SIGSTOP、SIGTTIN),就算没设置信号处理函数,也可以中断一些系统调用(connect,epoll_wait),
这是Linux独有
。
10.2 信号函数
10.2.1 signal系统调用
- 使用signal系统调用为一个信号设置处理函数
#include <signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);
- sig参数指出捕获信号类型
- _handler参数指定信号sig的处理函数
- 返回值:上一次调用signal传入的函数。若是第一次,返回信号sig对应的默认处理函数。
- 出错时返回SIG_ERR并设置errno
10.2.2 sigaction系统调用
- 比signal更好用的是sigaction系统调用
#include <signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* 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 sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}
- sa_handler 指定信号处理函数
- sa_mask 设置进程信号掩码,(在进程原有信号掩码上增加)以指定哪些信号不能发送给本进程。sa_mask是信号集sigset_t类型。
- sa_flags成员用于设置程序收到信号时的行为。
- sa_restorer已经成为历史
10.3 信号集
10.3.1 信号集函数
- sigset_t是一个长整型数组,数组的每个元素的一个位代表一个信号,和fd_set类似
#include <bits/sigset.h>
#define _SIGSET_NWORDS(1024/(8*sizeof(unsigned long int)))
typedef struct{
unsigned long int __val[SIGSET_NWORDS];
} sigset_t
- Linux提供了一组函数用来设置、修改、查询和删除信号集
#include <signal.h>
int sigemptyset(sigset_t *_set); //清空信号集
int sigfillset(sigset_t* _set); //在信号集中设置所有信号
int sigaddset(sigset_t* _set, int _signo); //将信号_signo添加到信号集中
int sigdelset(sigset* _set, int _signo); //将信号_signo从信号集中删除
int sigismember(sigset* _set, int _signo); //测试_signo是否在信号集中
10.3.2 进程信号掩码
- 设置或查看进程的信号掩码
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
- how指定设置进程信号掩码的方式
- set指定新的信号掩码,如果set==NULL,则进程信号掩码不变,oset就是当前进程的信号掩码
- oset输出原来的掩码
- 成功返回0,失败返回-1并设置errno。
10.3.3 被挂起的信号
- 对于被掩码屏蔽的信号,进程接收到之后会设置为进程的
被挂起的信号
。当我没取消对挂起信号的屏蔽,那么他立刻就能被进程接收。 - 获取当前进程被挂起的信号集
#include <signal.h>
int sigpending(sigset_t* set);
- 注意,进程多次接收到同一个被挂起的信号,set也只能反映一个,也就是set只能反映有没有,不能反映挂起的数量。
- 成功返回0,失败返回-1并设置errno。
10.4 统一事件源
- 信号处理函数和程序主循环是不同的执行路线。
- 信号在处理期间,系统不会再次触发它。所以应该尽快把信号处理函数执行完毕。
- 典型处理方法:把信号的主要处理逻辑放在程序的主循环中,当信号处理函数触发时,信号处理函数简单的通知主循环程序接收信号,并把信号值传递给主循环,主循环根据接收到的信号值,执行目标信号对应的逻辑代码。
- 信号处理函数通常使用
管道
来传递信号,使用IO复用系统监听管道读端的可读时间,这样信号时间就能和其他IO事件一起被处理。——这就是统一事件源
下边的程序有点问题
//统一事件源
#include "../create_sockfd.h"
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#define MAX_EVENT_NUMBER 1024
static int pipefd[2];
//信号处理函数
void sig_handler(int sig){
//保留原来的errno,在函数最后恢复,保证函数的可重入性
int save_errno = errno;
int msg = sig;
send(pipefd[1], (char*)&msg, 1, 0); //将信号写入管道,通知主循环
errno = save_errno;
}
//设置信号
void addsig(int sig){
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask); //设置所有掩码
assert(sigaction(sig, &sa, NULL)!=-1);
}
void addfd(int epollfd, int fd){
epoll_event event;
event.events = EPOLLIN;
event.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("Usage : %s ip port\n", basename(argv[0]));
return 1;
}
createSockfd sockfd(argv[1], atoi(argv[2]));
assert(sockfd.bindSockfd() != -1);
assert(sockfd.listenfd(5) != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, sockfd.sockfd);
//创建管道,并注册管道事件
assert(socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd) != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]);
//设置信号处理函数
addsig(SIGHUP);
addsig(SIGCHLD);
addsig(SIGTERM);
addsig(SIGINT);
bool stop_server = false;
while(!stop_server){
int n = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(n < 0 && errno != EINTR){
printf("epoll failure\n");
return 1;
}
for(int i = 0; i != n; ++i){
int sfd = events[i].data.fd;
if(sfd == sockfd.sockfd){
sockfd.connectSockfd();
addfd(epollfd, sockfd.connfd);
} else if((sfd == pipefd[0]) && (events[i].events & EPOLLIN)){
int sig;
char signals[1024];
int ret = recv(pipefd[0], signals, sizeof(signals), 0);
if(ret == -1){
continue;
} else if(ret == 0){
continue;
} else {
//每一个信号占一个字节,所以按字节逐个接收信号
for(int i = 0; i < ret; ++i){
switch (signals[i]){
case SIGCHLD:
{
printf("SIGCHLD\n");
continue;
}
case SIGHUP:{ //终端挂起
printf("SIGHUP\n");
continue;
}
case SIGTERM:{ //kill命令进程
printf("SIGTERM\n");
stop_server = true;
break;
}
case SIGINT:{ //ctrl+c中断进程
printf("SIGINT\n");
stop_server = true;
}
}
}
}
}
}
}
return 0;
}
10.5 网络编程相关信号
10.5.1 SIGHUP
- 挂起进程的控制终端时,SIGHUP信号会被触发。
- 没有控制终端的网络后台程序常常使用SIGHUP信号强制服务器重读配置文件。
10.5.2 SIGPIPE
- 往一个读端关闭的管道或socket连接中写数据将引发
SIGPIPE信号
- 我们应该在代码中捕获并处理该信号,因为程序接收到SIGPIPE信号的默认行为是结束进程。
10.5.3 SIGURG
- Linux环境下,内核通知应用程序带外数据到达主要有两种方法,一种是IO复用技术,select系统调用在接收到带外数据时候返回。如9-1所示。另一种是使用SIGURG信号。