文章目录
信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或者系统异常。
Linux的信号可由如下条件产生:
- 用户可以通过输入特殊的终端字符给前台进程发送信号,如
Ctrl + C
发送中断信号。 - 系统异常。比如浮点异常和非法内存段访问
- 系统状态变化。比如alarm定时器到期引起的
SIGALRM
信号 - 运行
kill
命令或者调用kill
函数
服务器应该及时处理或忽略常见信号,以避免异常终止。
Linux信号概述
发送信号
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
该函数将信号sig
发送给目标进程,目标进程由pid指定
sig是信号值,如果设置为0,则不发送任何信号。但将其设置为0可以检测目标进程或进程组是否存在,因为会在信号发送前执行检查操作。但是这种方法并不可靠:①进程的PID回绕,导致被检测的PID不是期望的PID。②这种检测方法不是原子性的
执行成功返回0,失败返回-1,常见的errno值:
- EINVAL 无效信号
- EPERM 该进程没有权限发送信号给其他进程
- ESRCH 目标进程或进程组不存在
信号处理方式
#include<signal.h>
// 函数原型
typedef void (*__sighandler_t) (int);
#include<bits/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
信号处理函数只有一个带整形参数来指示信号类型。信号处理函数应该是可重入的,以避免竞态条件,所以在信号处理函数中禁用不安全代码。
除了用户自定义信号处理函数,还有两个宏(如上所示)SIG_IGN
表示忽略该信号,SIG_DFL
表示使用信号的默认处理方式——结束进程(Term)、忽略信号(Ign),结束进程并生成核心转储文件(Core),暂停进程(Stop),继续进程(Cont)
Linux信号
定义在bits/signum.h
中,常用的信号:SIGHUP
, SIGPIPE
, SIGURG
, SIGALRM
, SIGCHLD
。
中断系统调用
信号函数
signal
signal系统调用可以为一个信号设置处理函数
#include<signal.h>
_sighandler_t signal (int sig, _sighandler_t _handler);
·sig参数指定要捕获的信号类型,_handler参数是_sighandler_t 类型的函数指针,用于指定信号sig的处理函数。
signal调用成功时返回一个函数指针,是前一次调用signal函数时传入的函数指针或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal的话)。
调用失败则返回SIG_ERR
sigaction
设置信号处理函数更健壮的接口是如下的系统调用
#include<signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
sig参数指定信号类型,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;
/* Restore handler. */
// 不再使用
void (*sa_restorer) (void);
};
Example
Ⅰ
void sig_urg(int sig){
// 为了保证可重入性,保存errno
int save_err = errno;
// 对信号的处理
return save_err;
}
// 绑定注册信号
void addsig(int sig, void (*sig_handler)(int)){
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);
}
int main(){
// 将信号SIGURG绑定到信号处理函数sig_urg上
addsig(SIGURG, sig_urg);
}
Ⅱ
void sig_handler(int sig){
// 为了保证可重入性,保存当前errno
int save_err = errno;
int msg = sig;
// 将信号写入管道,以通知主循环
send(pipefd[1], (char*)&msg, 1, 0);
errno = save_err;
}
// 设置信号处理函数
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);
}
信号集
信号集函数
Linux使用sigset_t表示一组信号
#include<bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigined loong int)));
typedef struct{
unsigned long int __val(_SIGSET_NWORDS);
} __sigset_t;
sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符fd_set相似。 操作信号集的函数:
#include<signal.h>
int sigemptyset(sigset_t* _set); // 清空信号集
int sigfillset(sigset_t* _set); // 在信号集中设置所有信号
int sigaddset(sigset_t* _set, int _signo); // 将信号添加到信号集中
int sigdelset(sigset_t* _set, int _signo); // 在信号集中删除信号
int int sigismember(sigset_t* _set, int _signo); // 检测是否在信号集中
进程信号掩码
我们可以利用sigaction结构体的sa_mask字段来设置进程的信号掩码,如下函数可以用于设置或查看进程的信号掩码。
#include<signal.h>
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);
_set
参数指定新的信号掩码,_oset
输出保存以前的旧的信号掩码(与set_nonblocking
里面return oldopt
一个意思),_how
参数指定设置进程信号掩码的方式
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能再被接收。如果向进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对该信号的屏蔽,它就可以被立即接收到。该函数可以查看被挂起的信号集
int sigpending(sigset_t* set);
set参数保存被挂起的信号集。
进程多次接收到同一个被挂起的信号,其函数也只能反映一次。
统一事件源
信号是异步事件,信号处理函数和程序的主循环是两条不同的执行路线。
信号在处理期间,系统不会再触发它。所以在执行信号处理函数时,应该尽快执行完毕,以确保该信号不会被屏蔽。
一般的解决方案:统一处理信号
将信号的主要处理逻辑放在程序的主循环里,当信号处理函数被触发时,他只是通知主循环程序接收到的信号的值,主循环再根据信号值处理相关的逻辑。
信号处理函数可以通过管道将信号传递给主循环,主循环通过IO复用来监听管道的读端上是否有读就绪事件。
关键函数:
void addfd(int epollfd, int fd){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
// 信号处理函数
void sig_handler(int sig){
// 为了保证可重入性,保存当前errno
int save_err = errno;
int msg = sig;
// 将信号写入管道,以通知主循环
send(pipefd[1], (char*)&msg, 1, 0);
errno = save_err;
}
// 设置信号处理函数
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);
}
// 创建管道,注册pipefd[0]上的可读事件
{
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
// 注册读事件
addfd(epollfd, pipefd[0]);
}
// 接收并处理信号
{
epoll_event events[MAX_EVENT_NUM];
int number = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
for(int i = 0; i < number; i++){
int fd = events[i].data.fd;
if(fd == pipefd[0] && events[i].events & EPOLLIN){
int sig;
char signals[1024];
ret = recv(fd, signals, sizeof(signals), 0);
if(ret <= 0){
// 处理
}else{
// 接收到了一个或多个信号,一个信号1B
for(int j = 0; j < ret; j++){
switch(signals[j]){
case SIGCHLD:
//
case SIGHUP:
//
case SIGTERM:
//
case SIGINT:
//
}
}
}
}
}
}
网络编程中常用的信号
SIGHUP
当挂起进程的控制终端时,SIGHUP信号将被触发。
对于没有控制终端的网络后台程序,常利用其强制服务器重新读取配置文件。
SIGPIPE
往一个读端关闭的管道或者socket连接中写数据将引发SIGPIPE信号。程序接收到SIGPIPE的默认行为是结束程序,所以程序员一般需要捕获并处理该信号,或者忽略其。引起SIGPIPE信号的写操作将设置errno为EPIPE