您好,登錄后才能下訂單哦!
這篇文章主要介紹了Linux的Signal機制是什么的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Linux的Signal機制是什么文章都會有所收獲,下面我們一起來看看吧。
Signal機制在Linux中是一個非常常用的進程間通信機制,很多人在使用的時候不會考慮該機制是具體如何實現的。signal機制可以被理解成進程的軟中斷,因此,在實時性方面還是相對比較高的。
信號的名字和編號: 每個信號都有一個名字和編號,這些名字都以“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 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#include#include //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 命令發送信號給之前所寫的程序,關于這個命令,我們后面再談。
通過 kill 命令發送信號
程序接收到的信號的處理結果
簡單的總結一下,我們通過 signal 函數注冊一個信號處理函數,分別注冊了兩個信號(SIGIO 和 SIGUSER1);隨后主程序就一直“長眠”了。 通過 kill 命令發送信號之前,我們需要先查看到接收者,通過 ps 命令查看了之前所寫的程序的 PID,通過 kill 函數來發送。 對于已注冊的信號,使用 kill 發送都可以正常接收到,但是如果發送了未注冊的信號,則會使得應用程序終止進程。
那么,已經可以設置信號處理函數了,信號的處理還有兩種狀態,分別是默認處理和忽略,這兩種設置很簡單,只需要將 handler 設置為 SIG_IGN(忽略信號)或 SIG_DFL(默認動作)即可。
在此還有兩個問題需要說明一下:
入門版的信號注冊還是比較簡單的,只需要一句注冊和一個處理函數即可,那么,接下來看看,如何發送信號吧。
kill 的函數原型
#include #include int kill(pid_t pid, int sig);
正如我之前所說的,信號的處理需要有接受者,顯然發送者必須要知道發給誰,根據 kill 函數的遠行可以看到,pid 就是接受者的 pid,sig 則是發送的信號的類型。從原型來看,發送信號要比接受信號還要簡單些,那么我們直接上代碼吧~~!Show me the code!!!
#include #include #include#include int main(int argc, char** argv) { if(3 != argc) { printf("[Arguments ERROR!]\n"); printf("\tUsage:\n"); printf("\t\t%s \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
關于信號,還有更多的話題,比如,信號是否都能夠準確的送達到目標進程呢?答案其實是不一定,那么這就有了可靠信號和不可靠信號
不可靠信號:信號可能會丟失,一旦信號丟失了,進程并不能知道信號丟失 可靠信號:也是阻塞信號,當發送了一個阻塞信號,并且該信號的動作時系統默認動作或捕捉該信號,如果信號從發出以后會一直保持未決的狀態,直到該進程對此信號解除了阻塞,或將對此信號的動作更改為忽略。 對于信號來說,信號編號小于等于31的信號都是不可靠信號,之后的信號為可卡信號,系統會根據有信號隊列,將信號在遞達之前進行阻塞。
信號的阻塞和未決是通過信號的狀態字來管理的,該狀態字是按位來管理信號的狀態。每個信號都有獨立的阻塞字,規定了當前要阻塞地達到該進程的信號集。
信號阻塞狀態字(block),1代表阻塞、0代表不阻塞;信號未決狀態字(pending)的1代表未決,0代表信號可以抵達了;它們都是每一個bit代表一個信號
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函數來獲取或者設置。該函數管理信號,是通過信號集的數據結構來進行管理的,信號集可以通過以下的函數進行管理。 信號集操作函數(狀態字表示)
#include 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 bset;
sigemptyset(&bset);
sigaddset(&bset, SIGINT);
sigprocmask(SIG_UNBLOCK, &bset, NULL);
#include int sigpending(sigset_t *set);
這個函數使用很簡單,對于調用他的進程來說,其中信號集中的信號是阻塞不能遞送的,那么,也就一定會是當前未決的。
#include int sigsuspend(const sigset_t *mask);
為何會出現原子性的解除阻塞的函數呢? 因為,當信號被阻塞的時候,產生了信號,那么該信號的遞送就要推遲到這個信號被解除了阻塞為止。如果此時,應用程序正好處在,解除 SIGINT 的阻塞和 pause 之間,那么此時,會產生問題,可能永遠 pause 不能夠等到SIGINT 信號來打斷他,造成程序永久阻塞在 pause 處。 為了解決這個問題,,需要在一個原子性的操作來恢復信號的屏蔽字,然后才能讓進程進入休眠狀態,以保證不會出現上述的問題。
進程的信號屏蔽字設置為
我們已經成功完成了信號的收發,那么為什么會有高級版出現呢?其實之前的信號存在一個問題就是,雖然發送和接收到了信號,可是總感覺少些什么,既然都已經把信號發送過去了,為何不能再攜帶一些數據呢? 正是如此,我們需要另外的函數來通過信號傳遞的過程中,攜帶一些數據。咱么先來看看發送的函數吧。
sigaction 的函數原型
#include 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 int sigqueue(pid_t pid, int sig, const union sigval value); union sigval { int sival_int; void *sival_ptr; };
使用這個函數之前,必須要有幾個操作需要完成
sigqueue 函數只能把信號發送給單個進程,可以使用 value 參數向信號處理程序傳遞整數值或者指針值。
sigqueue 函數不但可以發送額外的數據,還可以讓信號進行排隊(操作系統必須實現了 POSIX.1的實時擴展),對于設置了阻塞的信號,使用 sigqueue 發送多個同一信號,在解除阻塞時,接受者會接收到發送的信號隊列中的信號,而不是直接收到一次。
但是,信號不能無限的排隊,信號排隊的最大值受到SIGQUEUE_MAX
的限制,達到最大限制后,sigqueue 會失敗,errno 會被設置為 EAGAIN。
那么我們來嘗試一下,發送一個攜帶有額外數據的信號吧。 Show me the code!! 接收端
#include#include#include //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 #include #include#include int main(int argc, char** argv) { if(4 != argc) { printf("[Arguments ERROR!]\n"); printf("\tUsage:\n"); printf("\t\t%s \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; }
接收到的信號和信息
發送的信號和數據
關于“Linux的Signal機制是什么”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Linux的Signal機制是什么”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。