深入研究signal和sigaction
訊號通常使用一個無符號長整數(32位)中的位元位表示各種不同的訊號。因此最多可以表示32個不同的訊號。signal()和sigaction()的功能比較類似,都是更改訊號原處理控制代碼(handler,或稱為處理程式)。但signal()就是核心操作上述傳統訊號的方式,在某些特殊時刻可能會造成訊號丟失。
之前介紹過signal()
函式,signal()函式
的返回值是一個無返回值且具有一個整型引數的函式指標,是預設的處理方式。並且在新控制代碼被呼叫執行過一次後,訊號處理控制代碼又會被恢復成預設處理控制代碼值SIG_DFL。
在linux0.11原始碼中,include/signal.h
檔案中,預設控制代碼SIG_DFL和忽略處理控制代碼SIG_IGN的定義是:
#define SIG_DFL ((void (*)(int))0)
#define SIG_IGN ((void (*)(int))1)
signal()函式不可靠的原因在於當訊號已經發生而進入自己設定的訊號處理函式中,但在重新再一次設定自己的處理控制代碼之前,在這段時間內有可能又有同樣的訊號發生了。但是此時系統已經把處理控制代碼設定成預設值。因為就有可能造成訊號丟失。
為了防止訊號的丟失,sigaction()
函式是比signal()
函式更安全的選擇。如果堅持要用signal()
,可以在自己的訊號處理函式開頭再重新呼叫signal()
函式做相同的處理。
而sigaction()
signal()
函式更可靠的原因是,在訊號處理函式正在處理時,被捕捉的訊號在處理期間會被自動遮蔽,並且用struct sigaction
結構體中的void (*sa_handler)(int)
成員修改了訊號的處理方式之後,除非再改回來,否則就一直使用修改之後的處理函式。
下面附上signal()
函式在核心中的系統呼叫程式碼及註釋:
int sys_signal(int signum, long handler, long restorer) { struct sigaction tmp; //用來代替當前程序對signum的處理方式以及訊號遮蔽字 if (signum<1 || signum>32 || signum==SIGKILL) //當signum不合法時直接返回-1,SIGKILL不能被改變 return -1; tmp.sa_handler = (void (*)(int)) handler; //設定控制代碼為傳入的handler tmp.sa_mask = 0; //遮蔽字設定為什麼都不遮蔽 tmp.sa_flags = SA_ONESHOT | SA_NOMASK; //設定屬性,SA_ONESHOT即使用一次就恢復到預設值,SA_NOMASK表示設定不啟用點陣圖遮蔽訊號,即這個時候可以允許多個相同的訊號一起觸發。 tmp.sa_restorer = (void (*)(void)) restorer;//這個成員起到核心用來保護恢復處理函式的作用 handler = (long) current->sigaction[signum-1].sa_handler; //把當前程序的訊號處理函式儲存到handler中 current->sigaction[signum-1] = tmp; //把當前程序原來的該訊號的處理方式等替換成我們新設定的 return handler; //返回預設處理動作 }
看完了是不是有種原來如此的感覺,接下來是sigaction()
函式在核心中的系統呼叫程式碼及註釋:
int sys_sigaction(int signum, const struct sigaction * action,
struct sigaction * oldaction)
{
struct sigaction tmp; //首先也宣告一箇中間變數
if (signum<1 || signum>32 || signum==SIGKILL) //如果signum不合法直接返回-1,並且SIGKILL不能被改變
return -1;
tmp = current->sigaction[signum-1]; //將中間變數的值賦成signum對應的訊號的相關屬性
//在訊號的sigaction結構中設定新的處理動作
get_new((char *) action,
(char *) (signum-1+current->sigaction));
//這個函式的作用是:如果需要將以前的處理方式傳出的話,就把原來的處理方式儲存到oldaction中
if (oldaction)
save_old((char *) &tmp,(char *) oldaction);
//如果允許訊號在處理中接收到本訊號就令遮蔽碼為0,否則設定遮蔽本訊號
if (current->sigaction[signum-1].sa_flags & SA_NOMASK)
current->sigaction[signum-1].sa_mask = 0;
else
current->sigaction[signum-1].sa_mask |= (1<<(signum-1));
return 0;
}
這樣我們就瞭解了signal()
函式和sigaction()
函式在核心中的實現。但是還有一個更底層的問題,一個訊號是怎麼中斷當前程序,進入到核心並設定處理函式,然後再處理掉的呢。
在核心中,實現這個功能的函式是do_signal()
函式,原始碼及註釋如下:
void do_signal(long signr,long eax, long ebx, long ecx, long edx,
long fs, long es, long ds,
long eip, long cs, long eflags,
unsigned long * esp, long ss)
{
unsigned long sa_handler;
long old_eip=eip; //cs:eip確定了使用者程式中下一條指令執行的位置,old_eip=eip很明顯是將使用者程式中原來即將執行的位置儲存起來
struct sigaction * sa = current->sigaction + signr - 1; //這行程式碼其實和之前的tmp = current->sigaction[signum-1];這行類似,只不過這裡是用的指標,指向的current->sigaction+signr-1,對照著想一下就能理解
int longs;
unsigned long * tmp_esp; //看到esp就知道一定是和棧頂指標有關的
sa_handler = (unsigned long) sa->sa_handler; //將當前程序的處理控制代碼賦值給sa_handler
if (sa_handler==1) //1代表SIG_IGN(忽略),如果還是以前的處理動作,就可以直接返回了
return;
if (!sa_handler) { //如果是SIG_DEL預設處理
if (signr==SIGCHLD) //如果對應的是SIGCHLD(子程序給父程序發的訊號,預設處理動作是忽略)
return;
else //否則終止程序的執行,do_exit()函式是exit()函式的內部實現
do_exit(1<<(signr-1));
}
if (sa->sa_flags & SA_ONESHOT) //如果該訊號控制代碼只需執行一次(sa_flags其實是位掩碼,所以能進行相與操作來判斷),則將該控制代碼置空
sa->sa_handler = NULL;
*(&eip) = sa_handler; //將使用者呼叫系統呼叫的程式碼指標eip指向該訊號處理控制代碼
longs = (sa->sa_flags & SA_NOMASK)?7:8; //如果允許訊號自己的處理控制代碼接收到自己,就也需要把程序的阻塞碼壓入堆疊,7或者8代表棧要下移的值(以4位元組為單位)。
//將棧頂指標向下擴充套件7/8個字長(用來存放呼叫訊號控制代碼的引數等)並檢查記憶體使用情況
*(&esp) -= longs;
verify_area(esp,longs*4);
// 在使用者堆疊中從下到上存放 sa_restorer, 訊號 signr, 遮蔽碼 blocked(如果 SA_NOMASK 置位),
// eax, ecx, edx, eflags 和使用者程式原始碼指標。
tmp_esp=esp;
put_fs_long((long) sa->sa_restorer,tmp_esp++);
put_fs_long(signr,tmp_esp++);
if (!(sa->sa_flags & SA_NOMASK))
put_fs_long(current->blocked,tmp_esp++);
put_fs_long(eax,tmp_esp++);
put_fs_long(ecx,tmp_esp++);
put_fs_long(edx,tmp_esp++);
put_fs_long(eflags,tmp_esp++);
put_fs_long(old_eip,tmp_esp++);
current->blocked |= sa->sa_mask; //程序的遮蔽碼新增上sa_mask上的碼位
}
這裡引用《linux核心完全註釋》書中的一張圖,可以很好的理解do_signal
函式的工作過程。