Linux 訊號signal處理機制
http://oilbeater.com/2012/05/09/linux-signal/
訊號是什麼東西呢?
兩個直觀的感受,你在終端執行一個程式然後摁一下Ctrl+c就是向正在執行的程式傳送了一個終止訊號,程式就被終止了;在終端kill一個pid相當於傳送9號殺死這個程序;在終端執行kill -l
就可以檢視系統的所有訊號。
有了上面這些直觀感受,那麼訊號本質是什麼呢?訊號本質上是一種向一個程序通知發生非同步事件的機制,是在軟體層次上對中斷的一種模擬。這種通知機制可以用於通知硬體訊息like上面的感受1,也可以用來進行程序間通訊like上面的感受2,還可以用來通知一些程式錯誤如除0、非法記憶體訪問。非同步是說程序沒有對訊號進行實時監控,不必等待訊號到來,事實上程序也根本不知道訊號什麼時候會來。一個程序本來在歡樂的跑著突然就被你一個ctrl+c給殺死了,飛來橫禍呀。至於說是一種軟中斷,是因為在原理上,一個程序受到一個訊號與處理器收到一箇中斷請求可以說是一樣的,本來在歡樂的跑著就從你一個腳上給你來一個高電平。
訊號分類
通過kill -l
可以看到linux現在支援64個訊號,注意一下訊號不是從0編號的而是從1編號。其中前32為標準(Standard)訊號,後32為實時(Real-time)訊號。好吧,什麼是標準訊號,什麼又是實時訊號。
在遙遠的古代是隻有標準訊號的,那時候它也不叫標準訊號,就叫訊號,它是一種十分簡單的機制。先說一下訊號的執行,當訊號傳送到程式時並不是立即執行而是等待某個時機再執行,在這個時機還沒到來的時候你一個型別的訊號無論發多少個都只記錄一個,就好比有32個信箱每個信箱只能收一封信,多的就扔吧;另一方面訊號的響應也是不保證順序的,你傳送訊號的順序和訊號響應的順序可能根本就沒什麼關係,因為古代人類都比較簡單嘛。後來人類不斷髮展又想要可以響應一個型別的多個訊號又想保證響應順序,實時訊號就誕生了,其實就是加了個sigqueue這麼個佇列資料結構,需求就被滿足了。但是之前的簡單訊號已經成為了實際上的標準,而實時訊號的應用也還不如前者廣,兩者就共存了。
Tips:實時訊號的訊號範圍由SIGRTMIN和SIGRTMAX兩個巨集來決定,程式設計使用實時訊號的話可以使用SIGRTMIN+n指定一個訊號而不是直接一個數字,因為萬一標準訊號數量又增加呢,直接寫數字編碼可能就會出現bug,這兩個巨集也為將來訊號的靈活擴充套件提供了基礎。同時也是灌輸一個程式設計不要使用魔數的原則。
訊號響應
UNIX對前32個訊號都有預設的響應方式,分為以下5類:
- Term:終止程序
- Ign:忽略該訊號
- Core:終止程序並儲存記憶體資訊
- Stop:停止程序
- Cont:有停止就有恢復程序
當然只有5個響應方法怎麼夠呢,not fashion 於是sigaction()這個系統呼叫就上了,通過它可以給一個訊號繫結一個函式來當作訊號處理函式,你就可以在這個函式裡面胡作非為了。可是你胡作非為了核心開發人員又感覺不爽了於是就設了兩個訊號你是改不了的,以顯示他們不可動搖的地位,這兩個訊號就是9號SIGKILL和19號SIGSTOP,所以你也就不能定義Ctrl+c和Ctrl+z傳送出來的訊號的處理方式了。
Tips:當然你足夠邪惡的話可以定義這兩個組合鍵指向別的操作。
訊號處理機制
廢話這麼多終於開始講機制了。
訊號傳送和接收
最簡單的理解,一個程式給另一個程式發了個簡訊,通過中國移不動或者中國聯不通的網路,另一個程式的手機就收到了,一個訊號就算髮送成功了。具體來說就是一個程式呼叫一個傳送訊號的系統呼叫例如kill、raise、sigqueue、alarm、setitimer以及abort。然後核心就扮演運營商的角色把訊號扔給另一個程序。我們知道程序在記憶體裡還是有很多家當的,主要維護了一個程序描述符,裡面有著pid呀,程序狀態呀,優先順序呀一堆不可告人的祕密,等以後有空了我給大家八卦一下,這裡和訊號有勾當的是pending,signal和sighand。
pending和signal是兩個掛起訊號佇列,為什麼要有兩個呀?因為一個是私有的佇列一個是共享的佇列。為什麼有私有和共享之分呀?因為一個是針對輕量級程序的一個是針對執行緒組的?這兩個又是什麼東西呀?本小農發現這裡開始不好說了……簡單說,Linux是沒有程序和執行緒的,有的只有輕量級程序,如果一組輕量級程序之間可以共享資源,那麼就組成一個相當於執行緒組的東西,也就相當於一個執行緒,換句話說Linux是用輕量級程序這個東西模擬多執行緒,感興趣同學可以看一下LWP,總之這裡知道有兩個訊號掛起佇列就好了,如果前面LWP的東西沒看懂,你這裡可以認為一個是記錄的給執行緒的訊號,一個記錄的給程序的訊號。sighand就簡單多了就是記錄64個訊號對應的處理函式的入口地址,當然還有其他好多輔助的資料結構,但主要就是這個功能。如果能大致看懂下面這張圖說明你還沒暈。
回到手機簡訊,核心把簡訊發給程序是幹了什麼事呢?就是找個佇列把訊號插進去。當然程序也是有尊嚴的,不會讓你隨便插的如果是標準訊號的話你只能插一次,如果這個訊號還在的話就不讓你插了,不像對實時訊號那麼隨便想插多少插多少。有的程序比較專一,如果有一個訊號插進來他會設定一個遮蔽位不讓別的訊號插,可以對比一下中斷,處理器有時也會設一箇中斷遮蔽位有木有。
訊號的檢視與處理
簡訊發過來了,不一定就被看到了,這也是非同步說的意思,那麼什麼時候程序才會發現我有一個新的短訊息呢?原來程序是在從核心態這個黑暗的角落到使用者態切換之前偷偷的看一眼簡訊,看看都有誰插了進來,然後把他們處理掉,再回到使用者態光明正大的去接客。那麼什麼時候程序會去核心態這個小黑屋呢,主要有三種情況:
- 執行系統呼叫
- 處理中斷異常
- 程序排程上CPU
出小黑屋前,會看一眼手機,如果有短訊息就啪啪啪的處理簡訊,如果沒有的話就傷感的回用戶態。如果是比較規矩的簡訊只是執行五種規定的標準動作那麼在小黑屋解決就好了,但是有的簡訊比較壞呼叫了sigaction告訴程序要出來到這個地方來玩,然後程序就把手機扔小黑屋裡拔腿就跑到使用者態去玩了,玩完想起來手機還在家,就又回趟家看看還有沒有其他簡訊,沒有再去使用者態光明正大的見人。具體過程如下圖。
好了不調戲程序了,程序也是有尊嚴的,下面討論一下程序的節操問題,節操這個問題確實比較難說,唉……
不管你知不知道,程序從使用者態進入核心態是要再核心態儲存一份使用者態堆疊的副本的,其中最重要的就是儲存當前的pc這樣程序從核心態返回的時候把pc還原就可以按照原來的指令流行進了;不管你知不知道,當程序從核心態回到使用者態的時候這個堆疊的副本是被清空的。於是當程序在收到一個出去玩的簡訊出去之後,他原來的使用者態返回地址就被默默的清空了,然後他玩完回到小黑屋就發現找不到回用戶態的路了,一輩子就被關在這個陰冷黑暗的小黑屋,永世不得見光,這個故事告訴我們節操是很重要的。然而這個恐怖的故事沒有限制住任何一個程序尋歡作樂,程序們出去玩之前先往外發了個訊息把使用者態的返回地址,堆疊資訊什麼的都發出去了,等玩完回家再等小哥把資訊發回來,就又可以光明正大的回用戶態了,所以節操這個東西……
好了,上面都是為了加強理解的段子,下面是正兒八經的原理介紹,需要有對堆疊和函式呼叫機制有一些瞭解,你會發現還是節操比較好說。
我們知道,當程序陷入核心態的時候,會在堆疊中儲存中斷現場。因為使用者態和核心態是兩個執行級別,所以要使用兩個不同的棧。當用戶程序通過系統呼叫剛進入核心的時候,CPU會自動在該程序的核心棧上壓入下圖所示的內容:(圖來自《Linux核心完全註釋》)
在處理完系統呼叫以後,就要呼叫do_signal()函式進行設定frame等工作。這時核心堆疊的狀態應該跟下圖左半部分類似(系統呼叫將一些資訊壓入棧了):
在找到了訊號處理函式之後,do_signal函式首先把核心堆疊中存放返回執行點的eip儲存為old_eip,然後將eip替換為訊號處理函式的地址,然後將核心中儲存的“原ESP”(即使用者態棧地址)減去一定的值,目的是擴大使用者態的棧,然後將核心棧上的內容儲存到使用者棧上,這個過程就是設定frame.值得注意的是下面兩點:
- 之所以把EIP的值設定成訊號處理函式的地址,是因為一旦程序返回使用者態,就要去執行訊號處理程式,所以EIP要指向訊號處理程式而不是原來應該執行的地址。
- 之所以要把frame從核心棧拷貝到使用者棧,是因為程序從核心態返回使用者態會清理這次呼叫所用到的核心棧(類似函式呼叫),核心棧又太小,不能單純的在棧上儲存另一個frame(想象一下巢狀訊號處理),而我們需要EAX(系統呼叫返回值)、EIP這些資訊以便執行完訊號處理函式後能繼續執行程式,所以把它們拷貝到使用者態棧以儲存起來。
以上這些搞清楚之後,下面的事情就順利多了。這時程序返回使用者空間,就會根據核心棧中的EIP值執行訊號處理函式。那麼,訊號處理程式執行完後,怎麼返回程式繼續執行呢?
訊號處理程式執行完畢之後,程序會主動呼叫sigreturn()系統呼叫再次回到核心,檢視有沒有其他訊號需要處理,如果沒有,這時核心就會做一些善後工作,將之前儲存的frame恢復到核心棧,恢復eip的值為old_eip,然後返回使用者空間,程式就能夠繼續執行。至此,核心遍完成了一次(或幾次)訊號處理工作。
參考資料
- 《深入理解linux核心》說實話這本書講的不是很清楚,不過權威的也沒有別的書了
- man signal kill sigaction 說錯了還是有權威的linux自帶的manual
- IBM開發者社群的資料
- linux核心訊號處理機制簡介最後比較嚴肅的東西都是直接從這上面copy的
____________________________________________________________________________________________________________________________________
訊號是Linux程式設計中非常重要的部分,本文將詳細介紹訊號機制的基本概念、Linux對訊號機制的大致實現方法、如何使用訊號,以及有關訊號的幾個系統呼叫。
訊號機制是程序之間相互傳遞訊息的一種方法,訊號全稱為軟中斷訊號,也有人稱作軟中斷。從它的命名可以看出,它的實質和使用很象中斷。所以,訊號可以說是程序控制的一部分。
一、訊號的基本概念
本節先介紹訊號的一些基本概念,然後給出一些基本的訊號型別和訊號對應的事件。基本概念對於理解和使用訊號,對於理解訊號機制都特別重要。下面就來看看什麼是訊號。
1、基本概念
軟中斷訊號(signal,又簡稱為訊號)用來通知程序發生了非同步事件。程序之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程序傳送訊號,通知程序發生了某個事件。注意,訊號只是用來通知某程序發生了什麼事件,並不給該程序傳遞任何資料。
收 到訊號的程序對各種訊號有不同的處理方法。處理方法可以分為三類:第一種是類似中斷的處理程式,對於需要處理的訊號,程序可以指定處理函式,由該函式來處 理。第二種方法是,忽略某個訊號,對該訊號不做任何處理,就象未發生過一樣。第三種方法是,對該訊號的處理保留系統的預設值,這種預設操作,對大部分的信 號的預設操作是使得程序終止。程序通過系統呼叫signal來指定程序對某個訊號的處理行為。
在程序表的表項中有一個軟中斷訊號域,該域中每一位對應一個訊號,當有訊號傳送給程序時,對應位置位。由此可以看出,程序對不同的訊號可以同時保留,但對於同一個訊號,程序並不知道在處理之前來過多少個。
2、訊號的型別
發出訊號的原因很多,這裡按發出訊號的原因簡單分類,以瞭解各種訊號:
(1) 與程序終止相關的訊號。當程序退出,或者子程序終止時,發出這類訊號。
(2) 與程序例外事件相關的訊號。如程序越界,或企圖寫一個只讀的記憶體區域(如程式正文區),或執行一個特權指令及其他各種硬體錯誤。
(3) 與在系統呼叫期間遇到不可恢復條件相關的訊號。如執行系統呼叫exec時,原有資源已經釋放,而目前系統資源又已經耗盡。
(4) 與執行系統呼叫時遇到非預測錯誤條件相關的訊號。如執行一個並不存在的系統呼叫。
(5) 在使用者態下的程序發出的訊號。如程序呼叫系統呼叫kill向其他程序傳送訊號。
(6) 與終端互動相關的訊號。如使用者關閉一個終端,或按下break鍵等情況。
(7) 跟蹤程序執行的訊號。
Linux支援的訊號列表如下。很多訊號是與機器的體系結構相關的,首先列出的是POSIX.1中列出的訊號:
訊號 值 處理動作 發出訊號的原因
----------------------------------------------------------------------
SIGHUP 1 A 終端掛起或者控制程序終止
SIGINT 2 A 鍵盤中斷(如break鍵被按下)
SIGQUIT 3 C 鍵盤的退出鍵被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)發出的退出指令
SIGFPE 8 C 浮點異常
SIGKILL 9 AEF Kill訊號
SIGSEGV 11 C 無效的記憶體引用
SIGPIPE 13 A 管道破裂: 寫一個沒有讀埠的管道
SIGALRM 14 A 由alarm(2)發出的訊號
SIGTERM 15 A 終止訊號
SIGUSR1 30,10,16 A 使用者自定義訊號1
SIGUSR2 31,12,17 A 使用者自定義訊號2
SIGCHLD 20,17,18 B 子程序結束訊號
SIGCONT 19,18,25 程序繼續(曾被停止的程序)
SIGSTOP 17,19,23 DEF 終止程序
SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵
SIGTTIN 21,21,26 D 後臺程序企圖從控制終端讀
SIGTTOU 22,22,27 D 後臺程序企圖從控制終端寫
下面的訊號沒在POSIX.1中列出,而在SUSv2列出
訊號 值 處理動作 發出訊號的原因
--------------------------------------------------------------------
SIGBUS 10,7,10 C 匯流排錯誤(錯誤的記憶體訪問)
SIGPOLL A Sys V定義的Pollable事件,與SIGIO同義
SIGPROF 27,27,29 A Profiling定時器到
SIGSYS 12,-,12 C 無效的系統呼叫 (SVID)
SIGTRAP 5 C 跟蹤/斷點捕獲
SIGURG 16,23,21 B Socket出現緊急條件(4.2 BSD)
SIGVTALRM 26,26,28 A 實際時間報警時鐘訊號(4.2 BSD)
SIGXCPU 24,24,30 C 超出設定的CPU時間限制(4.2 BSD)
SIGXFSZ 25,25,31 C 超出設定的檔案大小限制(4.2 BSD)
(對於SIGSYS,SIGXCPU,SIGXFSZ,以及某些機器體系結構下的SIGBUS,Linux預設的動作是A (terminate),SUSv2 是C (terminate and dump core))。
下面是其它的一些訊號
訊號 值 處理動作 發出訊號的原因
----------------------------------------------------------------------
SIGIOT 6 C IO捕獲指令,與SIGABRT同義
SIGEMT 7,-,7
SIGSTKFLT -,16,- A 協處理器堆疊錯誤
SIGIO 23,29,22 A 某I/O操作現在可以進行了(4.2 BSD)
SIGCLD -,-,18 A 與SIGCHLD同義
SIGPWR 29,30,19 A 電源故障(System V)
SIGINFO 29,-,- A 與SIGPWR同義
SIGLOST -,-,- A 檔案鎖丟失
SIGWINCH 28,28,20 B 視窗大小改變(4.3 BSD, Sun)
SIGUNUSED -,31,- A 未使用的訊號(will be SIGSYS)
(在這裡,- 表示訊號沒有實現;有三個值給出的含義為,第一個值通常在Alpha和Sparc上有效,中間的值對應i386和ppc以及sh,最後一個值對應mips。訊號29在Alpha上為SIGINFO / SIGPWR ,在Sparc上為SIGLOST。)
處理動作一項中的字母含義如下
A 預設的動作是終止程序
B 預設的動作是忽略此訊號
C 預設的動作是終止程序並進行核心映像轉儲(dump core)
D 預設的動作是停止程序
E 訊號不能被捕獲
F 訊號不能被忽略
上 面介紹的訊號是常見系統所支援的。以表格的形式介紹了各種訊號的名稱、作用及其在預設情況下的處理動作。各種預設處理動作的含義是:終止程式是指程序退 出;忽略該訊號是將該訊號丟棄,不做處理;停止程式是指程式掛起,進入停止狀況以後還能重新進行下去,一般是在除錯的過程中(例如ptrace系統調 用);核心映像轉儲是指將程序資料在記憶體的映像和程序在核心結構中儲存的部分內容以一定格式轉儲到檔案系統,並且程序退出執行,這樣做的好處是為程式設計師提 供了方便,使得他們可以得到程序當時執行時的資料值,允許他們確定轉儲的原因,並且可以除錯他們的程式。
注意 訊號SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。訊號SIGIOT與SIGABRT是一個訊號。可以看出,同一個訊號在不同的系統中值可能不一樣,所以建議最好使用為訊號定義的名字,而不要直接使用訊號的值。
二、信 號 機 制
上 一節中介紹了訊號的基本概念,在這一節中,我們將介紹核心如何實現訊號機制。即核心如何向一個程序傳送訊號、程序如何接收一個訊號、程序怎樣控制自己對信 號的反應、核心在什麼時機處理和怎樣處理程序收到的訊號。還要介紹一下setjmp和longjmp在訊號中起到的作用。
1、核心對訊號的基本處理方法
內 核給一個程序傳送軟中斷訊號的方法,是在程序所在的程序表項的訊號域設定對應於該訊號的位。這裡要補充的是,如果訊號傳送給一個正在睡眠的程序,那麼要看 該程序進入睡眠的優先順序,如果程序睡眠在可被中斷的優先順序上,則喚醒程序;否則僅設定程序表中訊號域相應的位,而不喚醒程序。這一點比較重要,因為程序檢 查是否收到訊號的時機是:一個程序在即將從核心態返回到使用者態時;或者,在一個程序要進入或離開一個適當的低排程優先順序睡眠狀態時。
核心處理一個程序收到的訊號的時機是在一個程序從核心態返回使用者態時。所以,當一個程序在核心態下執行時,軟中斷訊號並不立即起作用,要等到將返回使用者態時才處理。程序只有處理完訊號才會返回使用者態,程序在使用者態下不會有未處理完的訊號。
內 核處理一個程序收到的軟中斷訊號是在該程序的上下文中,因此,程序必須處於執行狀態。前面介紹概念的時候講過,處理訊號有三種類型:程序接收到訊號後退 出;程序忽略該訊號;程序收到訊號後執行使用者設定用系統呼叫signal的函式。當程序接收到一個它忽略的訊號時,程序丟棄該訊號,就象沒有收到該訊號似 的繼續執行。如果程序收到一個要捕捉的訊號,那麼程序從核心態返回使用者態時執行使用者定義的函式。而且執行使用者定義的函式的方法很巧妙,核心是在使用者棧上創 建一個新的層,該層中將返回地址的值設定成使用者定義的處理函式的地址,這樣程序從核心返回彈出棧頂時就返回到使用者定義的函式處,從函式返回再彈出棧頂時, 才返回原先進入核心的地方。這樣做的原因是使用者定義的處理函式不能且不允許在核心態下執行(如果使用者定義的函式在核心態下執行的話,使用者就可以獲得任何權 限)。
在訊號的處理方法中有幾點特別要引起注意。第一,在一些系統中,當一個程序處理完中斷訊號返回使用者態之前,核心清除使用者區中設 定的對該訊號的處理例程的地址,即下一次程序對該訊號的處理方法又改為預設值,除非在下一次訊號到來之前再次使用signal系統呼叫。這可能會使得程序 在呼叫signal之前又得到該訊號而導致退出。在BSD中,核心不再清除該地址。但不清除該地址可能使得程序因為過多過快的得到某個訊號而導致堆疊溢 出。為了避免出現上述情況。在BSD系統中,核心模擬了對硬體中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。
第二個要 引起注意的是,如果要捕捉的訊號發生於程序正在一個系統呼叫中時,並且該程序睡眠在可中斷的優先順序上,這時該訊號引起程序作一次longjmp,跳出睡眠 狀態,返回使用者態並執行訊號處理例程。當從訊號處理例程返回時,程序就象從系統呼叫返回一樣,但返回了一個錯誤程式碼,指出該次系統呼叫曾經被中斷。這要注 意的是,BSD系統中核心可以自動地重新開始系統呼叫。
第三個要注意的地方:若程序睡眠在可中斷的優先順序上,則當它收到一個要忽略的訊號時,該程序被喚醒,但不做longjmp,一般是繼續睡眠。但使用者感覺不到程序曾經被喚醒,而是象沒有發生過該訊號一樣。
第 四個要注意的地方:核心對子程序終止(SIGCLD)訊號的處理方法與其他訊號有所區別。當程序檢查出收到了一個子程序終止的訊號時,預設情況下,該程序 就象沒有收到該訊號似的,如果父程序執行了系統呼叫wait,程序將從系統呼叫wait中醒來並返回wait呼叫,執行一系列wait呼叫的後續操作(找 出僵死的子程序,釋放子程序的程序表項),然後從wait中返回。SIGCLD訊號的作用是喚醒一個睡眠在可被中斷優先順序上的程序。如果該程序捕捉了這個 訊號,就象普通訊號處理一樣轉到處理例程。如果程序忽略該訊號,那麼系統呼叫wait的動作就有所不同,因為SIGCLD的作用僅僅是喚醒一個睡眠在可被 中斷優先順序上的程序,那麼執行wait呼叫的父程序被喚醒繼續執行wait呼叫的後續操作,然後等待其他的子程序。
如果一個程序呼叫signal系統呼叫,並設定了SIGCLD的處理方法,並且該程序有子程序處於僵死狀態,則核心將向該程序發一個SIGCLD訊號。
2、setjmp和longjmp的作用
前面在介紹訊號處理機制時,多次提到了setjmp和longjmp,但沒有仔細說明它們的作用和實現方法。這裡就此作一個簡單的介紹。
在 介紹訊號的時候,我們看到多個地方要求程序在檢查收到訊號後,從原來的系統呼叫中直接返回,而不是等到該呼叫完成。這種程序突然改變其上下文的情況,就是 使用setjmp和longjmp的結果。setjmp將儲存的上下文存入使用者區,並繼續在舊的上下文中執行。這就是說,程序執行一個系統呼叫,當因為資 源或其他原因要去睡眠時,核心為程序作了一次setjmp,如果在睡眠中被訊號喚醒,程序不能再進入睡眠時,核心為程序呼叫longjmp,該操作是核心 為程序將原先setjmp呼叫儲存在程序使用者區的上下文恢復成現在的上下文,這樣就使得程序可以恢復等待資源前的狀態,而且核心為setjmp返回1,使 得程序知道該次系統呼叫失敗。這就是它們的作用。
三、有關訊號的系統呼叫
前面兩節已經介紹了有關訊號的大部分知 識。這一節我們來了解一下這些系統呼叫。其中,系統呼叫signal是程序用來設定某個訊號的處理方法,系統呼叫kill是用來發送訊號給指定程序的。這 兩個呼叫可以形成訊號的基本操作。後兩個呼叫pause和alarm是通過訊號實現的程序暫停和定時器,呼叫alarm是通過訊號通知程序定時器到時。所 以在這裡,我們還要介紹這兩個呼叫。
1、signal 系統呼叫
系統呼叫signal用來設定某個訊號的處理方法。該呼叫宣告的格式如下:
void (*signal(int signum, void (*handler)(int)))(int);
在使用該呼叫的程序中加入以下標頭檔案:
#include <signal.h>
上述宣告格式比較複雜,如果不清楚如何使用,也可以通過下面這種型別定義的格式來使用(POSIX的定義):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
但這種格式在不同的系統中有不同的型別定義,所以要使用這種格式,最好還是參考一下聯機手冊。
在呼叫中,引數signum指出要設定處理方法的訊號。第二個引數handler是一個處理函式,或者是
SIG_IGN:忽略引數signum所指的訊號。
SIG_DFL:恢復引數signum所指訊號的處理方法為預設值。
傳遞給訊號處理例程的整數引數是訊號值,這樣可以使得一個訊號處理例程處理多個訊號。系統呼叫signal返回值是指定訊號signum前一次的處理例程或者錯誤時返回錯誤程式碼SIG_ERR。下面來看一個簡單的例子:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigroutine(int dunno) { /* 訊號處理例程,其中dunno將會得到訊號的值 */
switch (dunno) {
case 1:
printf("Get a signal -- SIGHUP ");
break;
case 2:
printf("Get a signal -- SIGINT ");
break;
case 3:
printf("Get a signal -- SIGQUIT ");
break;
}
return;
}
int main() {
printf("process id is %d ",getpid());
signal(SIGHUP, sigroutine); //* 下面設定三個訊號的處理方法
signal(SIGINT, sigroutine);
signal(SIGQUIT, sigroutine);
for (;;) ;
}
其中訊號SIGINT由按下Ctrl-C發出,訊號SIGQUIT由按下Ctrl-發出。該程式執行的結果如下:
localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的結果
Get a signal -SIGQUIT //按下Ctrl-得到的結果
//按下Ctrl-z將程序置於後臺
[1]+ Stopped ./sig_test
localhost:~$ bg
[1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向程序傳送SIGHUP訊號
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向程序傳送SIGKILL訊號,終止程序
localhost:~$
2、kill 系統呼叫
系統呼叫kill用來向程序傳送一個訊號。該呼叫宣告的格式如下:
int kill(pid_t pid, int sig);
在使用該呼叫的程序中加入以下標頭檔案:
#include <sys/types.h>
#include <signal.h>
該 系統呼叫可以用來向任何程序或程序組傳送任何訊號。如果引數pid是正數,那麼該呼叫將訊號sig傳送到程序號為pid的程序。如果pid等於0,那麼信 號sig將傳送給當前程序所屬程序組裡的所有程序。如果引數pid等於-1,訊號sig將傳送給除了程序1和自身以外的所有程序。如果引數pid小於- 1,訊號sig將傳送給屬於程序組-pid的所有程序。如果引數sig為0,將不傳送訊號。該呼叫執行成功時,返回值為0;錯誤時,返回-1,並設定相應 的錯誤程式碼errno。下面是一些可能返回的錯誤程式碼:
EINVAL:指定的訊號sig無效。
ESRCH:引數pid指定的程序或程序組不存在。注意,在程序表項中存在的程序,可能是一個還沒有被wait收回,但已經終止執行的僵死程序。
EPERM: 程序沒有權力將這個訊號傳送到指定接收訊號的程序。因為,一個程序被允許將訊號傳送到程序pid時,必須擁有root權力,或者是發出呼叫的程序的UID 或EUID與指定接收的程序的UID或儲存使用者ID(savedset-user-ID)相同。如果引數pid小於-1,即該訊號傳送給一個組,則該錯誤 表示組中有成員程序不能接收該訊號。
3、pause系統呼叫
系統呼叫pause的作用是等待一個訊號。該呼叫的宣告格式如下:
int pause(void);
在使用該呼叫的程序中加入以下標頭檔案:
#include <unistd.h>
該呼叫使得發出呼叫的程序進入睡眠,直到接收到一個訊號為止。該呼叫總是返回-1,並設定錯誤程式碼為EINTR(接收到一個訊號)。下面是一個簡單的範例:
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void sigroutine(int unused) {
printf("Catch a signal SIGINT ");
}
int main() {
signal(SIGINT, sigroutine);
pause();
printf("receive a signal ");
}
在這個例子中,程式開始執行,就象進入了死迴圈一樣,這是因為程序正在等待訊號,當我們按下Ctrl-C時,訊號被捕捉,並且使得pause退出等待狀態。
4、alarm和 setitimer系統呼叫
系統呼叫alarm的功能是設定一個定時器,當定時器計時到達時,將發出一個訊號給程序。該呼叫的宣告格式如下:
unsigned int alarm(unsigned int seconds);
在使用該呼叫的程序中加入以下標頭檔案:
#include <unistd.h>
系 統呼叫alarm安排核心為呼叫程序在指定的seconds秒後發出一個SIGALRM的訊號。如果指定的引數seconds為0,則不再發送 SIGALRM訊號。後一次設定將取消前一次的設定。該呼叫返回值為上次定時呼叫到傳送之間剩餘的時間,或者因為沒有前一次定時呼叫而返回0。
注意,在使用時,alarm只設定為傳送一次訊號,如果要多次傳送,就要多次使用alarm呼叫。
對於alarm,這裡不再舉例。現在的系統中很多程式不再使用alarm呼叫,而是使用setitimer呼叫來設定定時器,用getitimer來得到定時器的狀態,這兩個呼叫的宣告格式如下:
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
在使用這兩個呼叫的程序中加入以下標頭檔案:
#include <sys/time.h>
該系統呼叫給程序提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就傳送一個相應的訊號給程序,並使得計時器重新開始。三個計時器由引數which指定,如下所示:
TIMER_REAL:按實際時間計時,計時到達將給程序傳送SIGALRM訊號。
ITIMER_VIRTUAL:僅當程序執行時才進行計時。計時到達將傳送SIGVTALRM訊號給程序。
ITIMER_PROF:當程序執行時和系統為該程序執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計程序在使用者態和核心態花費的時間。計時到達將傳送SIGPROF訊號給程序。
定時器中的引數value用來指明定時器的時間,其結構如下:
struct itimerval {
struct timeval it_interval; /* 下一次的取值 */
struct timeval it_value; /* 本次的設定值 */
};
該結構中timeval結構定義如下:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};
在setitimer 呼叫中,引數ovalue如果不為空,則其中保留的是上次呼叫設定的值。定時器將it_value遞減到0時,產生一個訊號,並將it_value的值設 定為it_interval的值,然後重新開始計時,如此往復。當it_value設定為0時,計時器停止,或者當它計時到期,而it_interval 為0時停止。呼叫成功時,返回0;錯誤時,返回-1,並設定相應的錯誤程式碼errno:
EFAULT:引數value或ovalue是無效的指標。
EINVAL:引數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。
下面是關於setitimer呼叫的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM訊號:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
int sec;
void sigroutine(int signo) {
switch (signo) {
case SIGALRM:
printf("Catch a signal -- SIGALRM ");
break;
case SIGVTALRM:
printf("Catch a signal -- SIGVTALRM ");
break;
}
return;
}
int main() {
struct itimerval value,ovalue,value2;
sec = 5;
printf("process id is %d ",getpid());
signal(SIGALRM, sigroutine);
signal(SIGVTALRM, sigroutine);
value.it_value.tv_sec = 1;
value.it_value.tv_usec = 0;
value.it_interval.tv_sec = 1;
value.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &value, &ovalue);
value2.it_value.tv_sec = 0;
value2.it_value.tv_usec = 500000;
value2.it_interval.tv_sec = 0;
value2.it_interval.tv_usec = 500000;
setitimer(ITIMER_VIRTUAL, &value2, &ovalue);
for (;;) ;
}
該例子的螢幕拷貝如下:
localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM
本文簡單介紹了Linux下的訊號,如果希望瞭解其他呼叫,請參考聯機手冊或其他文件。