
信號就是一條消息,通知進程系統中發生了什么事,每種信號都對應著某種系統事件。一般的底層硬件異常是由內核的異常處理程序處理的,它對用戶進程來說是透明的。而信號機制,提供了一種方法通知用戶進程發生了這些異常。
例如,一個進程試圖除0,會引發內核向他發送SIGFPE信號;執行非法指令會引發SIGILL信號;非法內存訪問引發SIGSEGV;當你從鍵盤上鍵入Ctrl + C會引發SIGINT;當某個子進程結束會引發內核向其父進程發送SIGCHLD信號,等等。具體請看下圖:

1. 信號術語與原則
1.1信號發送
當內核檢測到某種系統事件(除零錯誤或子進程終止等等)或一個進程調用了kill函數顯式的要求內核發送一個信號給目的進程時,內核會通過更新目的進程上下文中的某個狀態而達到向它發送一個信號的目的。發送信號的方式為:
- 命令行:用
kill -signum PID
命令,向進程號為PID的進程發送signum信號; - 鍵盤:通過鍵盤發送特定信號,Ctrl + C 向前臺進程組中的每個進程發送SIGINT終止信號;Ctrl + Z 向前臺進程組中的每個進程發送SIGTSTP暫停信號;
- 函數alarm: 使內核在一段時間(secs秒)后,向自己發送SIGALRM信號;
```c #include
unsigned int alarm(unsigned int secs); //返回:待處理的鬧鐘在被發送前還剩余的秒數,若之前沒有待處理的鬧鐘,則返回0 //若secs = 0,不會調度安排新的鬧鐘。 ```
函數kill:進程通過調用kill函數發送信號給其它進程(包括自己)。
```c #include #include
int kill(pid_t pid, int sig); //成功返回0,失敗返回-1。 ```
- pid > 0 :發送信號sig給進程pid;
- pid = 0 :發送信號給自己所在進程組中的每個進程,包括自己。
- pid < 0 :發送信號sig給進程-pid。
1.2 信號處理
當進程從系統調用返回或是完成了一次上下文切換而重新取得控制權之前,內核會檢查該進程的待處理信號集(pengding&(~blocked)),如果為空則完成控制權的交接,如果不為空則會讓進程響應該信號集合中信號值最小的那個信號。
目的進程收到信號后有“忽略信號”、“終止進程”和“捕獲信號“這3種方式來響應。其中
- SIGKILL(終止)和SIGSTOP(暫停)這2個信號不可被忽略,也不能像其它信號一樣可以通過signal函數改變他們的默認處理函數;
```c #include typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); ```
如果handler = SIG_IGN,那么就忽略類型為signum的信號;
如果handler = SIG_DFL,那么就恢復類型為signum的信號的默認行為;
否則,handler就是用戶自定義的信號處理函數地址。
- 進程可以有選擇的忽略某些信號(通過將blocked位向量中相應的位置1),即該信號雖然被內核或進程發送了過來,但我可以選擇視而不見。
```c #include
int sigprocmask(int HOW, const sigset_t set, sigset_t oldset);
int sigemptyset(sigset_t set); //初始化set集為空(set = 0); int sigfillset(sigset_t set); //將所有信號都添加進set集(set = 1); int sigaddset(sigset_t set, int signum); int sigdelset(sigset_t set, int signum); //以上5個函數成功返回0,錯誤返回-1 int sigismember(const sigset_t *set, int signum); //是成員返回1,不是返回0,錯誤返回-1 ```
關于sigprocmask函數中的"HOW"有以下幾種可能的取值:
- SIG_BLOCK:把set集中的信號加到進程的blocked中(blocked |= set);
- SIG_UNBLOCK:從進程的blocked中刪除set集中的信號(blocked &= ~set);
- SIG_SETMASK:忽略set集中的信號(blocked = set);
oldset : 如果他是非空的,則將進程原先blocked的值保存在其中。
一下示例展示了臨時忽略SIGINT信號的程序片段:
```c sigset_t mask, oldmask;
sigemptyset(&mask); sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &oldmask); . . //此處的所有語句將不會響應SIGINT信號 . sigprocmask(SIG_SETMASK, &oldmask, NULL); //之后的語句將會正常響應SIGINT信號 ```
- 任何信號只能被記錄阻塞一次;即如果進程正在執行某類型信號的處理函數,那么在此進程返回主程序前,不管又收到了多少個該類型的信號,它只會被記錄一次(即等到該進程從上次處理函數返回后,它只會再響應一次該類型的信號)。因為內核為每個進程在pending位向量中維護著待處理信號的集合,而在blocked位向量中維護著被阻塞的信號集。每次,收到一個信號,就在blocked相應的位置1,響應一個信號,就在pengding中相應的清零。
2. 安全的信號處理函數
由于信號處理函數和主程序是并發運行的,他們享有相同的全局變量,他們的運行順序是不可預測的,這就導致何時接收到信號的規則往往有違人們的直覺,或者說主程序和子程序間不一定會按照你預想的順序去執行。所以為了防止競爭冒險,在編寫信號處理函數時有幾個保守的原則需要遵守:
- 處理程序盡可能簡單;
- 在處理程序中僅使用異步安全的函數,也就是說該函數是可重入的(只訪問局部變量)且不能中斷;下圖列出了所有Linux保證安全的系統函數,可以發現許多常見的庫函數(printf、sprintf、malloc、exit等)都不是安全函數,在編寫信號處理函數時要盡量避免使用。

為了在信號處理程序中能夠打印一些簡單的消息,我們可以使用一些異步信號安全的系統函數來構建自己的特有包裝函數。作為例子,下面的程序展示了利用異步信號安全的系統函數write編寫自己的SIO(safe I/O)函數。c ssize_t sio_puts(char s[]) { int count = 0; char *str = s; if(!str) _exit(1); while(*str++) count++; return write(STDOUT, s, count); }
- 保存和恢復error;為了避免處理程序中某些語句的出錯導致error被設置,進而影響主程序中的判斷,在信號處理程序的第一條語句保存原error,在它返回前恢復error。
- 不管是主程序還是子程序,在訪問全局變量時,都要阻塞所有的信號,以防相互干擾。
- 用volatile聲明全局變量。 volatile要求編譯器每次都是從內存中讀取全局變量的值,而非從緩存中。
- 使用sig_atomic_t聲明標志。 此處的標志代表在主程序和子程序間傳遞信號的全局變量,因為sig_atomic_t要求編譯器對它的操作是原子的,所以即使沒有阻塞所有信號,它也不會被任何信號打斷。
- 使用sigaction函數重新包裝signal函數,使得系統自動重啟被中斷的系統調用。 由于一些系統函數(例如read、write、accept等)需要執行較長時間,所以可能會被信號中斷。而在許多較早以前版本的Unix系統中,被中斷的系統調用并不會在信號處理返回后重啟,而是直接返回錯誤并將error設置為EINTR。而sigaction函數可以設置信號處理時的語義。
以下代碼用sigaction函數編寫了signal函數的包裝函數[Signal][1],并且具有如下語義:
- 只有當前處理的該類型信號被阻塞;
- 其它信號也不會排隊等待;
- 只要可能,被中斷的系統調用會自動重啟;
- 一旦為某信號設置了信號處理程序,它會一直保持到Signal重新為該信號設置SIG_IGN或SIG_DFL的信號處理程序。
``` handler_t Signal(int signum, handler_t handler) { struct sigaction action,oldaction;
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESTART;if(sigaction(signum, &action, &oldaction) < 0)unix_error("Signal error");
return(oldaction.sa_handler)
} ```
3.信號的同步
當需要編寫讀寫相同內存位置的并發進程,我們不得不考慮進程間的(既包括進程與進程之間,也包括主進程與子進程之間)競爭關系。這是一個很大的命題,在此限于文章主題,只討論信號之間的競爭關系如何處理。主要分兩個方面,一是隱式競爭,二是顯式競爭。
3.1 避免隱式競爭
考慮一個類似shell的函數功能,父進程在一個全局作業列表中記錄著它的當前子進程,每個作業一個條目。addjob和deletejob函數分別向這個作業列表中添加和刪除作業。父進程每創建一個子進程就把它添加在作業列表中,每當在SIGCHLD信號處理程序中回收一個僵死的子進程時,就在job列表中刪除這個子進程。
void handler(int sig)
{int olderrno = errno; //保存進程的原error值sigset_t mask_all,prev_all;pid_t pid;sigfillset(&mask_all); //將所有信號添加到信號集mask_all中while((pid = waitpid(-1, NULL, 0)) > 0){ //回收僵死子進程sigprocmask(SIG_BLOCK, &mask_all, prev_all); //阻塞(屏蔽)所有信號deletejob(pid); //從job列表中刪除僵死的子進程條目sigprocmask(SIG_SETMASK, &prev_all, NULL);}if(errno != ECHILD) //如果父進程的所有子進程都已經回收,則內核發送ECHILD錯誤Unix_error("waitpid error");errno = olderrno; //恢復進程的原error值
}int main(int argc, char **argv)
{int pid;sigset_t mask_all,mask_one,prev_one;sigfillset(mask_all);sigemptyset(mask_one);sigaddset(&mask_one, SIGCHLD); Signal(SIGCHLD, handler); //使用安全的Signal函數設置處理函數initjobs(); //初始化工作列表while(1){/*在產生子進程前屏蔽SIGCHLD,以防止主進程還沒執行到addjob就已經收到了因子進程終止而發來的SIGCHLD信號,進而進入handler導致在jobs中找不到要刪除的子進程條目*/sigprocmask(SIG_BLOCK, &mask_one, &prev_one); //頻閉SIGCHLD信號if((pid = fork()) == 0){sigprocmask(SIG_SETMASK, &prev_one, NULL); //子進程解除頻閉SIGCHLDexecve("/bin/date", argv, NULL);}sigprocmask(SIG_BLOCK, &mask_all, NULL); //父進程屏蔽所有信號addjob(pid); sigprocmask(SIG_SETMASK, &prev_one, NULL); //父進程解除屏蔽}exit(0);
}
3.2 避免顯式競爭
有時候主程序需要顯式地等待某個信號處理運行。例如shell程序,它必須等待當前的前臺進程結束,被SIGCHLD處理程序回收之后,才能繼續創建另一個進程。主進程在等待的這段時間應該干些什么才最好呢?我們可以用一個無限循環語句,讓主進程就在那執行。但這樣也太浪費CPU的資源了;我們也可以用一個sleep或者nanosleep函數讓主進程休眠,但到底休眠多長時間不好把握,間隔太小同樣會造成多次循環,間隔太大,程序又會太慢。
合適的解決辦法是,引入sigsuspend函數:
#include <signal.h>int sigsuspend(const sigset_t *mask); //返回-1
它暫時掛起調用它的進程,利用參數mask替換當前的信號阻塞集,直到收到一個信號并進入處理程序(如果是終止信號,就直接返回),處理完之后返回主進程,并恢復原來的阻塞集。
下面例子展示了主進程在創建完子進程后,如何利用該函數顯式的等待SIGCHLD的到來,以達到同步的效果。
#include <signal.h>volatile sig_atomic_t pid;void sigchld_handler(int signum)
{int olderror = errno;pid = waitpid(-1, NULL, 0);int errno = olderrno;
}void sigint_handler(int signum)
{}int main(int argc, char **argv)
{sigset_t mask,prev;Signal(SIGCHLD, sigchld_handler);Signal(SIGINT, sigint_handler);sigemptyset(&mask);sigaddset(&mask, SIGCHLD);while(1){sigprocmask(SIG_BLOCK, &mask, &prev); //屏蔽SIGCHLD信號if(fork() == 0) //子進程exit(0);pid = 0;while(!pid){ sigsuspend(&prev); //掛起并等待SIGCHLD信號的到來,其處理函數會使得pid大于0}sigprocmask(SIG_SETMASK, &prev, NULL);printf("...");}exit(0);
}
[1]: 引用:Unix Network Programming: The Sockets Networking API,第三版,第一卷
************************************************
嵌入式 Linux C ARM - 專題 - 簡書?www.jianshu.com
