title: 進程間通信 cover: https://img2.imgtp.com/2024/04/30/9GnvXoDg.png tags: - 通信 - linux categories: linux系統編程 引言 進程間通信(interprocess communication,簡稱 IPC ...
引言
進程間通信(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()調用均不收到影響,總會成功終止進程 。