前言
Linux产生信号中断,会打断当前正在执行程序,转而执行信号处理函数,由于执行信号处理函数时,正常执行程序被挂起,信号处理函数怎么操作才能保证程序再次被唤醒后能够正常执行,下面我们看两个案例
案例一
void signal_handler()
{
printf("this is a test
");
}
如果我们程序执行中调用malloc, printf等函数时,产生信号后执行上面信号处理函数,由于这些函数中有全局变量或static变量,执行后会破坏全局数据结构,造成不可预测后果。
案例二
void signal_handler()
{
lock(&mutex);
printf("this is a test
");
unlock(&mutex);
}
void main()
{
.....
lock(&mutex);
printf("this is a test
");
unlock(&mutex);
}
如果我们加锁是否就信号安全了呢,不尽然,看上面代码,假如我们执行main中lock函数获取锁后,程序捕获到信号,转而调用signal_handler函数,会发现永远无法加锁成功,造成死锁。
开源软件处理方式
Nginx
1.信号处理函数首先保存errno值,函数结束时恢复该值,由于errno是全局变量,信号处理函数中间可能会被函数修改。
2.通过设置一组全局变量,根据不同信号,设置不同值,当程序被唤醒后,通过判断这些值的状态来进行相应的操作,这个也是常用的做法。
3.程序启动时,从strerr获取所有错误的字符串描述,保存进一个全局数组中,保证信号处理函数调用的安全,因为strerr信号调用中并不安全。
4.信号处理函数中会进行一个时间的更新,由于时间变量是全局缓存变量,因此用锁进行了同步,不过这里的锁是一个尝试锁,并不会阻塞,尝试如果加锁失败,会立即返回并不会造成死锁。
void ngx_signal_handler(int signo)
{
char *action;
ngx_int_t ignore;
ngx_err_t err;
ngx_signal_t *sig;
ignore = 0;
/*保存errno值*/
err = ngx_errno;
/*判断是否是忽略的信号*/
for (sig = signals; sig->signo != 0; sig++) {
if (sig->signo == signo) {
break;
}
}
/*时间更新函数,这里有个尝试锁保证全局时间变量正确性*/
ngx_time_sigsafe_update();
action = "";
... ... ...
/*不同的信号,给不同的全局变量赋值*/
switch (signo) {
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
ngx_quit = 1;
action = ", shutting down";
break;
case ngx_signal_value(NGX_TERMINATE_SIGNAL):
case SIGINT:
ngx_terminate = 1;
action = ", exiting";
break;
case ngx_signal_value(NGX_NOACCEPT_SIGNAL):
if (ngx_daemonized) {
ngx_noaccept = 1;
action = ", stop accepting connections";
}
break;
case ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
ngx_reconfigure = 1;
action = ", reconfiguring";
break;
case ngx_signal_value(NGX_REOPEN_SIGNAL):
ngx_reopen = 1;
action = ", reopening logs";
break;
case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
if (getppid() > 1 || ngx_new_binary > 0) {
/*
* Ignore the signal in the new binary if its parent is
* not the init process, i.e. the old binary's process
* is still running. Or ignore the signal in the old binary's
* process if the new binary's process is already running.
*/
action = ", ignoring";
ignore = 1;
break;
}
ngx_change_binary = 1;
action = ", changing binary";
break;
case SIGALRM:
ngx_sigalrm = 1;
break;
case SIGIO:
ngx_sigio = 1;
break;
case SIGCHLD:
ngx_reap = 1;
break;
}
... ... ...
/*日志记录*/
ngx_log_error(NGX_LOG_NOTICE, ngx_cycle->log, 0,
"signal %d (%s) received%s", signo, sig->signame, action);
/*恢复errno值*/
ngx_set_errno(err);
}
libevent
1.信号处理函数首先保存errno值,函数结束时恢复该值,由于errno是全局变量,信号处理函数中间可能会被函数修改。
2.通过socketpair创建一个socket对fd[2],然后从fd[0]发送信号内容,程序被唤醒后会触发fd[1]接收事件,接收fd[0]发送的信号,进行相应处理。
static void evsignal_handler(int sig)
{
/*保存errno值*/
int save_errno = errno;
if (evsignal_base == NULL) {
event_warn(
"%s: received signal %d, but have no base configured",
__func__, sig);
return;
}
evsignal_base->sig.evsigcaught[sig]++;
evsignal_base->sig.evsignal_caught = 1;
#ifndef HAVE_SIGACTION
signal(sig, evsignal_handler);
#endif
/*通过socketpair发送产生信号事件,唤醒处理事件线程进行处理*/
send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
/*恢复errno值*/
errno = save_errno;
}
异步信号处理怎么做
从Nginx和libevent中可以看出,信号处理时只是记录了下产生了什么信号,并没有进行实际处理,处理过程还是交给主程序本身,避免调用一些非信号安全的函数。因此我们再编写信号处理函数的时候也只要记录下信号状态,对errno这种全局变量,进行保存,处理结束后恢复变量值,尽量避免使用锁。
附注:可重入函数,信号安全函数,线程安全函数区别
1.可重入函数是指在任何时候任何地方调用都能保证安全的函数,无论是线程还是信号处理函数,函数一般没有共享变量或者锁之类的东西,linux下系统函数只有80多个可重入函数,可以参考unix环境高级编程一书。
2.线程安全函数是指多个线程同时调用此函数,能保证安全执行,显然可重入函数只是线程安全函数的一个子集。
3.信号安全函数是在信号处理函数中可以安全调用的函数。