异步信号安全

前言

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.信号安全函数是在信号处理函数中可以安全调用的函数。