對于 Linux來說,實際信號是軟中斷,許多重要的程序都需要處理信號。信號,為 Linux 提供了一種處理異步事件的方法。比如,終端用戶輸入了 ctrl+c 來中斷程序,會通過信號機制停止一個程序。
信號概述
-
信號的名字和編號:
每個信號都有一個名字和編號,這些名字都以“SIG”開頭,例如“SIGIO ”、“SIGCHLD”等等。
信號定義在signal.h
頭文件中,信號名都定義為正整數。
具體的信號名稱可以使用kill -l
來查看信號的名字以及序號,信號是從1開始編號的,不存在0號信號。kill對于信號0又特殊的應用。
信號的名稱 信號的處理:
信號的處理有三種方法,分別是:忽略、捕捉和默認動作
- 忽略信號,大多數信號可以使用這個方式來處理,但是有兩種信號不能被忽略(分別是
SIGKILL
和SIGSTOP
)。因為他們向內核和超級用戶提供了進程終止和停止的可靠方法,如果忽略了,那么這個進程就變成了沒人能管理的的進程,顯然是內核設計者不希望看到的場景 - 捕捉信號,需要告訴內核,用戶希望如何處理某一種信號,說白了就是寫一個信號處理函數,然后將這個函數告訴內核。當該信號產生時,由內核來調用用戶自定義的函數,以此來實現某種信號的處理。
- 系統默認動作,對于每個信號來說,系統都對應由默認的處理動作,當發生了該信號,系統會自動執行。不過,對系統來說,大部分的處理方式都比較粗暴,就是直接殺死該進程。
具體的信號默認動作可以使用man 7 signal
來查看系統的具體定義。在此,我就不詳細展開了,需要查看的,可以自行查看。也可以參考 《UNIX 環境高級編程(第三部)》的 P251——P256中間對于每個信號有詳細的說明。
了解了信號的概述,那么,信號是如何來使用呢?
其實對于常用的 kill 命令就是一個發送信號的工具,
kill 9 PID
來殺死進程。比如,我在后臺運行了一個 top 工具,通過 ps 命令可以查看他的 PID,通過 kill 9 來發送了一個終止進程的信號來結束了 top 進程。如果查看信號編號和名稱,可以發現9對應的是 9) SIGKILL,正是殺死該進程的信號。而以下的執行過程實際也就是執行了9號信號的默認動作——殺死進程。
kill 殺死進程
對于信號來說,最大的意義不是為了殺死信號,而是實現一些異步通訊的手段,那么如何來自定義信號的處理函數呢?
信號處理函數的注冊
信號處理函數的注冊不只一種方法,分為入門版和高級版
- 入門版:函數
signal
- 高級版:函數
sigaction
信號處理發送函數
信號發送函數也不止一個,同樣分為入門版和高級版
1.入門版:kill
2.高級版:sigqueue
信號注冊函數——入門版
在正式開始了解這兩個函數之前,可以先來思考一下,處理中斷都需要處理什么問題。
按照我們之前思路來看,可以發送的信號類型是多種多樣的,每種信號的處理可能不一定相同,那么,我們肯定需要知道到底發生了什么信號。
另外,雖然我們知道了系統發出來的是哪種信號,但是還有一點也很重要,就是系統產生了一個信號,是由誰來響應?
如果系統通過 ctrl+c 產生了一個 SIGINT(中斷信號),顯然不是所有程序同時結束,那么,信號一定需要有一個接收者。對于處理信號的程序來說,接收者就是自己。
開始的時候,先來看看入門版本的信號注冊函數,他的函數原型如下:
signal 的函數原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
根據函數原型可以看出由兩部分組成,一個是真實處理信號的函數,另一個是注冊函數了。
對于sighandler_t signal(int signum, sighandler_t handler);
函數來說,signum 顯然是信號的編號,handler 是中斷函數的指針。
同樣,typedef void (*sighandler_t)(int);
中斷函數的原型中,有一個參數是 int 類型,顯然也是信號產生的類型,方便使用一個函數來處理多個信號。我們先來看看簡單一個信號注冊的代碼示例吧。
#include<signal.h>
#include<stdio.h>
#include <unistd.h>
//typedef void (*sighandler_t)(int);
void
handler(int signum)
{
if(signum == SIGIO)
printf("SIGIO signal: %d\n", signum);
else if(signum == SIGUSR1)
printf("SIGUSR1 signal: %d\n", signum);
else
printf("error\n");
}
int
main(void)
{
//sighandler_t signal(int signum, sighandler_t handler);
signal(SIGIO, handler);
signal(SIGUSR1, handler);
printf("%d %d\n", SIGIO, SIGUSR1);
for(;;)
{
sleep(10000);
}
return 0;
}
我們先使用 kill 命令發送信號給之前所寫的程序,關于這個命令,我們后面再談。
簡單的總結一下,我們通過 signal 函數注冊一個信號處理函數,分別注冊了兩個信號(SIGIO 和 SIGUSER1);隨后主程序就一直“長眠”了。
通過 kill 命令發送信號之前,我們需要先查看到接收者,通過 ps 命令查看了之前所寫的程序的 PID,通過 kill 函數來發送。
對于已注冊的信號,使用 kill 發送都可以正常接收到,但是如果發送了未注冊的信號,則會使得應用程序終止進程。
那么,已經可以設置信號處理函數了,信號的處理還有兩種狀態,分別是默認處理和忽略,這兩種設置很簡單,只需要將 handler 設置為 SIG_IGN(忽略信號)或 SIG_DFL(默認動作)即可。
在此還有兩個問題需要說明一下:
- 當執行一個程序時,所有信號的狀態都是系統默認或者忽略狀態的。除非是 調用exec進程忽略了某些信號。exec 函數將原先設置為要捕捉的信號都更改為默認動作,其他信號的狀態則不會改變 。
2.當一個進程調動了 fork 函數,那么子進程會繼承父進程的信號處理方式。
入門版的信號注冊還是比較簡單的,只需要一句注冊和一個處理函數即可,那么,接下來看看,如何發送信號吧。
信號發送函數——入門版
kill 的函數原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
正如我之前所說的,信號的處理需要有接受者,顯然發送者必須要知道發給誰,根據 kill 函數的遠行可以看到,pid 就是接受者的 pid,sig 則是發送的信號的類型。從原型來看,發送信號要比接受信號還要簡單些,那么我們直接上代碼吧~~!Show me the code!!!
#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <unistd.h>
int main(int argc, char** argv)
{
if(3 != argc)
{
printf("[Arguments ERROR!]\n");
printf("\tUsage:\n");
printf("\t\t%s <Target_PID> <Signal_Number>\n", argv[0]);
return -1;
}
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
//int kill(pid_t pid, int sig);
if(pid > 0 && sig > 0)
{
kill(pid, sig);
}
else
{
printf("Target_PID or Signal_Number MUST bigger than 0!\n");
}
return 0;
}
總結一下:
根據以上的結果可看到,基本可以實現了信號的發送,雖然不能直接發送信號名稱,但是通過信號的編號,可以正常的給程序發送信號了,也是初步實現了信號的發送流程。
關于 kill 函數,還有一點需要額外說明,上面的程序限定了 pid 必須為大于0的正整數,其實 kill 函數傳入的 pid 可以是小于等于0的整數。
pid > 0:將發送個該 pid 的進程
pid == 0:將會把信號發送給與發送進程屬于同一進程組的所有進程,并且發送進程具有權限想這些進程發送信號。
pid < 0:將信號發送給進程組ID 為 pid 的絕對值得,并且發送進程具有權限向其發送信號的所有進程
pid == -1:將該信號發送給發送進程的有權限向他發送信號的所有進程。(不包括系統進程集中的進程)
關于信號,還有更多的話題,比如,信號是否都能夠準確的送達到目標進程呢?答案其實是不一定,那么這就有了可靠信號和不可靠信號
可靠信號和不可靠信號
不可靠信號:信號可能會丟失,一旦信號丟失了,進程并不能知道信號丟失
可靠信號:也是阻塞信號,當發送了一個阻塞信號,并且該信號的動作時系統默認動作或捕捉該信號,如果信號從發出以后會一直保持未決的狀態,直到該進程對此信號解除了阻塞,或將對此信號的動作更改為忽略。
對于信號來說,信號編號小于等于31的信號都是不可靠信號,之后的信號為可卡信號,系統會根據有信號隊列,將信號在遞達之前進行阻塞。
信號的阻塞和未決是通過信號的狀態字來管理的,該狀態字是按位來管理信號的狀態。每個信號都有獨立的阻塞字,規定了當前要阻塞地達到該進程的信號集。
信號阻塞狀態字(block),1代表阻塞、0代表不阻塞;信號未決狀態字(pending)的1代表未決,0代表信號可以抵達了;它們都是每一個bit代表一個信號
阻塞和未決是如何工作的?
比如向進程發送SIGINT信號,內核首先會判斷該進程的信號阻塞狀態字是否阻塞狀態,如果該信號被設置為阻塞的狀態,也就是阻塞狀態字對應位為1,那么信號未決狀態字(pending)相應位會被內核設置為1;如果該信號阻塞解除了,也就是阻塞狀態字設置為了0,那么信號未決狀態字(pending)相應位會被內核設置為0,表示信號此時可以抵達了,也就是可以接收該信號了。
阻塞狀態字用戶可以讀寫,未決狀態字用戶只能讀,是由內核來設置表示信號遞達狀態的。
PS:這里額外說明以下,只有支持了 POSIX.1實時擴展的系統才支持排隊的功能(也就阻塞狀態下多次同一信號發送給某一進程可以得到多次,而不是一次)。關于進程關于信號的阻塞狀態字的設置
可以通過int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函數來獲取或者設置。
該函數管理信號,是通過信號集的數據結構來進行管理的,信號集可以通過以下的函數進行管理。
信號集操作函數(狀態字表示)
#include <signal.h>
int sigemptyset(sigset_t *set); //初始化 set 中傳入的信號集,清空其中所有信號
int sigfillset(sigset_t *set); //把信號集填1,讓 set 包含所有的信號
int sigaddset(sigset_t *set, int signum);//把信號集對應位置為1
int sigdelset(sigset_t *set, int signum);//吧信號集對應位置為0
int sigismember(const sigset_t *set, int signum);//判斷signal是否在信號集
對于信號集分配好內存空間,需要使用初始化函數來初始化。初始化完成后,可以在該集合中添加、刪除特定的信號。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
其中 how 變量決定了是如何操作該狀態字。
SIG_BLOCK:set包含了我們希望添加到當前信號阻塞字的信號,相當于mask=mask|set
SIG_UNBLOCK:set包含了我們希望從當前信號阻塞字中解除阻塞的信號,相當于mask=mask&~set
SIG_SETMASK:設置當前信號阻塞字為set所指的值,相當于mask=set
pending是由內核來根據block設置的,只可以讀取其中的數據,來段判斷信號是否會遞達。通過設置block可以將希望阻塞的信號進行阻塞,對應的pending會由內核來設置
設置信號阻塞、未達的步驟:
- 分配內存空間sigset
sigset bset;
- 置空
sigemptyset(&bset);
- 添加信號
sigaddset(&bset, SIGINT);
- 添加其他需要管理的信號....
- 設置信號集中的信號處理方案(此處為解除阻塞)
sigprocmask(SIG_UNBLOCK, &bset, NULL);
- 簡化版設置阻塞狀態字
#include <signal.h>
int sigpending(sigset_t *set);
這個函數使用很簡單,對于調用他的進程來說,其中信號集中的信號是阻塞不能遞送的,那么,也就一定會是當前未決的。
- 原子操作的信號阻塞字的恢復并進入休眠狀態
#include <signal.h>
int sigsuspend(const sigset_t *mask);
為何會出現原子性的解除阻塞的函數呢?
因為,當信號被阻塞的時候,產生了信號,那么該信號的遞送就要推遲到這個信號被解除了阻塞為止。如果此時,應用程序正好處在,解除 SIGINT 的阻塞和 pause 之間,那么此時,會產生問題,可能永遠 pause 不能夠等到SIGINT 信號來打斷他,造成程序永久阻塞在 pause 處。
為了解決這個問題,,需要在一個原子性的操作來恢復信號的屏蔽字,然后才能讓進程進入休眠狀態,以保證不會出現上述的問題。
進程的信號屏蔽字設置為
信號注冊函數——高級版
我們已經成功完成了信號的收發,那么為什么會有高級版出現呢?其實之前的信號存在一個問題就是,雖然發送和接收到了信號,可是總感覺少些什么,既然都已經把信號發送過去了,為何不能再攜帶一些數據呢?
正是如此,我們需要另外的函數來通過信號傳遞的過程中,攜帶一些數據。咱么先來看看發送的函數吧。
sigaction 的函數原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); //信號處理程序,不接受額外數據,SIG_IGN 為忽略,SIG_DFL 為默認動作
void (*sa_sigaction)(int, siginfo_t *, void *); //信號處理程序,能夠接受額外數據和sigqueue配合使用
sigset_t sa_mask;//阻塞關鍵字的信號集,可以再調用捕捉函數之前,把信號添加到信號阻塞字,信號捕捉函數返回之前恢復為原先的值。
int sa_flags;//影響信號的行為SA_SIGINFO表示能夠接受數據
};
//回調函數句柄sa_handler、sa_sigaction只能任選其一
這個函數的原版幫助信息,可以通過man sigaction
來查看。
sigaction 是一個系統調用,根據這個函數原型,我們不難看出,在函數原型中,第一個參數signum
應該就是注冊的信號的編號;第二個參數act
如果不為空說明需要對該信號有新的配置;第三個參數oldact
如果不為空,那么可以對之前的信號配置進行備份,以方便之后進行恢復。
在這里額外說一下struct sigaction
結構體中的 sa_mask 成員,設置在其的信號集中的信號,會在捕捉函數調用前設置為阻塞,并在捕捉函數返回時恢復默認原有設置。這樣的目的是,在調用信號處理函數時,就可以阻塞默寫信號了。在信號處理函數被調用時,操作系統會建立新的信號阻塞字,包括正在被遞送的信號。因此,可以保證在處理一個給定信號時,如果這個種信號再次發生,那么他會被阻塞到對之前一個信號的處理結束為止。
sigaction 的時效性:當對某一個信號設置了指定的動作的時候,那么,直到再次顯式調用 sigaction并改變動作之前都會一直有效。
關于結構體中的 flag 屬性的詳細配置,在此不做詳細的說明了,只說明其中一點。如果設置為 SA_SIGINFO 屬性時,說明了信號處理程序帶有附加信息,也就是會調用 sa_sigaction 這個函數指針所指向的信號處理函數。否則,系統會默認使用 sa_handler 所指向的信號處理函數。在此,還要特別說明一下,sa_sigaction 和 sa_handler 使用的是同一塊內存空間,相當于 union,所以只能設置其中的一個,不能兩個都同時設置。
關于void (*sa_sigaction)(int, siginfo_t *, void *);
處理函數來說還需要有一些說明。void*
是接收到信號所攜帶的額外數據;而struct siginfo
這個結構體主要適用于記錄接收信號的一些相關信息。
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
int si_band; /* Band event */
int si_fd; /* File descriptor */
}
其中的成員很多,si_signo 和 si_code 是必須實現的兩個成員。可以通過這個結構體獲取到信號的相關信息。
關于發送過來的數據是存在兩個地方的,sigval_t si_value這個成員中有保存了發送過來的信息;同時,在si_int或者si_ptr成員中也保存了對應的數據。
那么,kill 函數發送的信號是無法攜帶數據的,我們現在還無法驗證發送收的部分,那么,我們先來看看發送信號的高級用法后,我們再來看看如何通過信號來攜帶數據吧。
信號發送函數——高級版
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
使用這個函數之前,必須要有幾個操作需要完成
- 使用 sigaction 函數安裝信號處理程序時,制定了 SA_SIGINFO 的標志。
- sigaction 結構體中的 sa_sigaction 成員提供了信號捕捉函數。如果實現的時 sa_handler 成員,那么將無法獲取額外攜帶的數據。
sigqueue 函數只能把信號發送給單個進程,可以使用 value 參數向信號處理程序傳遞整數值或者指針值。
sigqueue 函數不但可以發送額外的數據,還可以讓信號進行排隊(操作系統必須實現了 POSIX.1的實時擴展),對于設置了阻塞的信號,使用 sigqueue 發送多個同一信號,在解除阻塞時,接受者會接收到發送的信號隊列中的信號,而不是直接收到一次。
但是,信號不能無限的排隊,信號排隊的最大值受到SIGQUEUE_MAX
的限制,達到最大限制后,sigqueue 會失敗,errno 會被設置為 EAGAIN。
那么我們來嘗試一下,發送一個攜帶有額外數據的信號吧。
Show me the code!!
接收端
#include<signal.h>
#include<stdio.h>
#include <unistd.h>
//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum, siginfo_t * info, void * context)
{
if(signum == SIGIO)
printf("SIGIO signal: %d\n", signum);
else if(signum == SIGUSR1)
printf("SIGUSR1 signal: %d\n", signum);
else
printf("error\n");
if(context)
{
printf("content: %d\n", info->si_int);
printf("content: %d\n", info->si_value.sival_int);
}
}
int main(void)
{
//int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction act;
/*
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
*/
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGIO, &act, NULL);
sigaction(SIGUSR1, &act, NULL);
for(;;)
{
sleep(10000);
}
return 0;
}
發送端
#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <unistd.h>
int main(int argc, char** argv)
{
if(4 != argc)
{
printf("[Arguments ERROR!]\n");
printf("\tUsage:\n");
printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]);
return -1;
}
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
if(pid > 0 && sig > 0)
{
//int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval val;
val.sival_int = atoi(argv[3]);
printf("send: %d\n", atoi(argv[3]));
sigqueue(pid, sig, val);
}
else
{
printf("Target_PID or Signal_Number MUST bigger than 0!\n");
}
return 0;
}