訊號是軟體中斷所提供的用於處理非同步時間的一種機制,它有一個非常精確的生命週期。首先是一個時間引發一個訊號,然後核心將該訊號儲存起來,直到被傳遞出去,最後核心在適當的時候處理該訊號。核心處理訊號有三種方式:
- 忽略,但是SIGKILL和SIGSTOP是不能被忽略的。
- 捕獲並處理,核心暫停當前程式的執行,跳躍到一個先前註冊的一個函數,進程執行該函數,然後返回到暫停地方。
- 執行預設操作,具體的操作取決有訊號類型,預設操作通常是終止進程。
static void sigwinch_handler(int signo){
printf("window size has been changed\n");
}
int main(){
if(signal(SIGWINCH, sigwinch_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
for(;;)
pause();
return 0;
}
上面這段代碼是一個簡單的訊號的例子:在終端視窗的大小發生變化的時候就會輸出"window size has been changed",也就是說把SIGWINCH訊號交給sigwinch_handler來處理了。
bool is_child;
bool has_fork;
static void sigwinch_handler(int signo){
if(is_child == true)
printf("child:\n");
else
printf("parent:\n");
printf("window size has been changed\n");
}
static void sigint_handler(int signo){
has_fork = true;
if(is_child == true)
printf("child:");
else
printf("parent:");
printf("int\n");
}
int main(){
has_fork = false;
if(signal(SIGWINCH, sigwinch_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
if(signal(SIGINT, sigint_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
while(has_fork == false){}
int pid = fork();
if(pid == 0)
is_child = true;
else
is_child = false;
for(;;)
pause();
return 0;
}
從上面的這段代碼中可以看到fork對訊號處理的影響。fork之後子進程也會接受到核心發給父進程的訊號,但是對於父進程已經接受過的訊號子進程是不會重複地去處理一次。
static void sigint_heandler(int signo){
printf("i get a signal: SIGINT\n");
}
int main(){
if(signal(SIGINT, sigint_heandler) == SIG_ERR){
printf("signal fail.\n");
exit(-1);
}
int pid = fork();
if(pid != 0){
if(kill(pid, SIGINT) == 0){
printf("send SIGINT success.\n");
}else{
printf("send fail.\n");
}
}
for(;;)
pause();
return 0;
}
訊號的引發方式很多,不僅僅是在程式啟動並執行時候我們在控制台按下"Ctrl+c"才能引發SIGINT,還可以在程式啟動並執行時候通過程式來發送,上面的代碼就是一個小例子。通過fork產生一個子進程後向它發送SIGINT訊號。當然這個是有許可權問題的,只有具有CAP_KILL能力的進程才能隨意地給其他進程發送各種訊號,如果不具備該能力的進程可以給相同的使用者的進程發送訊號。
如果在kill調用的時候pid=0,則signo被發送給它所屬的進程組。如果pid=-1,那範圍就更大了,除了它本身和init除外,能發送的進程都發送一次。而如果pid<-1,那麼就會給進程組-pid發送signo。signo=0為空白訊號,雖然什麼都不會做,但是仍然在發送的時候會進行錯誤偵測。所以可以通過它來檢測進程的許可權。
在想要給自己發送訊號的時候kill當然是能做到的,不過還有另外一個選擇:raise。而給進程組發送訊號也可以通過killg代替。
如果就像剛才的訊號處理常式那樣隨意的使用全域變數(當然在這個程式中是沒什麼大問題的)很可能就會引發大問題,因為核心發送一個訊號的時候根本不知道進程在執行什麼樣的代碼。所以在訊號處理常式中啟動並執行代碼必須保證做進行的操作或調用的函數不會出現多個並行啟動並執行時候引發問題,也就是要可重新進入。
static void sigint_handler(int signo){
printf("sigint_handler get a signal.\n");
}
void printset(sigset_t *sset){
for(int i = 0, *p = (int*)sset; i < sizeof(*sset)/sizeof(int); i++)
printf("%08x%c", *(p+i), (i%8!=7)?'':'\n');
printf("\n");
}
int main(){
if(signal(SIGINT, sigint_handler) == SIG_ERR){
printf("signal fail.\n");
exit(-1);
}
sigset_t *sset = (sigset_t*)malloc(sizeof(sigset_t));
sigfillset(sset);
printset(sset);
sigemptyset(sset);
printset(sset);
sigaddset(sset, SIGINT);
printset(sset);
int ret = sigismember(sset, SIGINT);
if(ret == 1){
printf("SIGINT is in set.\n\n");
}
ret = sigprocmask(SIG_SETMASK, sset, NULL);
if(ret == 0){
printf("set success.\n\n");
}
sleep(5);
ret = sigpending(sset);
printset(sset);
sigdelset(sset, SIGINT);
ret = sigprocmask(SIG_SETMASK, sset, NULL);
if(ret == 0)
printf("change success.\n");
return 0;
}
那在處理臨界區的代碼的時候不想讓訊號打斷應該怎麼處理呢?上面的代碼可以看出大概的方法:首先用sigprocmask來阻擋一組訊號的接受,在離開臨界區的時候再對這些訊號解除阻塞,那麼這些訊號就會在這時候被處理。
void do_sth(int signo, siginfo_t *info, void *context){
printf("do_sth.\n");
}
int main(){
struct sigaction sig;
sig.sa_sigaction = do_sth;
sig.sa_flags = SA_SIGINFO;
sigemptyset(&(sig.sa_mask));
int ret = sigaction(SIGINT, &sig, NULL);
while(true){
pause();
}
return 0;
}
與signal相比,sigaction有更大的靈活性,不過在一起的一篇aio的部落格中已經介紹過該函數的用法了,所以在這裡就不重複了。而如果想給指定的進程發送訊號可以用sigqueue來完成,同樣比kill更強大。
----------------------------
個人理解,歡迎拍磚。