8.5 信號
研究一種更高層次的軟件形式的異常, 也是一種軟件中斷技即,稱為Unix信號而叼,它允許進程中斷其他進程。
一個信號就是一條小消息液荸,它通知進程系統(tǒng)中發(fā)生一個某種類型的事件脱篙。
Linux系統(tǒng)支持30多種信號伤柄。
每種信號類型對應于某種系統(tǒng)事件
底層的信號文搂。
當?shù)讓影l(fā)生硬件異常,信號通知 用戶進程 發(fā)生了這些異常笔喉。
除以0:發(fā)送SIGILL信號常挚。
非法存儲器引用:發(fā)送SIGSEGV信號
較高層次的軟件事件
鍵入ctrl+c:發(fā)送SIGINT信號
一個進程可以發(fā)送給另一個進程SIGKILL信號強制終止它稽物。
子進程終止或者停止,內核會發(fā)送一個SIGCHLD信號給父進程秧倾。
8.5.1 信號術語
傳送一個信號到目的進程有兩個步驟那先。
發(fā)送信號: 內核通過更新目的進程上下文的某個狀態(tài)赡艰,就說發(fā)送一個信號給目的進程慷垮。
發(fā)送信號有兩個原因
內核檢測到一個系統(tǒng)事件。比如被零除錯誤汤纸,或者子進程終止芹血。
一個進程調用了kill函數(shù)。顯示要求進程發(fā)送信號給目的進程啃擦。
一個進程可以發(fā)信號給它自己饿悬。
接收信號: 當目的進程 被內核強迫以某種方式對信號的發(fā)送做出反應狡恬。目的進程就接收了信號蝎宇。
進程可以忽略這個信號夫啊,終止辆憔。
或者通過一個稱為信號處理程序(signal handler)的用戶層函數(shù)捕獲這個信號虱咧。
一個只發(fā)出而沒有被接收的信號叫做待處理信號(pending signal)
一種類型至多只有一個待處理信號。
如果一個進程有一個類型為k的待處理信號玄坦。
那么接下來發(fā)送到這個進程類型為k的信號都會被簡單的丟棄绘沉。
一個進程可以有選擇性地阻塞接收某種信號
它任然可以被發(fā)送车伞。但是產生的待處理信號不會被接收。
一個待處理信號最多被接收一次困曙。內核為每個進程在pending位向量維護著待處理信號的集合谦去,而在blocked位向量維護著被阻塞的信號集合鳄哭。只要傳送一個類型為k的信號,內核就會設置pending中的第k位锄俄,而只要接收了一個類型為k的信號飘痛,內核就會清除pending中的第k位宣脉。
8.5.2 發(fā)送信號
Unix系統(tǒng) 提供大量向進程發(fā)送信號的機制剔氏。所有這些機制都是基于進程組(process group)。
進程組
每個進程都屬于一個進程組塑陵。
由一個正整數(shù)進程組ID來標示
getpgrp()函數(shù)返回當前進程的進程組ID:
#include<unistd.h>
pid_t getpgrp(void);
1
2
默認蜡励,一個子進程和它的父進程同屬于一個進程組
一個進程可以通過setpgid()來改變自己或者其他進程的進程組凉倚。
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
如果pid是0 ,那么使用當前進程的pid扮碧。
如果pgid是0杏糙,那么使用指定的pid作為pgid(即pgid=pid)宏侍。
例如:進程15213調用setpgid(0,0)
那么進程15213會 創(chuàng)建/加入進程組15213.
1
2
3
4
5
6
7
用/bin/kill 程序發(fā)送信號
/bin/kill可以向另外的進程發(fā)送任意的信號。
比如
unix>/bin/kill -9 15213
1
發(fā)送信號9(SIGKILL)給進程15213漫蛔。
一個為負的PID會導致信號被發(fā)送到進程組PID中的每個進程旧蛾。
unix>/bin/kill -9 -15213
1
發(fā)送信號9(SIGKILL)給進程組15213中的每個進程锨天。
用/bin/kill的原因是,有些Unix shell 有自己的kill命令
從鍵盤發(fā)送信號
作業(yè)(job) :對一個命令行求值而創(chuàng)建的進程搂赋。
在任何時候至多只有一個前臺作業(yè)和0個或多個后臺作業(yè)
前臺作業(yè)就是需要等待的
后臺作業(yè)就是不需要等待的
鍵入unix>ls|sort
創(chuàng)建一個兩個進程組成的前臺作業(yè)脑奠。
兩個進程通過Unix管道鏈接幅慌。
shell為每個作業(yè)創(chuàng)建了一個獨立的進程組。
進程組ID取自作業(yè)中父進程中的一個齿诞。
在鍵盤輸入ctrl-c 會發(fā)送一個SIGINT信號到外殼祷杈。外殼捕獲該信號。然后發(fā)送SIGINT信號到這個前臺進程組的每個進程宿刮。在默認情況下私蕾,結果是終止前臺作業(yè)
類似是目,輸入ctrl-z會發(fā)送一個SIGTSTP信號到外殼,外殼捕獲這個信號揉抵,并發(fā)送SIGTSTP信號給前臺進程組的每個進程嗤疯,在默認情況茂缚,結果是停止(掛起)前臺作業(yè)(還是僵死的)
用kill函數(shù)發(fā)送信號
進程通過調用kill函數(shù)發(fā)送信號給其他進程,類似于bin/kill
int kill(pid_t pid, int sig);
1
pid>0,發(fā)送信號sig給進程pid
pid<0,發(fā)送信號sig給進程組abs(pid)
事例:kill(pid,SIGKILL)
用alarm函數(shù)發(fā)送信號
進程可以通過調用alarm函數(shù)向它自己SIGALRM信號龟糕。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次鬧鐘剩余的秒數(shù)讲岁。
1
2
3
4
5
alarm函數(shù)安排內核在secs秒內發(fā)送一個SIGALRM信號給調用進程
如果secs=0 那么不會調度鬧鐘衬以,當然不會發(fā)送SIGALRM信號看峻。
在任何情況,對alarm的調用會取消待處理(pending)的鬧鐘溪窒,并且會返回被取消的鬧鐘還剩余多少秒結束。如果沒有pending的話,返回0
一個例子:
輸出
unix> ./alarm
BEEP
BEEP
BEEP
BEEP
BEEP
BOOM!
//handler是一個自己定義的信號處理程序惜浅,通過signal函數(shù)捆綁坛悉。
1
2
3
4
5
6
7
8
8.5.3 接收信號
信號的處理時機是在從內核態(tài)切換到用戶態(tài)時裸影,會執(zhí)行do_signal()函數(shù)來處理信號
當內核從一個異常處理程序返回军熏,準備將控制傳遞給進程p時,它會檢查進程p的未被阻塞的待處理信號的集合(pening&~blocked)均践。
如果這個集合為空彤委,內核將控制傳遞到p的邏輯控制流的下一條指令或衡。
如果非空封断,內核選擇集合中某個信號k(通常是最小的k),并且強制p接收k椒涯。收到這個信號會觸發(fā)進程某些行為回梧。一旦進程完成行為狱意,傳遞到p的邏輯控制流的下一條指令。
每個信號類型都有一個預定義的默認類型财骨,以下幾種.
進程終止
進程終止并轉儲存器(dump core)
進程停止直到被SIGCONT信號重啟
進程忽略該信號
進程可以通過使用signal函數(shù)修改和信號相關聯(lián)的默認行為隆箩。
SIGSTOP,SIGKILL是不能被修改的例外。
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
1
2
3
4
signal函數(shù)通過下列三種方式之一改變和信號signum相關聯(lián)的行為杨蛋。
如果handler是SIG_IGN,那么忽略類型為signum的信號
如果handler是SIG_DFL,那么類型為signum的信號恢復為默認行為逞力。
否則糠爬,handler就是用戶定義的函數(shù)地址执隧,這個函數(shù)稱為信號處理程序
只要進程接收到一個類型為signum的信號,就會調用handler捅膘。
設置信號處理程序:把函數(shù)傳遞給signal改變信號的默認行為滚粟。
調用信號處理程序凡壤,叫捕獲信號
執(zhí)行信號處理程序亚侠,叫處理信號
當處理程序執(zhí)行它的return語句后,控制通常傳遞回控制流中進程被信號接收中斷位置處的指令箕别。
信號處理程序是計算機并發(fā)的又一個示例滞谢。信號處理程序的執(zhí)行中斷狮杨,類似于底層異常處理程序中斷當前應用程序的控制流的方式。因為信號處理程序的邏輯控制流與主函數(shù)的邏輯控制流重疊清寇,信號處理程序和主函數(shù)并發(fā)地運行华烟。
自我思考:信號是一種異常/中斷,當接收到信號的時候负饲,會停下當前進程所做的事,立馬去執(zhí)行信號處理程序衩藤。并不是多線程/并行,但確是并發(fā)的涛漂。從下面這張圖匈仗,可見一斑。
8.5.4 信號處理問題
當一個程序要捕獲多個信號時间狂,一些細微的問題就產生了鉴象。
待處理信號被阻塞
Unix 信號處理程序通常會阻塞 當前處理程序正在處理 的類型的待處理信號何鸡。
待處理信號(被拋棄了)不會排隊等待
當有兩個同類型信號都是待處理信號時骡男,有一個會被拋棄隔盛。
關鍵思想:存在一個待處理的信號k僅僅表明至少一個一個信號k到達過。
系統(tǒng)調用可以被中斷(在某些unix系統(tǒng)會出現(xiàn))
像read,wait和accept這樣的系統(tǒng)調用潛在的阻塞一段較長的時間已亥,稱為慢速系統(tǒng)調用虑椎。
當處理程序捕獲一個信號,被中斷的慢速系統(tǒng)調用在信號處理程序返回后將不在繼續(xù)传趾,而是立即返回給用戶一個錯誤條件泥技,并將errno設置為EINTR珊豹。
用一個后臺回收僵死子進程的程序,前臺讀入做例子
1.初始簡單利用接收SIGCHLD信號回收蜕便,一次調用只回收一個轿腺。
在調用的過程中丛楚,又有信號發(fā)送過來,但是被阻塞了仿荆。之后又被直接拋棄坏平。
如果不處理被阻塞和不會排隊等待的問題赖歌。會有信號被拋棄。
重要教訓:不可以用信號對其他進程中發(fā)送的事件計數(shù)
handle1-code
2.一次調用盡可能的多回收功茴,保證在回收過程中庐冯,沒有遺漏的信號。
handle2-code
3.還存在一個問題坎穿,在前臺中展父,某些unix系統(tǒng)(Solaris系統(tǒng))的read被中斷后不會自動重啟,需要手動重啟玲昧,Linux一般會自動重啟栖茉。
之前 read模塊 code
現(xiàn)在改為如果是errno==EINTR手動重啟孵延。
或者使用Signal包裝函數(shù)標準吕漂。8.5.5會提到。
8.5.5 可移植的信號處理
不同系統(tǒng)之間尘应,信號處理語義的差異(比如一個被中斷的慢速系統(tǒng)調用是重啟惶凝,還是永久放棄)是Unix信號系統(tǒng)的一個缺陷吼虎。
為了處理這個問題,Posix標準定義了sigaction函數(shù)苍鲜,它允許與Linux和Solaris這樣與Posix兼容的系統(tǒng)上的用戶思灰,明確指明他們想要的信號處理語義。
#include<signal.h>
int sigaction(int signum,stuct sigaction *act,struct sigaction *oldcat);
//若成功則為1混滔,出錯則為-1洒疚。
1
2
3
4
sigaction函數(shù)應用不廣泛,它要求用戶設置多個結構條目坯屿。
一個更簡潔的方式油湖,是定義一個包裝函數(shù),稱為Signal,它調用sigaction领跛。
它的調用方式與signal函數(shù)的調用方式一樣乏德。
Signal包裝函數(shù)設置了一個信號處理程序,其信號處理語義如下(設置標準):
只有這個處理程序當前正在處理的那種類型被阻塞隔节。
和所有信號實現(xiàn)一樣鹅经,信號不會排隊等待寂呛。
只要可能怎诫,被中斷的系統(tǒng)調用會自動重啟
一旦設置了信號處理程序,它就會一直保持贷痪,直到Signal帶著handler參數(shù)為SIG_IGN或者SIG_DFL被調用幻妓。
在某些比較老的Unix系統(tǒng),信號處理程序被使用一次后劫拢,又回到默認行為肉津。
8.5.6 顯示地阻塞和取消阻塞信號
通過sigprocmask函數(shù)來操作。
#include<signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
1
2
3
sigprocmask函數(shù)改變當前已阻塞信號的集合(8.5.1節(jié)描述的blocked位向量)舱沧。
具體行為依賴how值
SIG_BLOCK:添加set中的信號到blocked中妹沙。
SIG_UNBLOCK: 從blocked刪除set中的信號。
SIG_SETMASK: blocked=set熟吏。
如果oldset非空距糖,block位向量以前的值會保存到oldset中。
還有以下函數(shù)操作set集合
#include<signal.h>
int sigemptyset(sigset_t *set);
//置空
int sigfillset(sigset_t *set);
//每個信號全部填入
int sigaddset(sigset_t *set,int signum);
//添加
int sigdelset(sigset_t *set,int signum);
//刪除
//成功輸出0牵寺,出錯輸出-1
int sigismember(const sigset_t *set,int signum);
//判斷
//若signum是set的成員悍引,輸出1,不是輸出0帽氓,出錯輸出-1趣斤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
8.5.7 同步流以避免討厭的并發(fā)錯誤
如何編寫讀寫相同存儲位置的并發(fā)流程序的問題,困擾著數(shù)代計算機科學家黎休。
流可能交錯 的數(shù)量是與指令數(shù) 量呈指數(shù)關系
有些交錯會產生正確結果浓领,有些可能不會玉凯。
所謂同步流就是。以某種方式同步并發(fā)流镊逝,從而得到 最大的可行交錯的集合 壮啊,每個交錯集合都能得到正確的結果。
并發(fā)編程是一個很深奧撑蒜,很重要的問題歹啼。在第12章詳細討論。
現(xiàn)在我們只考慮一個并發(fā)相關的智力挑戰(zhàn)座菠。
code
如果發(fā)生以下情況狸眼,會出現(xiàn)同步錯誤。
父進程執(zhí)行fork函數(shù)浴滴,內核調度新創(chuàng)建的子進程運行拓萌,而不是父進程。
在父進程再次運行前升略,子進程已經(jīng)終止微王,變成僵死進程,需要內核一個SIGCHLD信號給父進程
父進程處理信號品嚣,調用deletejob.
調用addjob炕倘。
顯然deletejob必須在addjob之后,不然添加進去的job永久存在翰撑。這就是同步錯誤罩旋。
這是一個稱為競爭(race)的經(jīng)典同步錯誤的示例。
main中的addjob和處理程序中調用deletejob之間存在競爭眶诈。
必須addjob贏得進展涨醋,結果才是正確的,否則就是錯誤的逝撬。但是addjob不一定能贏浴骂,所以有可能錯誤。即為同步錯誤宪潮。
因為內核的調度問題溯警,這種錯誤十分難以被發(fā)現(xiàn)。難以調試坎炼。
Q:如何消除競爭愧膀?
A:可以在fork之前,阻塞SIGCHLD信號谣光,在調用addjob后取消阻塞檩淋。
注意,子進程繼承了阻塞,我們要小心地接觸子進程中的阻塞蟀悦。
消除競爭的原則就是媚朦,讓該贏得競爭的對象在任何情況下都能贏。
一個暴露你的代碼中競爭的簡便技巧
制造一個fork的包裝函數(shù)Fork日戈,通過隨機+休眠询张,在fork的那一瞬間,讓子進程浙炼,父進程都有50%機會先運行
8.6 非本地跳轉
C語言提供一種用戶級異撤菅酰控制流形式,稱為非本地跳轉(nonlocal jump)弯屈。
它將控制直接從一個函數(shù)轉移到另一個當前正在執(zhí)行的函數(shù)蜗帜。不需要經(jīng)過正常的調用-返回序列。
非本地跳轉是通過setjmp和longjmp函數(shù)來提供资厉。
#include<setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env,int savesigs);//信號處理程序使用
//參數(shù)savesigs若為非0則代表擱置的信號集合也會一塊保存
1
2
3
4
5
setjmp函數(shù)在env緩沖區(qū)保存當前調用環(huán)境厅缺,以供后面longjmp使用,并返回0
調用環(huán)境包括程序計數(shù)器宴偿,棧指針湘捎,通用目的寄存器。
#include
8.7 操作進程的工具
STRACE(痕跡):打印一個正在運行的程序和它的子進程調用的每個系統(tǒng)調用的軌跡窄刘。
用-static編譯窥妇,能得到一個更干凈,不帶有大量共享庫相關的輸出的軌跡都哭。
PS(Processes Status): 列出當前系統(tǒng)的進程(包括僵死進程)
TOP(因為我們關注峰值的幾個程序秩伞,所以叫TOP):打印當前進程使用的信息逞带。
PMAP(rePort Memory map of A Process): 查看進程的內存映像信息
/proc:一個虛擬文件系統(tǒng)欺矫,以ASCII文本格式輸出大量內核數(shù)據(jù)結構。
用戶程序可以讀取這些內容展氓。
比如穆趴,輸入"cat /proc/loadavg,觀察Linux系統(tǒng)上當前的平均負載遇汞。
8.8 小結
異澄疵茫控制流(ECF)發(fā)生在計算機系統(tǒng)的各個層次,是計算機系統(tǒng)中提供并發(fā)的基本機制空入。
在硬件層络它,異常是處理器中的事件出發(fā)的控制流中的突變⊥嵊控制流傳遞給一個異常處理程序化戳,該處理程序進行一些處理,然后返回控制被中斷的控制流埋凯。
有四種不同類型的異常:中斷点楼,故障扫尖,終止和陷阱。
定時器芯片或磁盤控制器掠廓,設置了處理器芯片上的中斷引腳時换怖,中斷會異步發(fā)生。返回到Inext
一條指令的執(zhí)行可能導致故障和終止同時出現(xiàn)蟀瞧。
故障可能返回調用指令沉颂。
終止不將控制返回。
陷阱用于系統(tǒng)調用悦污。結束后兆览,返回Inext
在操作系統(tǒng)層,內核用ECF提供進程的基本概念塞关。進程給應用兩個重要抽象:
邏輯控制流
私有地址空間
在操作系統(tǒng)和應用程序接口處抬探,有子進程,和信號帆赢。
最后小压,C語言的非本地跳轉 完成應用程序層面的異常處理。
至此椰于,異常貫穿了從底層硬件怠益,到抽象的軟件層次。