異步信號安全


前言

Linux產生信號中斷,會打斷當前正在執行程序,轉而執行信號處理函數,由於執行信號處理函數時,正常執行程序被掛起,信號處理函數怎么操作才能保證程序再次被喚醒后能夠正常執行,下面我們看兩個案例

案例一

void signal_handler()
{
	printf("this is a test\n");
}

如果我們程序執行中調用malloc, printf等函數時,產生信號后執行上面信號處理函數,由於這些函數中有全局變量或static變量,執行后會破壞全局數據結構,造成不可預測后果。

案例二

void signal_handler()
{
     lock(&mutex);
     printf("this is a test\n");
     unlock(&mutex);
}

void  main()
{
     .....
     lock(&mutex);
     printf("this is a test\n");
     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.信號安全函數是在信號處理函數中可以安全調用的函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM