信號是在軟件層面對中斷機制的一種模擬,信號的出現使得進程直接的通信不在是被動的,不在向之前那樣,read()操作往往需要等待write()操作結束。因爲信號是對中斷的一種模擬。既然是中斷,那麼它的發生就是不確定。就不會發生一個進程阻塞在這裏等待另一個進程執行的結果。這樣的異步性通信機制無疑是更加強大的。
在終端輸入kill -l可以查看當前系統所支持的所有信號。(我這個是Ubuntu)
可以看到有64個信號,其中有兩個較爲特殊的信號是SIGRTMIN和SIGRTMAX。Linux下的通信機制是遵從POSIX標準的。34號信號SIGRTMIN信號之前的是早期UNIX操作系統的。它們是不可靠的信號。它的主要問題是:進程每次處理信號後,會設置對該信號的默認處理動作,有時候我們不想讓他這麼處理了(按照默認處理),這時候就需要調用signal()函數重新安裝一次信號。這樣會形成新的默認動作。還有更加討厭的是,信號有可能會丟失。
Linux對不可靠信號做了一些改進,現在的主要問題變成了“信號會丟失”。
後來POSIX僅僅只對可靠信號做了標準化。信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號。可靠信號它不會丟失。
可靠信號都是實時信號,不可靠信號都是非實時信號。可靠信號都支持隊列處理,不可靠信號不支持隊列處理。在UNIX時代就定義好了前面的不可靠信號的功能,而後來增加的可靠信號是讓用戶自定義使用的。
信號處理的三種方式:
- 忽略信號:對信號不做任何處理,就當做沒發生任何事情一樣。(SIGKILL和SIGSTOP這兩個不能忽略)
- 捕捉信號:定義信號處理函數,當信號發出的時候,執行相應的操作。(這個和Qt的信號槽差不多)
- 執行默認動作:Linux對每一個信號都規定了默認操作(可靠信號的默認操作是進程終止)。
發送信號
發送信號的函數有kill(),raise(),sigqueue(),alarm(),setittimer(),abort()。常用的是kill()。它們依賴的頭文件是#include<signal.h>和#include<sys/types.h>
函數原型:int kill(pid_t pid,int sig);
函數功能:用來將sig所指定的信號發送到pid所指定的進程。
pid有下面幾種情形,分別對應於不同情況下應用。
- pid > 0:把信號傳遞到進程ID爲pid的進程
- pid == 0:把信號傳送給當前進程所在組的所有進程
- pid == -1:將信號以廣播的形式傳送給系統內所有進程
- pid < -1: 講信號傳遞給進程組識別碼爲pid絕對值的所有進程
函數執行成功返回0,否則返回-1.
測試代碼如下:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<wait.h>
#include<unistd.h>
int main()
{
pid_t pid;
int statu;
pid = fork();
if(0 == pid)
{
printf("son\n");
sleep(5);
printf("I am son!\n");
exit(0);
}
if(0 < pid)
{
sleep(2);
printf("father\n");
kill(pid,SIGABRT); //SIGABRT是終止子進程
wait(NULL);
printf("My son GG\n");
exit(0);
}
return 0;
}
讓子進程先執行,打印出son。然後讓子進程掛起。輪到父進程執行,父進程執行到kill()函數的時候給子進程發了個SIGABRT信號,讓子進程終止了。然後wait()回收子進程,打印My son GG.
執行結果如下:
可以看到,子進程收到SIGABRT信號後,終止了。沒有向屏幕打印I am son.關於信號的詳解,看這裏:https://blog.csdn.net/zy010101/article/details/83932113
上面的kill函數發送的信號是不可靠信號,它執行默認操作。即:終止進程。如果我們需要自定義信號處理方式,那麼就需要安裝信號。Linux安裝信號主要由signal()和sigaction()完成。signal是在可靠信號系統調用的基礎上實現的,是庫函數。
signal()的原型很複雜,我們還是從signal.h這個頭文件來看一下吧!
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
__THROW;
可以看到signal有兩個參數,一個是信號值,另一個我們再來看看
typedef void (*__sighandler_t) (int);
另一個是這樣的一個函數指針變量,那麼說明__handler代表了一個函數的入口地址(實際就是函數)。另外,這個函數指針指向的函數需要一個int類型的參數。signal函數的返回值也是一個函數指針。
注意:__handler如果不是函數指針,它只能是SIG_IGN或者是SIG_DFL.
SIG_IGN:忽略參數指定的信號。(忽略該信號)
SIG_DFL:將參數指定的信號重新設置爲內核默認的處理方式。
返回值:signal函數本身在成功時返回NULL,它的參數__handler則會返回處理信號的函數的地址(函數指針)。失敗返回:SIG_ERR.(看不明白函數指針的同學,請閱讀《C缺陷與陷阱》,《C與指針》好像是這兩本講的很清楚,我記不太清了)
所以這就要求自定義的信號處理函數的函數原型是這樣的:
void 函數名(int 參數名);即:函數必須有一個int類型的參數。
signal()函數只是定義了將指定信號傳送到指定進程。還需要一個用於捕捉信號的函數。在Linux下pause()函數用於捕捉信號,如果沒有信號發生,pause函數將會一直等待。直到有信號發生。
函數原型:int pause();
當pause函數捕捉到信號的時候返回-1(注意不是捕捉到的信號的值)。
測試程序如下:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<wait.h>
//自定義的信號處理函數
void My_Fun(int sig)
{
if(SIGRTMIN == sig)
{
printf("MIN!\n");
}
if(SIGRTMAX == sig)
{
printf("MAX!\n");
}
}
int main()
{
//註冊信號處理函數
signal(SIGRTMIN,My_Fun);
signal(SIGRTMAX,My_Fun);
//掛起10s
sleep(3);
//發出信號
kill(getpid(),SIGRTMIN); //getpid()函數用於獲取當前進程的pid.
kill(getpid(),SIGRTMAX);
return 0;
}
輸出結果如下:
這樣就完成了自定義信號的使用。使用自定義信號有兩個關鍵點。一是必須註冊自定義信號的處理函數,二是必須發送自定義信號。怎麼樣發送自定義信號由你自己來定義,這爲程序設計帶來了極大的便利。比如上面我們只是直接了當的發送兩個信號。你也可以使當滿足一定條件的時候才發送信號。比如下面這樣。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<wait.h>
//自定義的信號處理函數
void My_Fun(int sig)
{
if(SIGRTMIN == sig)
{
printf("MIN!\n");
}
if(SIGRTMAX == sig)
{
printf("MAX!\n");
}
}
int main()
{
//註冊信號處理函數
signal(SIGRTMIN,My_Fun);
signal(SIGRTMAX,My_Fun);
//發出信號
char c;
while(1)
{
scanf("%c",&c);
getchar(); //吸收回車
if('a' == c)
{
kill(getpid(),SIGRTMIN); //getpid()函數用於獲取當前進程的pid.
}
else
{
kill(getpid(),SIGRTMAX);
}
}
return 0;
}
運行結果如下:
這樣就實現了發送信號的控制。可以想象,鍵盤,鼠標等發送的信號很有可能就是被系統採取這樣的方式處理的。