【linux系统编程】进程间通信:信号中断处理
什么是信號?
信號是 Linux 進程間通信的最古老的方式。信號是軟件中斷,它是在軟件層次上對中斷機制的一種模擬,是一種異步通信的方式?。信號可以導致一個正在運行的進程被另一個正在運行的異步進程中斷,轉而處理某一個突發事件。
“中斷”在我們生活中經常遇到,譬如,我正在房間里打游戲,突然送快遞的來了,把正在玩游戲的我給“中斷”了,我去簽收快遞( 處理中斷 ),處理完成后,再繼續玩我的游戲。這里我們學習的“信號”就是屬于這么一種“中斷”。我們在終端上敲“Ctrl+c”,就產生一個“中斷”,相當于產生一個信號,接著就會處理這么一個“中斷任務”(默認的處理方式為中斷當前進程)。
信號可以直接進行用戶空間進程和內核空間進程的交互,內核進程可以利用它來通知用戶空間進程發生了哪些系統事件。
一個完整的信號周期包括三個部分:信號的產生,信號在進程中的注冊,信號在進程中的注銷,執行信號處理函數。如下圖所示:
注意:這里信號的產生,注冊,注銷時信號的內部機制,而不是信號的函數實現。
Linux 可使用命令:kill -l("l" 為字母),查看相應的信號。
列表中,編號為?1 ~ 31?的信號為傳統 UNIX 支持的信號,是不可靠信號(非實時的),編號為?32 ~ 63?的信號是后來擴充的,稱做可靠信號(實時信號)。不可靠信號和可靠信號的區別在于前者不支持排隊,可能會造成信號丟失,而后者不會。非可靠信號一般都有確定的用途及含義, ?可靠信號則可以讓用戶自定義使用。更多詳情,請看《Linux信號列表》。
信號的產生方式
1)當用戶按某些終端鍵時,將產生信號。
終端上按“Ctrl+c”組合鍵通常產生中斷信號 SIGINT,終端上按“Ctrl+\”鍵通常產生中斷信號 SIGQUIT,終端上按“Ctrl+z”鍵通常產生中斷信號 SIGSTOP 等。
2)硬件異常將產生信號。
除數為 0,無效的內存訪問等。這些情況通常由硬件檢測到,并通知內核,然后內核產生適當的信號發送給相應的進程。
3)軟件異常將產生信號。
當檢測到某種軟件條件已發生,并將其通知有關進程時,產生信號。
4)調用 kill() 函數將發送信號。
注意:接收信號進程和發送信號進程的所有者必須相同,或發送信號進程的所有者必須是超級用戶。
5)運行 kill 命令將發送信號。
此程序實際上是使用 kill 函數來發送信號。也常用此命令終止一個失控的后臺進程。
信號的常用操作
發送信號
所需頭文件:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signum);
功能:
給指定進程發送信號。
注意:使用 kill() 函數發送信號,接收信號進程和發送信號進程的所有者必須相同,或者發送信號進程的所有者是超級用戶。
參數:
pid:?取值有 4 種情況:
pid > 0: 將信號傳送給進程 ID 為pid的進程。
pid = 0: 將信號傳送給當前進程所在進程組中的所有進程。
pid = -1: 將信號傳送給系統內所有的進程。
pid < -1: 將信號傳給指定進程組的所有進程。這個進程組號等于 pid 的絕對值。
signum:?信號的編號,這里可以填數字編號,也可以填信號的宏定義,可以通過命令?kill -l?("l" 為字母)進行相應查看。
返回值:
成功:0
失敗:-1
下面為測試代碼,本來父子進程各自每隔一秒打印一句話,3 秒后,父進程通過 kill() 函數給子進程發送一個中斷信號?SIGINT( 2 號信號),最終,子進程結束,剩下父進程在打印信息:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h>int main(int argc, char *argv[]) {pid_t pid;int i = 0;pid = fork(); // 創建進程if( pid < 0 ){ // 出錯perror("fork");}if(pid == 0){ // 子進程while(1){printf("I am son\n");sleep(1);}}else if(pid > 0){ // 父進程while(1){printf("I am father\n");sleep(1);i++;if(3 == i){// 3秒后kill(pid, SIGINT); // 給子進程 pid ,發送中斷信號 SIGINT// kill(pid, 2); // 等級于kill(pid, SIGINT);}}}return 0; }等待信號
所需頭文件:
#include <unistd.h>
int pause(void);
功能:
等待信號的到來(此函數會阻塞)。將調用進程掛起直至捕捉到信號為止,此函數通常用于判斷信號是否已到。
參數:
無。
返回值:
直到捕獲到信號才返回 -1,且 errno 被設置成 EINTR。
測試代碼如下:
#include <unistd.h> #include <stdio.h>int main(int argc, char *argv[]) {printf("in pause function\n");pause();return 0; }
沒有產生信號前,進程一直阻塞在 pause() 不會往下執行,假如,我們按“Ctrl+c”,pause() 會捕獲到此信號,中斷當前進程。
處理信號
一個進程收到一個信號的時候,可以用如下方法進行處理:
1)執行系統默認動作
對大多數信號來說,系統默認動作是用來終止該進程。
2)忽略此信號
接收到此信號后沒有任何動作。
3)執行自定義信號處理函數
用用戶定義的信號處理函數處理該信號。
注意:SIGKILL 和 SIGSTOP 不能更改信號的處理方式,因為它們向用戶提供了一種使進程終止的可靠方法。
產生一個信號,我們可以讓其執行自定義信號處理函數。假如有函數 A, B, C,我們如何確定信號產生后只調用函數 A,而不是函數 B 或 C。這時候,我們需要一種規則規定,信號產生后就調用函數 A,就像交通規則一樣,紅燈走綠燈行,信號注冊函數 signal() 就是做這樣的事情。
所需頭文件:
#include <signal.h>
typedef void (*sighandler_t)(int);// 回調函數的聲明
sighandler_t signal(int signum,sighandler_t handler);
功能:
注冊信號處理函數(不可用于 SIGKILL、SIGSTOP 信號),即確定收到信號后處理函數的入口地址。此函數不會阻塞。
參數:
signum:信號的編號,這里可以填數字編號,也可以填信號的宏定義,可以通過命令?kill -l?("l" 為字母)進行相應查看。
handler: 取值有 3 種情況:
SIG_IGN:忽略該信號
SIG_DFL:執行系統默認動作
信號處理函數名:自定義信號處理函數,如:fun
回調函數的定義如下:
void fun(int signo)
{
// signo 為觸發的信號,為 signal() 第一個參數的值
}
注意:信號處理函數應該為可重入函數,關于可重入函數的更多詳情,請《淺談可重入函數與不可重入函數》。返回值:
成功:第一次返回 NULL,下一次返回此信號上一次注冊的信號處理函數的地址。如果需要使用此返回值,必須在前面先聲明此函數指針的類型。
失敗:返回 SIG_ERR
示例一:
#include <stdio.h> #include <signal.h> #include <unistd.h>// 信號處理函數 void signal_handler(int signo) {if(signo == SIGINT){printf("recv SIGINT\n");}else if(signo == SIGQUIT){printf("recv SIGQUIT\n");} }int main(int argc, char *argv[]) {printf("wait for SIGINT OR SIGQUIT\n");/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */// 信號注冊函數signal(SIGINT, signal_handler);signal(SIGQUIT, signal_handler);// 等待信號pause();pause();return 0; }示例二:
#include <stdio.h> #include <signal.h> #include <unistd.h>// 回調函數的聲明 typedef void (*sighandler_t)(int);void fun1(int signo) {printf("in fun1\n"); }void fun2(int signo) {printf("in fun2\n"); }int main(int argc, char *argv[]) {sighandler_t previous = NULL;// 第一次返回 NULLprevious = signal(SIGINT,fun1); if(previous == NULL){printf("return fun addr is NULL\n");}// 下一次返回此信號上一次注冊的信號處理函數的地址。previous = signal(SIGINT, fun2);if(previous == fun1){printf("return fun addr is fun1\n");}// 還是返回 NULL,因為處理的信號變了previous = signal(SIGQUIT,fun1);if(previous == NULL){printf("return fun addr is NULL\n");}return 0; }運行結果如下:
信號集與信號阻塞集
信號集
為了方便對多個信號進行處理,一個用戶進程常常需要對多個信號做出處理,在 Linux 系統中引入了信號集(信號的集合)。這個信號集有點類似于我們的 QQ 群,一個個的信號相當于 QQ 群里的一個個好友。
信號集是用來表示多個信號的數據類型(sigset_t),其定義路徑為:/usr/include/i386-linux-gnu/bits/sigset.h。
信號集相關的操作主要有如下幾個函數
#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigismember(const sigset_t *set, int signum); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum);#include <signal.h> #include <stdio.h>int main(int argc, char *argv[]) {sigset_t set; // 定義一個信號集變量int ret = 0;sigemptyset(&set); // 清空信號集的內容// 判斷 SIGINT 是否在信號集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGINT);if(ret == 0){printf("SIGINT is not a member of set \nret = %d\n", ret);}sigaddset(&set, SIGINT); // 把 SIGINT 添加到信號集 setsigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信號集 set// 判斷 SIGINT 是否在信號集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGINT);if(ret == 1){printf("SIGINT is a member of set \nret = %d\n", ret);}sigdelset(&set, SIGQUIT); // 把 SIGQUIT 從信號集 set 移除// 判斷 SIGQUIT 是否在信號集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGQUIT);if(ret == 0){printf("SIGQUIT is not a member of set \nret = %d\n", ret);}return 0; }
信號阻塞集(屏蔽集、掩碼)
信號阻塞集也稱信號屏蔽集、信號掩碼。每個進程都有一個阻塞集,創建子進程時子進程將繼承父進程的阻塞集。信號阻塞集用來描述哪些信號遞送到該進程的時候被阻塞(在信號發生時記住它,直到進程準備好時再將信號通知進程)。
所謂阻塞并不是禁止傳送信號, 而是暫緩信號的傳送。若將被阻塞的信號從信號阻塞集中刪除,且對應的信號在被阻塞時發生了,進程將會收到相應的信號。
我們可以通過 sigprocmask()?修改當前的信號掩碼來改變信號的阻塞情況。
所需頭文件:
#include <signal.h>
int sigprocmask(int how,?const sigset_t *set,?sigset_t *oldset);
功能:
檢查或修改信號阻塞集,根據 how 指定的方法對進程的阻塞集合進行修改,新的信號阻塞集由 set 指定,而原先的信號阻塞集合由 oldset 保存。
參數:
how: 信號阻塞集合的修改方法,有 3 種情況:
SIG_BLOCK:向信號阻塞集合中添加 set 信號集,新的信號掩碼是set和舊信號掩碼的并集。
SIG_UNBLOCK:從信號阻塞集合中刪除 set 信號集,從當前信號掩碼中去除 set 中的信號。
SIG_SETMASK:將信號阻塞集合設為 set 信號集,相當于原來信號阻塞集的內容清空,然后按照 set 中的信號重新設置信號阻塞集。
set:?要操作的信號集地址。
若 set 為 NULL,則不改變信號阻塞集合,函數只把當前信號阻塞集合保存到 oldset 中。
oldset:?保存原先信號阻塞集地址
返回值:
成功:0,
失敗:-1,失敗時錯誤代碼只可能是 EINVAL,表示參數 how 不合法。
注意:不能阻塞 SIGKILL 和 SIGSTOP 等信號,但是當 set 參數包含這些信號時 sigprocmask() 不返回錯誤,只是忽略它們。另外,阻塞 SIGFPE 這樣的信號可能導致不可挽回的結果,因為這些信號是由程序錯誤產生的,忽略它們只能導致程序無法執行而被終止。
示例代碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h>int main(int argc, char *argv[]) {sigset_t set; // 信號集合int i = 0;sigemptyset(&set); // 清空信號集合sigaddset(&set, SIGINT); // SIGINT 加入 set 集合while(1){// set 集合加入阻塞集,在沒有移除前,SIGINT 會被阻塞sigprocmask(SIG_BLOCK, &set, NULL);for(i=0; i<5; i++){printf("SIGINT signal is blocked\n");sleep(1);}// set 集合從阻塞集中移除// 假如 SIGINT 信號在被阻塞時發生了// 此刻,SIGINT 信號立馬生效,中斷當前進程sigprocmask(SIG_UNBLOCK, &set, NULL);for(i=0; i<5; i++){printf("SIGINT signal unblocked\n");sleep(1);}}return 0; }可靠信號的操作
從 UNIX 系統繼承過來的信號(SIGHUP~SIGSYS,前 32 個)都是不可靠信號,不支持排隊(多次發送相同的信號, 進程可能只能收到一次,可能會丟失)。
SIGRTMIN 至 SIGRTMAX 的信號支持排隊(發多少次, 就可以收到多少次, 不會丟失),故稱為可靠信號。
可靠信號就是實時信號,非可靠信號就是非實時信號。
signal() 函數只能提供簡單的信號安裝操作,使用 signal() 函數處理信號比較簡單,只要把要處理的信號和處理函數列出即可。
signal() 函數主要用于前面 32 種不可靠、非實時信號的處理,并且不支持信號傳遞信息。
Linux 提供了功能更強大的 sigaction() 函數,此函數可以用來檢查和更改信號處理操作,可以支持可靠、實時信號的處理,并且支持信號傳遞信息。
下面我們一起學習其相關函數的使用。
所需頭文件:
#include <signal.h>
int sigqueue(pid_t pid, int sig,?const union sigval value);
功能:
給指定進程發送信號。
參數:
pid:?進程號。
sig: 信號的編號,這里可以填數字編號,也可以填信號的宏定義,可以通過命令 kill -l ("l" 為字母)進行相應查看。
value:?通過信號傳遞的參數。
union sigval 類型如下:
union sigval {int sival_int;void *sival_ptr; };
返回值:
成功:0
失敗:-1
int sigaction(int signum,const struct sigaction *act,?struct sigaction *oldact );
功能:
檢查或修改指定信號的設置(或同時執行這兩種操作)。
參數:
signum:要操作的信號。
act: ? 要設置的對信號的新處理方式(設置)。
oldact:原來對信號的處理方式(設置)。
如果 act 指針非空,則要改變指定信號的處理方式(設置),如果 oldact 指針非空,則系統將此前指定信號的處理方式(設置)存入 oldact。
返回值:
成功:0
失敗:-1
信號設置結構體:
struct sigaction {/*舊的信號處理函數指針*/void (*sa_handler)(int signum) ;/*新的信號處理函數指針*/void (*sa_sigaction)(int signum, siginfo_t *info, void *context);sigset_t sa_mask;/*信號阻塞集*/int sa_flags;/*信號處理的方式*/ };sa_handler、sa_sigaction:信號處理函數指針,和 signal() 里的函數指針用法一樣,應根據情況給 sa_sigaction、sa_handler 兩者之一賦值,其取值如下:
SIG_IGN:忽略該信號
SIG_DFL:執行系統默認動作
處理函數名:自定義信號處理函數
sa_mask:信號阻塞集sa_flags:用于指定信號處理的行為,它可以是一下值的“按位或”組合:
SA_RESTART:使被信號打斷的系統調用自動重新發起(已經廢棄)
SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 信號。
SA_NOCLDWAIT:使父進程在它的子進程退出時不會收到 SIGCHLD 信號,這時子進程如果退出也不會成為僵尸進程。
SA_NODEFER:使對信號的屏蔽無效,即在信號處理函數執行期間仍能發出這個信號。
SA_RESETHAND:信號處理之后重新設置為默認的處理方式。
SA_SIGINFO:使用 sa_sigaction 成員而不是 sa_handler 作為信號處理函數。
void (*sa_sigaction)( int signum,? siginfo_t *info,? void *context );
參數說明:
signum:信號的編號。
info:記錄信號發送進程信息的結構體,進程信息結構體路徑:/usr/include/i386-linux-gnu/bits/siginfo.h,其結構體詳情請點此鏈接。
context:可以賦給指向 ucontext_t 類型的一個對象的指針,以引用在傳遞信號時被中斷的接收進程或線程的上下文,其結構體詳情點此鏈接。
下面我們做這么一個例子,一個進程在發送信號,一個進程在接收信號的發送。
發送信號示例代碼如下:
#include <stdio.h> #include <signal.h> #include <sys/types.h> #include <unistd.h>/******************************************************* *功能: 發 SIGINT 信號及信號攜帶的值給指定的進程 *參數: argv[1]:進程號argv[2]:待發送的值(默認為100) *返回值: 0 ********************************************************/ int main(int argc, char *argv[]) {if(argc >= 2){pid_t pid,pid_self;union sigval tmp;pid = atoi(argv[1]); // 進程號if( argc >= 3 ){tmp.sival_int = atoi(argv[2]);}else{tmp.sival_int = 100;}// 給進程 pid,發送 SIGINT 信號,并把 tmp 傳遞過去sigqueue(pid, SIGINT, tmp);pid_self = getpid(); // 進程號printf("pid = %d, pid_self = %d\n", pid, pid_self);}return 0; }#include <signal.h> #include <stdio.h>// 信號處理回電函數 void signal_handler(int signum, siginfo_t *info, void *ptr) {printf("signum = %d\n", signum); // 信號編號printf("info->si_pid = %d\n", info->si_pid); // 對方的進程號printf("info->si_sigval = %d\n", info->si_value.sival_int); // 對方傳遞過來的信息 }int main(int argc, char *argv[]) {struct sigaction act, oact;act.sa_sigaction = signal_handler; //指定信號處理回調函數sigemptyset(&act.sa_mask); // 阻塞集為空act.sa_flags = SA_SIGINFO; // 指定調用 signal_handler// 注冊信號 SIGINTsigaction(SIGINT, &act, &oact);while(1){printf("pid is %d\n", getpid()); // 進程號pause(); // 捕獲信號,此函數會阻塞}return 0; }
兩個終端分別編譯代碼,一個進程接收,一個進程發送,運行結果如下:
總結
以上是生活随笔為你收集整理的【linux系统编程】进程间通信:信号中断处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Linux系统编程】进程的控制:结束进
- 下一篇: 【Linux系统编程】进程间通信--无名