前言
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.信號安全函數是在信號處理函數中可以安全調用的函數。