信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常,Linux中有很多种不同的信号。可以在终端输入kill -l 来查看Linux支持的信号,如下图:
Linux信号可由如下条件产生:
1、对于前台进程,用户可以通过输入特殊的终端字符来发送,比如输入Ctrl+C通常会给正在运行的进程发送一个中断信号。
2、系统异常。比如浮点异常和非法内存段访问。
3、系统状态变化。比如alarm定时器到期时将引起SIGALRM信号。
4、在终端运行kill命令或在程序中调用kill函数,例如:如果要杀死一个进程,我们可以使用 kill -9 pid 来杀死进程,-9表示发送9号信号,也就是SIGKILL信号,pid为发送信号的目标进程的进程ID;9号信号无法被忽略以及改变默认处理方式,因此发送9号信号一定能杀死进程。
Linux中,给进程发送信号的系统调用为kill,定义如下:
#include
#include
int kill(pid_t pid, int sig); // 给进程ID为pid的进程发送sig信号
pid参数 | 含义 |
---|---|
pid > 0 | 信号发送给进程ID为pid的进程。 |
pid = 0 | 信号发送给本进程组内的其它进程。 |
pid = -1 | 信号发送给除init进程外的所有进程,需要有权限 |
pid < -1 | 信号发送给ID为-pid的进程组中的所有成员 |
kill函数在成功时返回0, 失败时返回-1,并设置errno。
每个信号都有默认的处理方式,有的信号的默认处理方式是终止进程,有的是忽略信号以及结束进程并生成核心转储文件、暂停进程以及继续进程等。我们也可以在程序中修改信号的处理方式,注意:无法修改9号信号SIGKILL的默认处理方式,也无法忽略该信号。
信号处理函数的原型为:
#include
typedef void (*sighandler_t)(int);
除了用户自定义信号处理函数外,bits/signum.h头文件还定义了信号的两种其它处理方式:
#include
#define SIG_DFL ((sighandler_t) 0) // 使用信号的默认处理方式
#define SIG_IGN ((sighandler_t) 1) // 忽略目标信号
要为一个信号设置处理函数,可以使用signal系统调用:
#include
sighandler_t signal(int signum, sighandler_t handler);
signum为要捕获的信号类型,handler用于指定新的信号处理函数,也就是程序在收到signum类型信号后执行的回调函数。返回值为旧的信号处理函数。
例如下面代码:
代码中修改了2号信号SIGINT的处理函数,SIGINT信号可以由终端中按Ctrl+C来产生,因此当我们运行该程序后按Ctrl+C就会输出hello,world,为了结束该进程我们可以按Ctrl+\,这个按键组合会产生SIGQUIT信号,该信号的默认处理函数是结束进程并产生转储文件。如下图所示:
#include
#include
#include
// 信号处理回调函数
void handleSignal(int signum)
{
std::cout << "hello, world!" << std::endl;
}
int main()
{
signal(SIGINT, handleSignal); //修改SIGINT信号的处理方式
while(1)
{
sleep(1);
}
return 0;
}
在下面的代码中修改9号信号SIGKILL的信号处理函数进行一个测试:
#include
#include
#include
#include
#include
typedef void (*sighandler_t)(int);
void sig_handler(int signum)
{
std::cout << "hello, world!" << std::endl;
}
int main()
{
sighandler_t ret = signal(SIGKILL, sig_handler);
if( ret == SIG_ERR)
{
std::cout << "ignore SIGKILL failed, reason: " << strerror(errno) << std::endl;
}
while(1)
{
sleep(1);
}
return 0;
}
发现signal系统调用失败了,打开另一个终端查看该进程ID,并发送9号信号给此进程,发现进程还是被杀死了。说明9号信号SIGKILL的默认处理动作是无法被修改的。而且该信号也是不能被忽略的。
sigaction系统调用:
#include
// act为新的处理方式,odlact为旧的处理方式
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 第二种形式的信号处理函数
// 屏蔽信号集,调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sigset_t sa_mask;
int sa_flags; // 通常设置为0,表使用默认属性
void (*sa_restorer)(void); // 过时的元素,弃用
};
使用sigaction函数时可以指定在信号处理函数被调用的过程中要屏蔽的信号。
进程或用户A给一个进程B发送信号,B在收到信号之前执行自己的代码,当B进程收到信号后,不管程序执行到什么位置,都要暂停运行,去处理信号,也就是调用信号处理函数,处理完再继续执行。与硬件中断类似——异步模式。但信号是软件层面实现的中断,早期常被成为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
内核实现信号捕捉过程:
Linux使用数据结构sigset_t来表示一组信号,定义如下:
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
unsigned long int _val[_SIGSET_NWORDS];
} sigset_t;
// sigset_t 实际上是一个长整型数组,数组中每个元素的每个位表示一个信号,Linux提供了如下一组函数来设置、修改、删除和查询信号集:
int sigemptyset(sigset_t *set); //将信号集清0 成功:0;失败:-1
int sigfillset(sigset_t *set); //将信号集置1 成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); //将信号加入信号集 成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); //将信号清出信号集 成功:0;失败:-1
int sigismember(const sigset_t *set, int signum); //判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错:-1
我们可以利用sigprocmask来设置进程的信号掩码。该函数可以用来设置进程要屏蔽的信号或者解除屏蔽的信号,其本质,读取或修改进程的信号掩码(也叫信号屏蔽集)(PCB中)。进程的PCB中保存了该进程的信号屏蔽集,该屏蔽集用来指示哪些信号在产生时会被屏蔽。
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* how参数:
SIG_BLOCK: 新的进程信号掩码是其当前值和set指定信号集的并集,相当于 mask = mask|set
SIG_UNBLOCK: 新的信号掩码是其当前值和~set信号集的交集,因此set指定的信号集将不被屏蔽,相当于 mask = mask & ~set
SIG_SETMASK: 直接将进程信号掩码设置为set
sigprocmask成功时返回0,失败时返回-1并设置errno。
设置信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。下面的函数可以获得进程当前被挂起的信号集。
#include
int sigpending(sigset_t *set);
在信号屏蔽期间如果该信号多次产生,在屏蔽结束后进程也只能接收到一次该信号。
例如下面代码:在main函数中分别注册了SIGINT和SIGQUIT信号的处理函数,并屏蔽了SIGINT信号,SIGINT信号可由终端按Ctrl+C产生,因此当我们按这个按键组合时,进程无法收到信号。当我们使用Ctrl+\来产生SIGQUIT信号时,由于该信号没有被屏蔽因此其处理函数会被调用,解除对SIGINT信号的屏蔽,之后进程便可以收到SIGINT信号,并输出hello,world。但是无论按了多少次Ctrl+C,hello,world只会输出一次。如下图:
#include
#include
#include
// SIGINT 信号处理函数
void handle_sigint(int signum)
{
std::cout << "hello,world!" << std::endl;
}
// SIGQUIT 信号处理函数
void handle_sigquit(int signum)
{
// 解除SIGINT信号的屏蔽
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_UNBLOCK, &set, nullptr);
}
int main()
{
// 注册SIGINT和SIGQUIT的信号处理函数
signal(SIGINT, handle_sigint);
signal(SIGQUIT, handle_sigquit);
// 屏蔽SIGINT信号
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, nullptr);
while(1)
{
sleep(1);
}
return 0;
}
但是上面的代码会出现一个让人奇怪的情况,当按下Ctrl+\解除了信号屏蔽之后,进程会立即收到SIGINT信号,但是再按Ctrl+C,将会发现SIGINT信号又被屏蔽了。但是如果在main函数中解除信号屏蔽,那么之后再发送SIGINT信号,进程都将立刻收到,如下图:
代码如下:
#include
#include
#include
bool flag = false;
void handle_sigint(int signum)
{
std::cout << "hello,world!" << std::endl;
}
void handle_sigquit(int signum)
{
flag = true;
}
int main()
{
signal(SIGINT, handle_sigint);
signal(SIGQUIT, handle_sigquit);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, nullptr);
while(1)
{
sleep(1);
if(flag)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_UNBLOCK, &set, nullptr);
flag = false;
}
}
return 0;
}
关于为什么会出现这样的情况,可能是进入信号处理函数后,修改的信号掩码是临时的,在信号处理函数调用完毕后,又恢复了原来的mask。
SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
SIGINT:当用户按下了
组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。 SIGQUIT:当用户按下
组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。 SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
SIGTSTP:停止终端交互进程的运行。按下
组合键时发出这个信号。默认动作为暂停进程。 SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
SIGPWR:关机。默认动作为终止进程。
SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
SIGRTMIN ~ 64 SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
系统调用可以分为两类:
慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait…
其他系统调用:getpid、getppid、fork…
如果程序在执行处于阻塞状态的系统调用时收到信号,则默认情况下系统调用将会被中断,并且errno被设置为EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志自动启动被该信号中断的系统调用。
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽太久(为了避免一些竞态条件,在调用信号处理函数期间,该信号不会被再次触发)。一种解决方案是:将信号的主要处理逻辑放在程序的主循环中,当信号处理函数触发时,它只是简单地通知主循环接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行对应的处理逻辑。信号处理函数通常使用管道来将信号传递给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据呢?我们只需使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件即可。如此一来,信号事件就能和其它I/O事件一样被处理,即统一事件源。
——《Linux高性能服务器编程》
下面实现的回射服务器可以将I/O事件以及信号事件统一在主循环中进行处理:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
#define PORT 6666
#define MAX_EVENTS 1024
#define MAX_BUF_SIZE 1024
struct Event;
using readHandle = void(*)(Event *);
using writeHandle = void(*)(Event *);
// 自定义结构体,用来保存一个连接的相关数据
struct Event
{
int fd;
char ip[64];
uint16_t port;
epoll_event event;
char buf[MAX_BUF_SIZE];
int buf_size;
readHandle read_cb;
writeHandle write_cb;
};
int epfd;
static int pipefd[2];
void err_exit(const char *reason)
{
cout << reason << ":" << strerror(errno) << endl;
exit(1);
}
// 设置非阻塞
int setNonblcoking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
// 设置端口复用
void setReusedAddr(int fd)
{
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
}
// 信号处理函数
void sig_handler(int signum)
{
// 保留原来的errno,在函数最后恢复,以保证函数的可重入性
int save_errno = errno;
int msg = signum;
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);
sigaction(sig, &sa, nullptr);
}
// 初始化server socket
int socket_init(unsigned short port, bool reuseAddr)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
err_exit("socket error");
}
if(reuseAddr)
{
setReusedAddr(fd);
}
struct sockaddr_in addr;
bzero(&addr, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret < 0)
{
err_exit("bind error");
}
setNonblcoking(fd);
ret = listen(fd, 128);
if(ret < 0)
{
err_exit("listen error");
}
return fd;
}
void readData(Event *ev)
{
ev->buf_size = read(ev->fd, ev->buf, MAX_BUF_SIZE - 1);
ev->event.events = EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_MOD, ev->fd, &ev->event);
}
void writeData(Event *ev)
{
write(ev->fd, ev->buf, ev->buf_size);
ev->event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_MOD, ev->fd, &ev->event);
}
// 接收连接回调函数
void acceptConn(Event *ev)
{
Event *cli = new Event;
struct sockaddr_in cli_addr;
socklen_t sock_len = sizeof(cli_addr);
int cfd = accept(ev->fd, (struct sockaddr *)&cli_addr, &sock_len);
if(cfd < 0)
{
cout << "accept error, reason:" << strerror(errno) << endl;
return;
}
setNonblcoking(cfd);
cli->fd = cfd;
cli->port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr, cli->ip, sock_len);
cli->read_cb = readData;
cli->write_cb = writeData;
cli->event.events = EPOLLIN;
cli->event.data.ptr = (void *) cli;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &cli->event);
cout << "New Connection, ip:[" << cli->ip << ":" << cli->port << "]" << endl;
}
int main(int argc, char *argv[])
{
int fd = socket_init(PORT, true);
Event server;
server.fd = fd;
epfd = epoll_create(MAX_EVENTS);
if(epfd < 0)
{
err_exit("epoll create error");
}
server.event.events = EPOLLIN;
server.event.data.ptr = (void *)&server;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &server.event);
// 使用socketpair创建管道,注册pipefd[0]上的可读事件
int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
if(ret == -1)
{
err_exit("socketpair error");
}
setNonblcoking(pipefd[1]);
Event pipeEv;
pipeEv.fd = pipefd[0];
pipeEv.event.events = EPOLLIN;
pipeEv.event.data.ptr = (void *)&pipeEv;
epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &pipeEv.event);
// 设置一些信号的处理函数
addsig(SIGINT);
addsig(SIGCHLD);
addsig(SIGTERM);
addsig(SIGQUIT);
bool stop_server =false;
struct epoll_event events[MAX_EVENTS];
int nready = 0;
while(!stop_server)
{
// 将定时容器中定时时间最短的时长作为epoll_wait的最大等待时间
nready = epoll_wait(epfd, events, MAX_EVENTS, 1000);
if(nready < 0)
{
cout << "epoll wait error, reason:" << strerror(errno) << endl;
}
else if(nready > 0)
{
for(int i = 0; i < nready; i++)
{
Event *ev = (Event *) events[i].data.ptr;
// 接受新的连接
if(ev->fd == fd )
{
acceptConn(ev);
}
else if(ev->fd == pipefd[0]) //处理信号
{
int sig;
ret = recv(ev->fd, ev->buf, MAX_BUF_SIZE, 0);
if(ret <= 0)
{
continue;
}
else
{
// 每个信号占一个字节,所以按字节来逐个接收信号
for(int i = 0; i < ret; i++)
{
switch (ev->buf[i])
{
case SIGCHLD:
cout << "SIGCHLD\n";
break;
case SIGQUIT:
stop_server = true;
break;
case SIGTERM:
cout << "SIGTERM\n";
case SIGINT:
cout << "别按了,休想终止我!\n";
}
}
}
}
else if(ev->event.events & EPOLLIN)
{
ev->read_cb(ev);
}
else if(ev->event.events & EPOLLOUT)
{
ev->write_cb(ev);
}
}
}
}
close(fd);
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们常利用SIGHUP信号来强制服务器重新读取配置文件。
默认情况下,往一个读端关闭的管道或socket连接中写入数据将引发SIGPIPE信号,程序收到SIGPIPE信号的默认处理方式是结束进程,而我们绝不希望因为错误的写操作而导致进程退出,因此我们需要在代码中捕获并处理该信号,或者至少忽略它。
我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下我们应该使用send函数反馈的errno值来判断管道或者socket连接的读端是否已经关闭。
由alarm和setitimer函数设置的定时器一旦超时,将触发SIGALRM信号,因此我们可以利用该信号的信号处理函数来处理定时任务。SIGALRM的默认处理是终止进程,而且每个进程都有且只有唯一个定时器。
相关函数如下:
#include
unsigned int alarm(unsigned int seconds); // 返回0或剩余的秒数,无失败
#include
// us级别的定时,可以实现周期定时
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
struct itimerval {
struct timeval it_interval; /* 用来设定两次定时任务之间间隔的时间。 */
struct timeval it_value; /* 定时的时长 */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。