引言
程序間通訊(interprocess communication,簡稱 IPC) 指兩個程序之間的通訊。 系統中的每一個程序都有
各自的地址空間,並且相互獨立、隔離, 每個程序都處於自己的地址空間中。 所以同一個程序的不同模組(譬如不同的函式)之間進行通訊都是很簡單的,譬如使用全域性變數等。但是,兩個不同的程序之間要進行通訊通常是比較難的,因為這兩個程序處於不同的地址空間中;通常情況下,大部分的程式是不要考慮程序間通訊的,因為大家所接觸絕大部分程式都是單程序程式(可以有多個執行緒),對於一些複雜、大型的應用程式,則會根據實際需要將其設計成多程序程式,譬如 GUI、服務區應用程式等 。
程序間通訊通常有:管道,訊息佇列,訊號量,共享記憶體,socket,streams。其中socket和streams支援兩個不同主機間的程序通訊
管道
把一個程序連線到另一個程序的資料流稱為管道,管道被抽象成一個檔案,這種檔案叫管道檔案,都是該檔案不屬於如何檔案系統。管道分為匿名管道和命名管道
匿名管道
匿名管道的特點:也稱為無名管道,是最常用的管道
- 半雙工,同一時間只能由一端傳送給另一端
- 只能在父子或者兄弟程序間使用
- 管道的本質是檔案,管道檔案不屬於任何檔案系統
pipe():用於建立一個匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
返回值:成功返回0,失敗返回-1,並且設定errno。
引數介紹:
- fd為檔案描述符陣列,其中
fd[0]
表示讀端,fd[1]
表示寫端
父子程序通訊過程解析
- 父程序建立管道,得到兩個檔案描述符指向管道的兩端;
- 父程序fork出子程序,子程序也有兩個檔案描述符指向同一個管道。
- 父程序關閉
fd[0](讀端)
,子程序關閉fd[1](寫端)
,因為管道只支援單向通訊。(也可父寫子讀)
示例:子程序寫資料,父程序讀資料
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2];
pid_t pid;
char buf[128]={0};
if(pipe(fd)==-1) //建立管道
{
perror("pipe error");
exit(-1);
}
pid = fork(); //建立子程序
switch(pid)
{
case -1:
perror("fork error");
exit(-1);
case 0: //子程序寫資料,父程序讀資料
printf("我是子程序\r\n");
close(fd[0]); //關閉讀功能
write(fd[1],"hello father",strlen("hello father"));
_exit(0);
default:
break;
}
//以下是父程序執行的內容
sleep(1);
printf("我是父程序\r\n");
close(fd[1]); //關閉寫功能
read(fd[0],buf,128);
printf("read from child: %s\r\n",buf);
wait(NULL);
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
我是子程序
我是父程序
read from child: hello father
命名管道
不同於匿名管道之處在於它提供一個路徑名與之關聯,以FIFO的檔案形式儲存於檔案系統中。說白了就是命名管道會以檔案儲存在系統中。
命名管道特徵:
- 可以進行不相干程序間的通訊
- 命名管道是一個檔案,對於檔案的相關操作對其同樣適用
- 對於管道檔案,當前程序操作為只讀時,則進行阻塞,直至有程序對其寫入資料
- 對於管道檔案,當前程序操作為只寫時,則進行阻塞,直至有程序從管道中讀取資料
mkfifo():用於建立一個命名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
返回值:成功都返回0,失敗都返回-1,並會設定errno。
引數介紹:
- path:建立的命名管道的全路徑名
- mode:指定了檔案的讀寫許可權
注意
- 命名管道和匿名管道的使用方法法基本是相同的。只是使用命名管道時,必須先呼叫
open()
將其開啟。因為命名管道是一個存在於硬碟上的檔案,而匿名管道是存在於記憶體中的特殊檔案。 - 呼叫open()開啟命名管道的程序可能會被阻塞。
- 但如果同時用讀寫方式( O_RDWR)開啟,則一定不會導致阻塞
- 如果以只讀方式( O_RDONLY)開啟,則呼叫open()函式的程序將會被阻塞直到有寫方開啟管道;
- 同樣以只寫方式( O_WRONLY)開啟也會阻塞直到有讀方式開啟管道。
示例:
//服務端,讀資料的程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
umask(0);//將許可權清0
if(mkfifo("./file",0666|S_IFIFO)==-1 && errno!=EEXIST){//建立管道
perror("mkfifo");
return 1;
}
int fd = open("./file",O_RDONLY);//開啟管道
if(fd < 0){
perror("open");
return 2;
}
char buf[1024];
while(1){
buf[0] = 0;
printf("請等待。。。\n");
ssize_t s = read(fd,buf,sizeof(buf)-1);
if(s > 0){
buf[s-1] = 0;//過濾\n
printf("伺服器:%s\n",buf);
}else if(s == 0){//當客戶端退出時,read返回0
printf("客戶端退出,自己退出\n");
break;
}
}
close(fd);
return 0;
}
//客戶端,寫資料的程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("./file",O_WRONLY);//開啟管道
if(fd < 0){
perror("open");
return 1;
}
char buf[1024];
while(1){
printf("客戶端:");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);//向管道檔案中寫資料
if(s > 0){
buf[s] = 0;//以字串的形式寫
write(fd,buf,strlen(buf));
}
}
close(fd);
return 0;
}
訊息佇列
是訊息的連結表,存放在核心中。一個訊息佇列由一個識別符號(即佇列ID)來標識。
訊息佇列特點:
- 訊息佇列是面向記錄的,其中的訊息具有特定的格式以及特定的優先順序。
- 訊息佇列獨立於傳送與接收程序。程序終止時,訊息佇列及其內容並不會被刪除。
- 訊息佇列可以實現訊息的隨機查詢,訊息不一定要以先進先出的次序讀取,也可以按訊息的型別讀取
標頭檔案
#include <sys/msg.h>
ftok()
系統建立IPC通訊(如訊息佇列、共享記憶體時)必須指定一個ID值。通常情況下,該id值透過ftok函式得到。
key_t ftok(const char *pathname, int proj_id);
返回值:當成功執行的時候,一個key_t值將會被返回,否則 -1 被返回。
引數介紹:
- pathname:傳入一個路徑(一般是當前路徑“ . ”)。
- proj_id:隨便填寫一個數(要做通訊的話通訊的另外一端要與這個數保持一致才能找到對應的icpID)。
建立或開啟訊息佇列:
int msgget(key_t key, int flag);
返回值:成功返回佇列ID,失敗返回-1。
引數介紹:
- key:key是一個鍵值,由ftok獲得。通訊雙方要想通訊,key值要一樣。
- flag:標識函式的行為以及訊息佇列的許可權
- IPC_CREAT:建立訊息佇列
- IPC_EXCL:檢測訊息佇列是否存在
- 位或許可權位:訊息佇列位或許可權位後可以設定訊息佇列的訪問許可權,格式和open函式的mode_t一樣,但可執行許可權未使用
新增訊息
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
返回值:成功返回0,失敗返回-1。
引數介紹:
-
msqid:訊息物件ID。
-
ptr:要是的訊息的結構體變數的地址
// 訊息結構 struct msgbuf { long mtype; //訊息的型別 char mtext[256]; //訊息的內容 };
-
size:訊息正文的位元組數(等於訊息結構體的大小減去long型別的大小)
-
flag:0:msgsnd呼叫阻塞直到條件滿足為止;IPC_NOWAIT:若訊息沒有立刻傳送則呼叫該函式的程序會立刻返回
讀取訊息
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
返回值:成功返回訊息資料的長度,失敗返回-1
引數介紹:
- msqid:訊息佇列的識別符號,代表要從哪個訊息佇列中獲取訊息
- ptr:存放訊息的結構體地址
- size:訊息正文的位元組數
- type:感興趣的訊息型別,可以有以下幾種型別
- type=0:返回佇列中第一個訊息
- type>0:返回佇列中訊息型別為type的訊息
- type<0:返回佇列中訊息型別小於等於msgtyp的絕對值的訊息,如果這種訊息有若干個,則取型別值最小的訊息
- flag:函式的控制屬性
- 0:msgrcv呼叫阻塞直到接收訊息成功為止
- MSG_NOERROR:若返回的訊息位元組數比nbytes位元組數多,則訊息就會截斷到nbytes位元組,且不通知訊息傳送程序;
- IPC_NOWAIT:呼叫程序會立即返回,若沒有收到訊息則立即返回-1
訊息佇列的控制
對訊息佇列進行各種控制,如刪除訊息佇列,修改訊息佇列的屬性
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回值:成功返回0,失敗返回-1。
引數介紹:
- msqid:訊息佇列的識別符號 c
- md:函式功能的控制
- IPC_RMID:刪除由msqid標識的訊息佇列,將他從系統中刪除並破壞相關的資料結
- IPC_STAT:將msqid相關的資料結構中各個元素的當前值存入到由buf指向的結構中 IPC_SET
- 將msqid相關的資料結構中的元素設定為由buf指向的結構中的對應值
- buf:msqid_ds資料型別的地址,用來存放或更改訊息佇列的屬性
示例
/*程序A*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>
// 訊息結構
struct msgbuf
{
long mtype; //訊息的型別
char mtext[128]; //訊息的內容
};
int main()
{
key_t key;
key = ftok(".",'z');
printf("key=%x\r\n",key);
int msgid = msgget(key,IPC_CREAT|0777);
if(-1 == msgid)
{
printf("msgget error\r\n");
return -1;
}
//從佇列中讀取訊息
struct msgbuf readBuf;
msgrcv(msgid,&readBuf,sizeof(readBuf.mtext),888,0); //讀取型別為888的資料
printf("read from Que:%s\r\n",readBuf.mtext);
//傳送訊息道佇列
struct msgbuf sendBuf = {888,"我是程序A"};
msgsnd(msgid,&sendBuf,strlen(sendBuf.mtext),0); //傳送型別為888的資料
msgctl(msgid,IPC_RMID,NULL); //刪除訊息佇列
}
/*程序B*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype; //訊息的型別
char mtext[128]; //訊息的內容
};
int main()
{
key_t key;
key = ftok(".",'z');
printf("key=%x\r\n",key);
int msgid = msgget(key,IPC_CREAT|0777);
if(-1 == msgid)
{
printf("msgget error\r\n");
return -1;
}
//傳送訊息道佇列
struct msgbuf sendBuf = {888,"我是程序B"};
msgsnd(msgid,&sendBuf,strlen(sendBuf.mtext),0); //傳送型別為888的資料
//從佇列中讀取訊息
struct msgbuf readBuf;
msgrcv(msgid,&readBuf,sizeof(readBuf.mtext),888,0); //讀取型別為888的資料
printf("read from Que:%s\r\n",readBuf.mtext);
msgctl(msgid,IPC_RMID,NULL); //刪除訊息佇列
}
共享記憶體
共享記憶體就是對映一段能被其它程序所訪問的記憶體, 這段共享記憶體由一個程序建立, 但其它的多個程序
都可以訪問, 使得多個程序可以訪問同一塊記憶體空間。共享記憶體是最快的 IPC 方式, 它是針對其它程序間
通訊方式執行效率低而專門設計的, 它往往與其它通訊機制, 譬如結合訊號量來使用, 以實現程序間的同步
和通訊
共享記憶體(Shared Memory),指兩個或多個程序共享一個給定的儲存區。
共享記憶體特點:
- 共享記憶體是最快的一種 IPC,因為程序是直接對記憶體進行存取。
- 因為多個程序可以同時操作,所以需要進行同步。
- 訊號量+共享記憶體通常結合在一起使用,訊號量用來同步對共享記憶體的訪問。
共享記憶體操作步驟:
- 建立共享記憶體/開啟共享記憶體
- 連線:連線該共享記憶體到當前程序的地址空間,連線成功後把共享記憶體區物件對映到呼叫程序的地址空間,隨後可像本地空間一樣訪問。
- 讀寫資料:
- 分離:並不是從系統中刪除該共享記憶體,只是當前程序不能再訪問該共享記憶體而已。
- 銷燬:從系統中刪除該共享記憶體。
標頭檔案
#include <sys/shm.h>
建立或獲取——shmget()
建立或獲取一個共享記憶體。
#include <sys/ipc.h>
int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回共享記憶體ID(shmid),失敗返回-1。
引數介紹:
- key:key是一個鍵值,由ftok獲得。用於唯一標識一塊共享記憶體
- size:共享記憶體大小。而如果引用一個已存在的共享記憶體,則將 size 指定為0 。
- shmflg:該引數用於確定共享記憶體屬性。
標誌位 | 記憶體許可權
- IPC_CREAT;IPC_EXCL
- 值得注意
PC_EXCL
無法單獨使用
連線——shmat()
建立共享記憶體後還不能直接使用,需要找到記憶體地址後才能使用,即連線。
#include <sys/types.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
返回值:連線成功返回共享記憶體在程序中的起始地址,失敗返回-1。
引數介紹:
- shmid:共享記憶體ID,即shmget的建立成功的返回值
- shmaddr:用於確定將共享記憶體掛在程序虛擬地址哪個位置,一般填
nullptr
即可代表讓核心自己確定位置。 - shmflg:用於確定掛接方式,
一般填0
。
分離——shmdt()
當使用共享記憶體完畢後,需要分離掛接的共享記憶體。
注意
,這並不是從系統中刪除該共享記憶體,只是當前程序不能再訪問該共享記憶體而已。
#include <sys/types.h>
int shmdt(const void *shmaddr);
返回值:分離成功返回0,失敗返回-1。
引數介紹:
- shmaddr:為共享記憶體在程序中地址位置,一般填
nullptr
。
控制——shmctl()
該介面本身用於控制共享記憶體,可用於銷燬。
#include <sys/ipc.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:成功返回0,失敗返回-1。
引數介紹:
- shmid:共享記憶體ID,即shmget的建立成功的返回值
- cmd:函式功能的控制
- IPC_RMID:銷燬由shqid標識的共享記憶體
- ···
- buf:傳nullptr。
ipcs:該指令為系統指令。使用時可以檢視當前全部共享記憶體。ipcs -m
ipcrm:透過指定共享記憶體shmid,進行刪除。ipcrm -m [shmid]
示例
/*程序A*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
int main()
{
int shmid;
key_t key;
char *shmaddr;
key = ftok(".",1);
shmid = shmget(key,1024,IPC_CREAT|0666); //建立共享記憶體
if(-1==shmid)
{
printf("shmget error\r\n");
return -1;
}
shmaddr = shmat(shmid,0,0); //連線對映
if(-1 == shmaddr)
{
printf("shmat error\r\n");
}
strcpy(shmaddr,"hello world"); //往共享記憶體對映在程序空間中的記憶體中寫資料
sleep(5); //等待程序B讀取完
shmdt(shmaddr); //分離
shmctl(shmid,IPC_RMID,0); //銷燬
printf("共享記憶體已銷燬\r\n");
}
/*程序B*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main()
{
int shmid;
key_t key;
char *shmaddr;
key = ftok(".",1);
shmid = shmget(key,1024,0); //開啟共享記憶體
if(-1 == shmid)
{
printf("shmget error\r\n");
return -1;
}
shmaddr = shmat(shmid,0,0); //連線對映
if(-1 == shmaddr)
{
printf("shmat error\r\n");
}
printf("read data:%s\r\n",shmaddr); //從共享記憶體對映在程序空間中的記憶體中讀資料並列印
shmdt(shmaddr); //分離
// shmctl(shmid,PC_RMID,nullptr); //銷燬,另一個程序A已經銷燬
printf("共享記憶體已銷燬\r\n");
}
訊號
訊號是事件發生時對程序的通知機制,也可以把它稱為軟體中斷。訊號與硬體中斷的相似之處在於能夠
打斷程式當前執行的正常流程, 其實是在軟體層次上對中斷機制的一種模擬。 大多數情況下,是無法預測信
號達到的準確時間,所以,訊號提供了一種處理非同步事件的方法
訊號的目的是用來通訊的,用於通知接收訊號的程序有某種事件發生,所以可用於程序間通訊;除了用於程序間通訊之外,程序還可以傳送訊號給程序本身。
訊號的分類
linux系統下可以從兩個角度對訊號進行分類,從可靠性方面將訊號分為可靠訊號與不可靠訊號,從實時性方面將訊號分為實時訊號與非實時訊號。
可靠訊號
可靠訊號支援排隊,不會丟失。編號34~64,可靠訊號沒有具體對應的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAXN 的方式來表示
不可靠訊號
編號 1~31 所對應的是不可靠訊號
訊號的處理方式有三種:忽略、捕捉和預設動作
程序對訊號的處理
signal()函式
signal()函式是 Linux 系統下設定訊號處理方式最簡單的介面, 可將訊號的處理方式設定為捕獲訊號、 忽略訊號以及系統預設操作
#include <signal.h>
typedef void (*sig_t)(int); //函式指標
sig_t signal(int signum, sig_t handler);
返回值:返回值: 此函式的返回值也是一個 sig_t 型別的函式指標,成功情況下的返回值則是指向在此之前的訊號處理函式;如果出錯則返回 SIG_ERR,並會設定 errno。
引數介紹:
-
signum: 此引數指定需要進行設定的訊號,可使用訊號名(宏)或訊號的數字編號,建議使用訊號名
-
handler: sig_t 型別的函式指標,指向訊號對應的訊號處理函式,當程序接收到訊號後會自動執行該處
理函式; SIG_IGN 或 SIG_DFL, SIG_IGN 表示此程序需要忽略該訊號, SIG_DFL 則表示設定為系統預設操作。 -
sig_t 函式指標的 int 型別引數指的是,當前觸發該函式的訊號,可將多個訊號繫結到同一個訊號處理函式
上,此時就可透過此引數來判斷當前觸發的是哪個訊號。
示例:
signal()函式將 SIGINT(2) 訊號繫結到了一個使用者自定的處理函式sig_handler(int sig)上, 當程序收到 SIGINT 訊號後會執行該函式然後執行 printf 列印語句 。
當執行程式之後,程式會卡在 for 死迴圈處,此時在終端按下中斷符 CTRL + C,系統便會給前臺程序組中的每一個程序傳送SIGINT 訊號,我們測試程式便會收到該訊號。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
sig_t ret = NULL;
ret = signal(SIGINT, (sig_t)sig_handler); //設定訊號處理方式
if (SIG_ERR == ret)
{
perror("signal error");
exit(-1);
}
/* 死迴圈 */
while(1);
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
^CReceived signal: 2
^CReceived signal: 2
^CReceived signal: 2
注意
- 程序建立 當一個程序呼叫 fork()建立子程序時,其子程序將會繼承父程序的訊號處理方式,因為子程序在開始時複製了父程序的記憶體映像,所以訊號捕獲函式的地址在子程序中是有意義的。
- 當一個應用程式剛啟動的時候(或者程式中沒有呼叫 signal()函式) , 通常情況下, 程序對所有訊號的
處理方式都設定為系統預設操作。
sigaction()函式
sigaction()允許單獨獲取訊號的處理函式而不是設定,並且還可以設定各種屬性對呼叫訊號處理函式時
的行為施以更加精準的控制。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
返回值:成功返回 0;失敗將返回-1,並設定 errno。
引數介紹:
- signum: 需要設定的訊號,除了 SIGKILL 訊號和 SIGSTOP 訊號之外的任何訊號。
- act: 指向一個 struct sigaction 資料結構,該資料結構描述了訊號的處理方式,如果引數 act 為 NULL,則表示無需改變訊號當前的處理方式。
- oldact:指向一個 struct sigaction 資料結構。如oldact 不為 NULL, 將訊號之前的處理方式等資訊透過引數 oldact 返回出來;為NULL表示不獲取之前訊號資訊。
**struct sigaction 結構體 **
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler:指定訊號處理函式,與 signal()函式的 handler 引數相同。
- sa_sigaction:也用於指定訊號處理函式,這是一個替代的訊號處理函式,可以透過該函式獲取到更多資訊,這些訊號透過 siginfo_t 引數獲取(自行查詢資料);
sa_handler 和sa_sigaction 是互斥的
,不能同時設定, 對於標準訊號來說, 使用 sa_handler 就可以了,可透過SA_SIGINFO 標誌進行選擇 - sa_mask:訊號掩碼可以避免一些訊號之間的競爭狀態(也稱為競態)。
- sa_flags:SA_SIGINFO如果設定了該標誌,則表示使用 sa_sigaction 作為訊號處理函式、而不是 sa_handler,設定為0表示使用sa_handler。
- sa_restorer:該成員已過時,不要再使用了。
示例:與signal()函式示例的功能相同
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
int ret;
struct sigaction sig = {0}; //初始化結構體
sig.sa_handler = sig_handler;
sig.sa_flags = 0; //值為0,呼叫sa_handler儲存的函式
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret) {
perror("sigaction error");
exit(-1);
}
/* 死迴圈 */
while(1);
exit(0);
}
}
向程序傳送訊號
與 kill 命令相類似, Linux 系統提供了 kill()系統呼叫,一個程序可透過 kill()向另一個程序傳送訊號;
除了 kill()系統呼叫之外, Linux 系統還提供了系統呼叫 killpg()以及庫函式 raise(),也可用於實現傳送訊號
的功能 。
kill()函式
可將訊號傳送給指定的程序或程序組中的每一個程序
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
返回值:成功返回 0;失敗將返回-1,並設定 errno。
引數介紹:
- pid:用於指定接收此訊號的程序 pid;除此之外,引數 pid 也可設定為 0 或-1 以及小於-1 等不同值,稍後給說明
- pid為正:則訊號 sig 將傳送到 pid 指定的程序
- pid等於0:則將 sig 傳送到當前程序的程序組中的每個程序。
- pid等於-1:則將 sig 傳送到當前程序有權傳送訊號的每個程序,但程序 1(init)除外。
- pid 小於-1,則將 sig 傳送到 ID 為-pid 的程序組中的每個程序。
- sig: 指定需要傳送的訊號,設定為 0表示不傳送訊號,但任執行錯誤檢查,這通常可用於檢查引數 pid 指定的程序是否存在。 如果向一個不存在的程序傳送訊號, kill()將會返回-1, errno 將被設定ESRCH,表示程序不存在
注意:程序傳送訊號給另外一個程序需要許可權
- 超級使用者root 程序可以將訊號傳送給任何程序
- 非超級使用者(普通使用者)程序來說,其基本規則是傳送者程序的實際使用者 ID 或有效使用者 ID 必須等於接收者程序的實際使用者 ID 或有效使用者 ID
示例: sigaction()函式的示例程式碼用於接收該訊號
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int pid;
/* 判斷傳參個數 */
if (2 > argc)
exit(-1);
/* 將傳入的字串轉為整形數字 */
pid = atoi(argv[1]);
printf("pid: %d\n", pid);
/* 向 pid 指定的程序傳送訊號 */
if (-1 == kill(pid, SIGINT))
{
perror("kill error");
exit(-1);
}
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./testApp & #接收訊號程序掛後臺執行,程序號為8879
[1] 8879
ten@ten-virtual-machine:~/H616/demo$ ./testApp2 8879 #給8879程序發訊號
pid: 8879
Received signal: 2 #接收程序的列印
raise()函式
用於程序向自身傳送訊號,raise(sig)等價於kill(getpid(), sig);
#include <signal.h>
int raise(int sig);
返回值: 成功返回 0;失敗將返回非零值。
引數介紹:sig 為需要傳送的訊號。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
//訊號處理函式
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
int ret;
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret)
{
perror("sigaction error");
exit(-1);
}
while(1)
{
/* 向自身傳送 SIGINT 訊號 */
if (0 != raise(SIGINT))
{
printf("raise error\n");
exit(-1);
}
sleep(3); // 每隔 3 秒傳送一次
}
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
Received signal: 2
Received signal: 2
Received signal: 2
alarm()和 pause()函式
alarm()函式
使用 alarm()函式可以設定一個定時器(鬧鐘) ,當定時器定時時間到時,核心會向程序傳送 SIGALRM
訊號 。補充
:SIGALRM 訊號的系統預設操作是終止程序 。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
返回值: 如果在呼叫 alarm()時,之前已經為該程序設定了 alarm 鬧鐘還沒有超時,則該鬧鐘的剩餘值作為本次 alarm()函式呼叫的返回值,之前設定的鬧鐘則被新的替代;否則返回 0。
seconds: 設定定時時間,以秒為單位;如果引數 seconds 等於 0,則表示取消之前設定的 alarm 鬧鐘。
注意: alarm 鬧鐘並不能迴圈觸發,只能觸發一次,若想要實現迴圈觸發,可以在 SIGALRM 訊號處理函式中再次呼叫 alarm()函式設定定時器。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
exit(0);
}
int main(int argc, char *argv[])
{
int second;
struct sigaction sig = {0};
/* 檢驗傳參個數 */
if (2 > argc)
exit(-1);
/* 為 SIGALRM 訊號繫結處理函式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
/* 啟動 alarm 定時器 */
second = atoi(argv[1]);
printf("定時時長: %d 秒\n", second);
alarm(second);
/* 迴圈 */
while(1)
{
sleep(1);
}
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 5
定時時長: 5 秒
Alarm timeout
pause()函式
pause()系統呼叫可以使得程序暫停執行、進入休眠狀態,直到程序捕獲到一個訊號為止,只有執行了信
號處理函式並從其返回時, pause()才返回,在這種情況下, pause()返回-1,並且將 errno 設定為 EINTR。
#include <unistd.h>
int pause(void);
示例:透過 alarm()和 pause()函式模擬 sleep 功能。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
int second;
struct sigaction sig = {0};
/* 檢驗傳參個數 */
if (2 > argc)
exit(-1);
/* 為 SIGALRM 訊號繫結處理函式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
/* 啟動 alarm 定時器 */
second = atoi(argv[1]);
printf("定時時長: %d 秒\n", second);
alarm(second);
/* 進入休眠狀態 */
pause();
puts("休眠結束");
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig 3
定時時長: 3 秒
Alarm timeout
休眠結束
訊號集
一個能表示多個訊號(一組訊號)的資料型別---訊號集(signal set),很多系統呼叫都
使用到了訊號集這種資料型別來作為引數傳遞,譬如 sigaction()函式、 sigprocmask()函式、 sigpending()函式
等。
訊號集其實就是 sigset_t 型別資料結構
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
使用這個結構體可以表示一組訊號,將多個訊號新增到該資料結構中, 當然 Linux 系統封裝了用於操作
sigset_t 訊號集的 API,譬如 sigemptyset()、 sigfillset()、 sigaddset()、 sigdelset()、 sigismember()。
初始化訊號集
sigemptyset():初始化訊號集,使其不包含任何訊號
sigfillset():初始化訊號集,使其包含所有訊號(包括所有實時訊號)。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
返回值: 成功返回 0;失敗將返回-1,並設定 errno 。
set: 指向需要進行初始化的訊號集變數。
示例
//初始化為空訊號集
sigset_t sig_set;
sigemptyset(&sig_set);
//初始化訊號集,使其包含所有訊號:
sigset_t sig_set;
sigfillset(&sig_set);
向訊號集中新增/刪除訊號
sigaddset():向訊號集中新增訊號
sigdelset():向訊號集中移除訊號
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
返回值: 成功返回 0;失敗將返回-1,並設定 errno。
set: 指向訊號集。
signum: 需要新增/刪除的訊號。
示例:
//新增訊號
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
//移除訊號
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);
測試訊號是否在訊號集中
sigismember():可以測試某一個訊號是否在指定的訊號集中。
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
返回值: 如果訊號 signum 在訊號集 set 中,則返回 1;如果不在訊號集 set 中,則返回 0;失敗則返回-
1,並設定 errno。
set: 指定訊號集
signum: 需要進行測試的訊號。
示例:判斷 SIGINT 訊號是否在 sig_set 訊號集中 。
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("訊號集中包含 SIGINT 訊號");
else if (!sigismember(&sig_set, SIGINT))
puts("訊號集中不包含 SIGINT 訊號");
獲取訊號描述資訊
在 Linux 下,每個訊號都有一串與之相對應的字串描述資訊,用於對該訊號進行相應的描述。這些字
符串位於 sys_siglist 陣列中, sys_siglist 陣列是一個 char *型別的陣列,陣列中的每一個元素存放的是一個字
符串指標,指向一個訊號描述資訊。譬如,可以使用 sys_siglist[SIGINT]來獲取對 SIGINT 訊號的描述。
Tips
:使用 sys_siglist 陣列需要包含<signal.h>標頭檔案
補充
:在某些較新的系統或者不同的編譯環境下,sys_siglist
可能不能直接可用。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("SIGINT 描述資訊: %s\n", sys_siglist[SIGINT]);
printf("SIGQUIT 描述資訊: %s\n", sys_siglist[SIGQUIT]);
printf("SIGBUS 描述資訊: %s\n", sys_siglist[SIGBUS]);
exit(0);
}
strsignal()函式 :
用來獲取訊號的描述字串,推薦使用這種方案。
#include <string.h>
char *strsignal(int sig);
返回值:sig訊號描述資訊字串的指標 。函式會對引數 sig 進行檢查,若傳入的 sig 無效,則會返回"Unknown signal"資訊 。
sig:指定訊號
示例:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
printf("SIGINT 描述資訊: %s\n", strsignal(SIGINT));
printf("SIGQUIT 描述資訊: %s\n", strsignal(SIGQUIT));
printf("SIGBUS 描述資訊: %s\n", strsignal(SIGBUS));
printf("編號為 1000 的描述資訊: %s\n", strsignal(1000));
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
SIGINT 描述資訊: Interrupt
SIGQUIT 描述資訊: Quit
SIGBUS 描述資訊: Bus error
編號為 1000 的描述資訊: Unknown signal 1000
psignal()函式
可以在標準錯誤(stderr)上輸出訊號描述資訊
補充
:stderr不存在緩衝區,也就是說stderr的輸出內容會直接列印在螢幕上。
#include <signal.h>
void psignal(int sig, const char *s);
sig:指定訊號
s:呼叫者需要新增的一些輸出資訊
示例:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
psignal(SIGINT, "SIGINT 訊號描述資訊");
psignal(SIGQUIT, "SIGQUIT 訊號描述資訊");
psignal(SIGBUS, "SIGBUS 訊號描述資訊");
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
SIGINT 訊號描述資訊: Interrupt
SIGQUIT 訊號描述資訊: Quit
SIGBUS 訊號描述資訊: Bus error
訊號掩碼
核心為每一個程序維護了一個訊號掩碼(其實就是一個訊號集) ,即一組訊號。當程序接收到一個屬於
訊號掩碼中定義的訊號時,核心會將該訊號進行阻塞、導致無法傳遞給程序進行處理,直到該訊號從訊號掩碼中移除,核心才會把該訊號傳遞給程序從而得到處理 。注
:只是將該訊號阻塞,並不是刪除該訊號了(可以理解為把訊號堵在管道里了,當從訊號掩碼中移除訊號時,該訊號又就能出來了)。
向訊號掩碼中新增一個訊號,通常有如下幾種方式:
- 當應用程式呼叫 signal()或 sigaction()函式為某一個訊號設定處理方式時,程序會自動將該訊號新增
到訊號掩碼中, 這樣保證了在處理一個給定的訊號時,如果此訊號再次發生,那麼它將會被阻塞;
當然對於 sigaction()而言,是否會如此,需要根據 sigaction()函式是否設定了 SA_NODEFER 標誌
而定;當訊號處理函式結束返回後,會自動將該訊號從訊號掩碼中移除。 - 使用 sigaction()函式為訊號設定處理方式時,可以額外指定一組訊號,當呼叫訊號處理函式時將該
組訊號自動新增到訊號掩碼中, 當訊號處理函式結束返回後,再將這組訊號從訊號掩碼中移除; 通
過 sa_mask 引數進行設定。 - 除了以上兩種方式之外,還可以使用 sigprocmask()系統呼叫,隨時可以顯式地向訊號掩碼中新增/
移除訊號。
sigprocmask():向訊號掩碼中新增/移除訊號
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
返回值:返回值: 成功返回 0;失敗將返回-1,並設定 errno 。
引數介紹:
- how: 指定了呼叫函式時的一些行為。
- SIG_BLOCK:將引數 set 所指向的訊號集內的所有訊號新增到程序的訊號掩碼中。換言之,將信
號掩碼設定為當前值與 set 的並集。 - SIG_UNBLOCK:將引數 set 指向的訊號集內的所有訊號從程序訊號掩碼中移除
- SIG_SETMASK:程序訊號掩碼直接設定為引數 set 指向的訊號集
- SIG_BLOCK:將引數 set 所指向的訊號集內的所有訊號新增到程序的訊號掩碼中。換言之,將信
- set:指向的訊號集內的所有訊號新增到訊號掩碼中或者從訊號掩碼中移除;如果引數 set 為
NULL,則表示無需對當前訊號掩碼作出改動。 - oldset: 如果引數 oldset 不為 NULL,在向訊號掩碼中新增新的訊號之前,獲取到程序當前的訊號掩碼,存放在 oldset 所指定的訊號集中;如果為 NULL 則表示不獲取當前的訊號掩碼。
示例:由於將SIGINT訊號加入了訊號掩碼,不會立馬執行訊號處理函式列印”執行訊號處理函式...“,而是睡眠2秒後在”"休眠結束“後列印”執行訊號處理函式...“
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("執行訊號處理函式...\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t sig_set;
/* 註冊訊號處理函式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
{
exit(-1);
}
/* 訊號集初始化 */
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/* 向訊號掩碼中新增訊號 */
if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
{
exit(-1);
}
/* 向自己傳送訊號 */
raise(SIGINT);
/* 休眠 2 秒 */
sleep(2);
printf("休眠結束\n");
/* 從訊號掩碼中移除新增的訊號 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
{
exit(-1);
}
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
休眠結束
執行訊號處理函式...
阻塞等待訊號sigsuspend()
將恢復訊號掩碼和 pause()掛起程序這兩個動作封裝成一個原子操作 。補充
:原子操作就是兩個操作是在一起執行的,不會被打斷。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
返回值: sigsuspend()始終返回-1,並設定 errno 來指示錯誤(通常為 EINTR) ,表示被訊號所中斷,如
果呼叫失敗,將 errno 設定為 EFAULT。
mask: 引數 mask 指向一個訊號集。
示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("執行訊號處理函式...\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t new_mask, old_mask, wait_mask;
/* 訊號集初始化 */
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigemptyset(&wait_mask);
/* 註冊訊號處理函式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
{
exit(-1);
}
/* 向訊號掩碼中新增訊號 */
if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
{
exit(-1);
}
/* 執行保護程式碼段 */
puts("執行保護程式碼段");
/******************/
/* 掛起、等待訊號喚醒 */
if (-1 != sigsuspend(&wait_mask)) //訊號掩碼被替wait_mask替換,wait_mask為空
{
exit(-1);
}
/* 恢復訊號掩碼 */
if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
{
exit(-1);
}
exit(0);
}
在上述程式碼中,我們希望執行受保護程式碼段時不被 SIGINT 中斷訊號打斷,所以在執行保護程式碼段之前
將 SIGINT 訊號新增到程序的訊號掩碼中,執行完受保護的程式碼段之後,呼叫 sigsuspend()掛起程序,等待
被訊號喚醒,被喚醒之後再解除 SIGINT 訊號的阻塞狀態。
實時性訊號
Linux 核心定義了 31 個不同的實時訊號,訊號編號範圍為 34~64,使用 SIGRTMIN 表示編號最小的實
時訊號,使用 SIGRTMAX 表示編號最大的實時訊號,其它訊號編號可使用這兩個宏加上一個整數或減去一
個整數。
sigpending()函式
當接收到的訊號是在訊號掩碼中時,會被阻塞,此時訊號被新增到程序的等待訊號集(等待被處理,處於等待狀態的訊號)中 。
#include <signal.h>
int sigpending(sigset_t *set);
返回值: 成功返回 0;失敗將返回-1,並設定 errno。
set: 處於等待狀態的訊號會存放在引數 set 所指向的訊號集中 。
示例
補充
:sigismember()此函式用於檢查一個指定的訊號是否在給定的訊號集中,也就是檢查該訊號是否被阻塞
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main()
{
/* 定義訊號集 */
sigset_t sig_set;
/* 將訊號集初始化為空 */
sigemptyset(&sig_set);
/* 獲取當前處於等待狀態的訊號 */
sigpending(&sig_set);
/* 判斷 SIGINT 訊號是否處於等待狀態 */
if (1 == sigismember(&sig_set, SIGINT))
puts("SIGINT 訊號處於等待狀態");
else if (!sigismember(&sig_set, SIGINT))
puts("SIGINT 訊號未處於等待狀態");
}
傳送實時訊號
如果同一個訊號在阻塞狀態下產生了多次,那麼會將該訊號記錄在等待訊號集中,並在之後僅傳遞一次(僅當做發生了一次) ,這是標準訊號的缺點之一。
實時訊號較之於標準訊號,其優勢如下 :
- 實時訊號的訊號範圍有所擴大,可應用於應用程式自定義的目的,而標準訊號僅提供了兩個訊號可
用於應用程式自定義使用: SIGUSR1 和 SIGUSR2 。 - 核心對於實時訊號所採取的是佇列化管理。如果將某一實時訊號多次傳送給另一個程序,那麼將會
多次傳遞此訊號。 - 當傳送一個實時訊號時,可為訊號指定伴隨資料(一整形資料或者指標值),供接收訊號的程序在
它的訊號處理函式中獲取。 - 訊號傳遞順序得到保障。訊號的編號越小,其優先順序越高,如果是同一型別的多個訊號在排隊,那麼訊號(以及伴隨資料)的傳遞順序與訊號傳送來時的順序保持一致。
應用程式當中使用實時訊號,需要有以下的兩點要求:
-
傳送程序使用 sigqueue()系統呼叫傳送訊號及伴隨資料
-
sa_handler接收程序使用sigaction函式為訊號建立處理函式,並且使用sa_sigaction,而不是sa_handler。可以使用sa_handler,但是獲取不到伴隨資料了。
sigqueue()
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
返回值: 成功將返回 0;失敗將返回-1,並設定 errno。
引數介紹:
-
pid: 指定接收訊號的程序對應的 pid,將訊號傳送給該程序。
-
sig: 指定需要傳送的訊號。與 kill()函式一樣,也可將引數 sig 設定為 0,用於檢查引數 pid 所指定的進
程是否存在。 -
value: 引數 value 指定了訊號的伴隨資料, union sigval 資料型別。 union sigval 資料型別(共用體) 如下所示:
typedef union sigval { int sival_int; void *sival_ptr; } sigval_t;
示例:
/*傳送程序使用 sigqueue()系統呼叫向另一個程序傳送實時訊號*/ #include <stdio.h> #include <stdlib.h> #include <signal.h> int main(int argc, char *argv[]) { union sigval sig_val; int pid; int sig; /* 判斷傳參個數 */ if (3 > argc) { exit(-1); } /* 獲取使用者傳遞的引數 */ pid = atoi(argv[1]); //接收程序pid sig = atoi(argv[2]); //傳送的訊號 printf("pid: %d\nsignal: %d\n", pid, sig); /* 傳送訊號 */ sig_val.sival_int = 10; //伴隨資料 if (-1 == sigqueue(pid, sig, sig_val)) { perror("sigqueue error"); exit(-1); } puts("訊號傳送成功!"); exit(0); }
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> static void sig_handler(int sig, siginfo_t *info, void *context) { sigval_t sig_val = info->si_value; printf("接收到實時訊號: %d\n", sig); printf("伴隨資料為: %d\n", sig_val.sival_int); } int main(int argc, char *argv[]) { struct sigaction sig = {0}; int num; /* 判斷傳參個數 */ if (2 > argc) exit(-1); /* 獲取使用者傳遞的引數 */ num = atoi(argv[1]); //接收的訊號 /* 為實時訊號繫結處理函式 */ sig.sa_sigaction = sig_handler; sig.sa_flags = SA_SIGINFO; if (-1 == sigaction(num, &sig, NULL)) { perror("sigaction error"); exit(-1); } /* 死迴圈 */ for ( ; ; ) { sleep(1); } exit(0); }
先執行接收程序,並且設定接收的訊號為34。然後檢視接收程式的pid,使用傳送程序向該pid傳送34訊號
ten@ten-virtual-machine:~/H616/demo$ ./testApp 34 接收到實時訊號: 34 伴隨資料為: 10
ten@ten-virtual-machine:~/H616/demo$ ./testApp2 4218 34 pid: 4218 signal: 34 訊號傳送成功!
異常退出 abort()函式
程序異常退出使用 abort()終止程序執行,會生成核心轉儲檔案,可用於判斷程式呼叫 abort()時的程式狀態
當呼叫 abort()函式之後,核心會向程序傳送 SIGABRT 訊號, SIGABRT 訊號的系統預設操作是終止程序執行、並生成核心轉儲檔案;
#include <stdlib.h>
void abort(void);
示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("接收到訊號: %d\n", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGABRT, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
sleep(2);
abort(); // 呼叫 abort
for ( ; ; )
sleep(1);
exit(0);
}
ten@ten-virtual-machine:~/H616/demo$ ./sig
接收到訊號: 6
已中止 (核心已轉儲)
從列印資訊可知,即使在我們的程式當中捕獲了 SIGABRT 訊號,但是程式依然會無情的終止,無論阻
塞或忽略 SIGABRT 訊號, abort()呼叫均不收到影響,總會成功終止程序 。