linux系統程式設計--訊號
訊號的概念
什麼是訊號?訊號其實是編號從1-64的一組數,用SIG開頭的巨集表示。訊號是一個軟中斷,可以給我們提供一種停止當前執行流,進而去執行另一部分程式碼的方法。其中,1-31是基本訊號,32-64是實時訊號。實時訊號暫不關注。
訊號的產生
1、終端按鍵產生。 比如Ctrl+c產生SIGINT、Ctrl+\產生SIGQUIT訊號、Ctrl+z產生SIGITSP訊號等等。 2、硬體異常產生。 硬體檢測到某些條件發生時,會產生訊號。比如除以零、訪問非法記憶體。訊號由硬體產生通知核心,然後核心再把訊號通知程序。 3、命令及函式產生。 shell下的kill命令及編寫程式碼時用到的kill函式。 4、軟體產生
這裡面有幾個相關函式: 1、給自己傳送訊號
#include<signal.h>
int raise(int signo)
返回值:成功返回0,失敗返回-1。
在指定秒之後給程序自己傳送SIGALARM訊號。
#include<unistd.h> unsigned int alarm(unsigned int secondes) 返回值:返回0或者之前設定的鬧鐘時間的餘留描述
2、給自己或者別的程序傳送訊號 使用kill給呼叫者自己傳送訊號的時候,在kill返回之前,signo指定的訊號或者其他某個未決、非阻塞訊號將被遞送至程序。
#include<signal.h> int kill(pid_t pid, int signo) pid:指明發送訊號給哪個程序或者程序組。 pid > 0 訊號傳送給程序id為pid的程序。 pid = 0 訊號傳送給與傳送程序同一程序組內的所有程序,前提是呼叫kill的程序有許可權向這些程序傳送訊號。 pid < 0 訊號傳送給程序組id等於pid的絕對值,並且傳送程序有許可權向其傳送訊號的所有程序。 pid == -1 該訊號傳送給程序有權傳送給的所有程序。 signo:要傳送的訊號 返回值: 成功返回零;出錯返回-1.
訊號的處理方式
linux提供了三種訊號處理的方式:
1、忽略此訊號。 訊號會產生並且遞送給程序,但程序不執行任何動作。 2、執行預設動作。 大多數訊號的預設動作是終止程序。 3、捕捉訊號。 程序可以為特定的訊號註冊訊號處理函式。這樣當訊號被遞送給程序的時候,程序轉而去執行訊號處理函式,從訊號處理函式返回之後再繼續原先的執行流
注意:SIGKILL和SIGSTOP不能被忽略也不能被捕捉。因為它們提供了一種可靠的終止程序的方式。
標準訊號的不可靠性
對於標準訊號(1-31),在訊號處理函式執行期間,如果該訊號再產生,會被加入到未決訊號集中去,等處理函式返回後再遞送給程序。但是,如果執行期間該訊號產生了多次,只會遞送一次,其他的都被丟棄了。
訊號集
訊號集就是訊號的集合,用sigset_t型別表示,sigset_t是typedef的,它其實就是一個64位的資料,每一位代表一個訊號,分別是1-64,沒有編號為零的訊號 訊號集有一系列的操作函式,如下:
#include<signal.h>
int sigemptyset(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int signo)
int sigdelset(sigset_t *set, int signo)
返回值:成功返回0,失敗返回-1
int sigismember(const sigset_t *set, int segno)
返回值:若真,返回1;若假,返回0
每個程序都有兩個訊號集:阻塞訊號集和未決訊號集
阻塞訊號集
每個程序維護一個阻塞訊號集,在該集合內的訊號不會被遞送給程序。 可以通過sigprocmask來檢視或者更改阻塞訊號集:
#include<signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
返回值:成功返回0;出錯返回-1
how有下面幾個選項:
how | 作用 |
---|---|
SIG_BLOCK | 將set中的訊號新增到阻塞訊號集中去 |
SIG_UNBLOCK | 將set中的訊號從阻塞訊號集中移除 |
SIG_SETMASK | 新的阻塞訊號集是set指向的集合 |
呼叫sigprocmask後如果有任何未決的、不再阻塞的訊號,則在sigprocmask返回前至少將其中之一遞送給程序
未決訊號集
何為未決?訊號產生了但是還未遞送給程序,稱為未決的。如果為程序產生了一個訊號,並且對訊號的動作是捕捉或者預設動作,那麼該訊號就會被加入到未決訊號集,直到程序對該訊號解除阻塞,或者訊號的動作更改為忽略;在別的程序執行期間產生的訊號,也會被加入到未決訊號集。
可以使用sigpending來檢視未決訊號集。
int sigpending(sigset_t *set)
返回值:成功返回0;出錯返回-1。
訊號與系統呼叫
如果程序在執行一個低速系統呼叫而阻塞期間捕捉到一個訊號,則該系統呼叫就被中斷,如果該系統呼叫是可自動重啟動的,則重新執行該系統呼叫。否則該系統呼叫返回出錯,並將errno設定為EINTR. 低速系統呼叫:可能會是系統永遠阻塞的一類系統呼叫。 如管道、終端、網路裝置的資料不存在時,則讀操作就有可能永遠阻塞。
被中斷的系統呼叫
當程序執行低速系統呼叫而阻塞的時候,程序的狀態是TASK_INTERRUPTIBLE, 但是,當程序收到一個訊號,並且該訊號不在阻塞集中時,核心會把程序的狀態改成TASK_RUNNING,即使系統呼叫請求的資源沒有準備好。這樣當程序被排程執行的時候從系統呼叫返回前會檢查有沒有未處理的訊號,進而去執行訊號處理函式。
訊號處理方式的更改
1、signal函式
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1
#include<signal.h>
typedef void Sigfunc (int);
Sigfunc *signal(int signo, Sigfunc *func)
func:沒有返回值且只有一個int型引數的函式指標,指向訊號處理函式。也可以是SIG_IGN或者SIG_DFL。
返回值:成功返回之前的訊號處理配置,失敗返回SIG_ERR
signal的缺點: 1、被signal註冊的訊號處理函式中斷的系統呼叫預設是自動重啟動的,當該型別的系統呼叫被中斷之後,不會返回錯誤,會重新執行下該系統呼叫。 2、在某個訊號處理函式執行期間不能阻塞其他訊號。 3、不能給訊號處理函式傳遞額外引數。 下面的sigaction函式可以解決上面的問題。
2、帶引數的訊號處理函式
2.1 sigaction函式
#include<signal.h>
int sigaction(int signo, const struct sigaction *restrict act,
struct sigaction *oact)
返回值:成功返回0,失敗返回-1.
2.2 struct sigaction 結構
struct sigaction{
void (*sa_handler)(int) /*addr of sighandler or SIG_IGN,SIG_DFL*/
sigset_t sa_mask;
int sa_flags
void (*sa_sigaction)(int, siginfo_t * void *)
}
sa_mask:當訊號的動作是捕捉的時候,在訊號處理函式返回之前,這一訊號集指定的訊號會被加到訊號遮蔽字當中去,從訊號處理函式返回時遮蔽字回覆為原先值。同時,訊號處理函式執行期間,新訊號遮蔽字包含正被遞送的訊號。 sa_flags:選項有很多,主要列出如下常用幾個 SA_INTERRUPT:由此訊號中斷的系統呼叫不重新啟動。 SA_RESTART:由此訊號中斷的系統呼叫自動重啟動。 SA_SIGINFO:此選項對訊號處理程式提供附加資訊。並不是說指定了該選項之後只能使用sa_sigaction函式,也可以使用sa_handler函式,只不過這麼使用沒什麼意義。
2.3 sa_sigaction成員
void sa_sigaction(int signo, siginfo_t *info,void *context)
signo:訊號
info:要傳遞的資訊
context:它表示傳送程序在傳送訊號時的上下文,這個引數暫時不關心。
siginfo_t 實際上是一個結構體,如下:
typedef struct siginfo
{
int si_signo;
int si_errno;
int si_code;
pid_t si_pid;
uid_t si_uid;
void *si_addr;
int si_status;
union sigval si_value;
}siginfo_t;
union sigval
{
int sival_int;
void *sigval_ptr;
}
傳遞引數給訊號處理函式的時候,sa_flags中的SA_SIGINFO要置位 示例:
2.4 傳送訊號時如何攜帶引數
使用kill函式無法傳送攜帶額外資料的訊號。linux提供了另外一個函式:sigqueue
int sigqueue(pid_t pid, int signo, const union sigval value)
返回值:成功返回0;出錯返回-1.