CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~
信号
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
- 用户可以通过输入特殊的终端字符给进程发送信号,例如Ctrl+C发送中断信号。这样结束进程不会产生僵尸进程。
- 系统异常。除0,非法内存段访问。
- 系统状态变化。alarm定时器到期将引起SIGALRM信号。
- kill命令等。
服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。
发送信号
Linux下,一个进程给其他进程发送信号的API是kill函数。
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
将sig设置为0,可以用来检测目标进程或进程组是否存在。
信号处理方式
信号处理函数
#include<signal.h>
typedef void(*__sighandler_t)(int);
参数为一个int,用来指示信号类型。
除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了 信号的两种其他处理方式——SIG_IGN和SIG_DEL。
#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)
中断系统调用
如果程序在执行处于阻塞状态的系统调用时,接收到了信号,并且为该信号设置了信号处理函数,那么默认情况下系统调用将被中断,并且errno被设置为EINTR,可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN), 如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统 调用(比如connect、epoll_wait)。
信号函数
-
signal系统调用
#include<signal.h> _sighandler_t signal(int sig,_sighandler_t_handler)
sig:信号类型,$handler$用于指定信号sig的处理函数。
signal函数成功时返回一个函数指针,该函数指针的类型也是 _sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针, 或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用 signal的话)。
-
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; void(*sa_restorer)(void); };
sa_hander成员指定信号处理函数;
sa_mask成员设置 进程的信号掩码,指定哪些信号不能发送给本进程;
sa_flags成员用于设置程序收到信号时的行为;
sa_restorer成员已经过时,最好不要使用.
信号集函数
Linux使用数据结构sigset_t来表示一组信号。
#include<bits/sigset.h>
#define_SIGSET_NWORDS(1024/(8*sizeof(unsigned long int)))
typedef struct
{
unsigned long int__val[_SIGSET_NWORDS];
}__sigset_t;
#include<signal.h>
int sigemptyset(sigset_t*_set)/*清空信号集*/
int sigfillset(sigset_t*_set)/*在信号集中设置所有信号*/
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是否在信 号集中*/
sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号
进程信号掩码
利用sigaction结构体的sa_mask成员来设置进程的信号掩码
#include<signal.h>
int sigprocmask(int_how,_const sigset_t*_set,sigset_t*_oset);
_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码(如果不为NULL的话)。
如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式:
如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用 _oset参数来获得进程当前的信号掩码。
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。
获得进程当前被挂起的信号集:
#include<signal.h>
int sigpending(sigset_t*set);
统一事件源
信 号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往 管道的写端写入信号值,主循环则从管道的读端读出该信号值。
使用 I/O复用系统调用来监听管道的读端文件描述符上的可读事件.
int setnonblocking(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 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_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);
}
网络编程相关信号
-
SIGHUP
当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。
-
SIGPIPE
往一个读端关闭的管道或socket连接中写数据将引 发SIGPIPE信号。程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。
可以使用send函数的MSG_NOSIGNAL标志来禁 止写操作触发SIGPIPE信号,使用send函数反 馈的errno值来判断管道或者socket连接的读端是否已经关闭。
利用I/O复用系统调用来检测管道和socket连接 的读端是否已经关闭。当管道的读端关闭时,写端文件 描述符上的POLLHUP事件将被触发;当socket连接被对方关闭时, socket上的POLLRDHUP事件将被触发。
-
SIGURG
带外数据。