進程間通信(IPC,InterProcess Communication)是指在不同進程之間傳播或交換信息。IPC的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
一、管道
管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。
1、特點:
它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
它只能用於具有親緣關係的進程之間的通信(也是父子進程或者兄弟進程之間)。
它可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存中。
2、原型:
1 | #include <unistd.h> |
當一個管道建立時,它會創建兩個文件描述符:fd[0]
爲讀而打開,fd[1]
爲寫而打開。如下圖:
要關閉管道只需將這兩個文件描述符關閉即可。
3、例子
單個進程中的管道幾乎沒有任何用處。所以,通常調用 pipe 的進程接着調用 fork,這樣就創建了父進程與子進程之間的 IPC 通道。如下圖所示:
若要數據流從父進程流向子進程,則關閉父進程的讀端(fd[0]
)與子進程的寫端(fd[1]
);反之,則可以使數據流從子進程流向父進程。
1 | #include<stdio.h> |
二、FIFO
FIFO,也稱爲命名管道,它是一種文件類型。
1、特點
FIFO可以在無關的進程之間交換數據,與無名管道不同。
FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中。
2、原型
1 | #include <sys/stat.h> |
其中的 mode 參數與open
函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。
當 open 一個FIFO時,是否設置非阻塞標誌(O_NONBLOCK
)的區別:
若沒有指定
O_NONBLOCK
(默認),只讀 open 要阻塞到某個其他進程爲寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程爲讀而打開它。若指定了
O_NONBLOCK
,則只讀 open 立即返回。而只寫 open 將出錯返回 -1 如果沒有進程已經爲讀而打開該 FIFO,其errno置ENXIO。
3、例子
FIFO的通信方式類似於在進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性。在數據讀出時,FIFO管道中同時清除數據,並且“先進先出”。下面的例子演示了使用 FIFO 進行 IPC 的過程:
write_fifo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38#include<stdio.h>
#include<stdlib.h> // exit
#include<fcntl.h> // O_WRONLY
#include<sys/stat.h>
#include<time.h> // time
int main()
{
int fd;
int n, i;
char buf[1024];
time_t tp;
printf("I am %d process.\n", getpid()); // 說明進程ID
if((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個FIFO
{
perror("Open FIFO Failed");
exit(1);
}
for(i=0; i<10; ++i)
{
time(&tp); // 取系統當前時間
n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
printf("Send message: %s", buf); // 打印
if(write(fd, buf, n+1) < 0) // 寫入到FIFO中
{
perror("Write FIFO Failed");
close(fd);
exit(1);
}
sleep(1); // 休眠1秒
}
close(fd); // 關閉FIFO文件
return 0;
}read_fifo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
int fd;
int len;
char buf[1024];
if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 創建FIFO管道
perror("Create FIFO Failed");
if((fd = open("fifo1", O_RDONLY)) < 0) // 以讀打開FIFO
{
perror("Open FIFO Failed");
exit(1);
}
while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道
printf("Read message: %s", buf);
close(fd); // 關閉FIFO文件
return 0;
}
在兩個終端裏用 gcc 分別編譯運行上面兩個文件,可以看到輸出結果如下:
1 | [songlee@localhost]$ ./write_fifo |
1 | [songlee@localhost]$ ./read_fifo |
上述例子可以擴展成 客戶進程—服務器進程 通信的實例,write_fifo
的作用類似於客戶端,可以打開多個客戶端向一個服務器發送請求信息,read_fifo
類似於服務器,它適時監控着FIFO的讀端,當有數據時,讀出並進行處理,但是有一個關鍵的問題是,每一個客戶端必須預先知道服務器提供的FIFO接口,下圖顯示了這種安排:
三、消息隊列
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
1、特點
消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
2、原型
1 | #include <sys/msg.h> |
在以下兩種情況下,msgget
將創建一個新的消息隊列:
- 如果沒有與鍵值key相對應的消息隊列,並且flag中包含了
IPC_CREAT
標誌位。 - key參數爲
IPC_PRIVATE
。
函數msgrcv
在讀取消息隊列時,type參數有下面幾種情況:
type == 0
,返回隊列中的第一個消息;type > 0
,返回隊列中消息類型爲 type 的第一個消息;type < 0
,返回隊列中消息類型值小於或等於 type 絕對值的消息,如果有多個,則取類型值最小的消息。
可以看出,type值非 0 時用於以非先進先出次序讀消息。也可以把 type 看做優先級的權值。(其他的參數解釋,請自行Google之)
3、例子
下面寫了一個簡單的使用消息隊列進行IPC的例子,服務端程序一直在等待特定類型的消息,當收到該類型的消息以後,發送另一種特定類型的消息作爲反饋,客戶端讀取該反饋並打印出來。
msg_server.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
// 用於創建一個唯一的key
#define MSG_FILE "/etc/passwd"
// 消息結構
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 獲取key值
if((key = ftok(MSG_FILE,'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Server key is: %d.\n", key);
// 創建消息隊列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息隊列ID及進程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 循環讀取消息
for(;;)
{
msgrcv(msqid, &msg, 256, 888, 0);// 返回類型爲888的第一個消息
printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
printf("Server: receive msg.mtype is: %d.\n", msg.mtype);
msg.mtype = 999; // 客戶端接收的消息類型
sprintf(msg.mtext, "hello, I'm server %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
return 0;
}msg_client.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
// 用於創建一個唯一的key
#define MSG_FILE "/etc/passwd"
// 消息結構
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 獲取key值
if ((key = ftok(MSG_FILE, 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Client key is: %d.\n", key);
// 打開消息隊列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息隊列ID及進程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 添加消息,類型爲888
msg.mtype = 888;
sprintf(msg.mtext, "hello, I'm client %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
// 讀取類型爲777的消息
msgrcv(msqid, &msg, 256, 999, 0);
printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
return 0;
}
四、信號量
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
1、特點
信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
支持信號量組。
2、原型
最簡單的信號量是隻能取 0 和 1 的變量,這也是信號量最常見的一種形式,叫做二值信號量(Binary Semaphore)。而可以取多個正整數的信號量被稱爲通用信號量。
Linux 下的信號量函數都是在通用的信號量數組上進行操作,而不是在一個單一的二值信號量上進行操作。
1 | #include <sys/sem.h> |
當semget
創建新的信號量集合時,必須指定集合中信號量的個數(即num_sems
),通常爲1; 如果是引用一個現有的集合,則將num_sems
指定爲 0 。
在semop
函數中,sembuf
結構的定義如下:
1 | struct sembuf |
其中 sem_op 是一次操作中的信號量的改變量:
若
sem_op > 0
,表示進程釋放相應的資源數,將 sem_op 的值加到信號量的值上。如果有進程正在休眠等待此信號量,則換行它們。若
sem_op < 0
,請求 sem_op 的絕對值的資源。- 如果相應的資源數可以滿足請求,則將該信號量的值減去sem_op的絕對值,函數成功返回。
- 當相應的資源數不能滿足請求時,這個操作與
sem_flg
有關。- sem_flg 指定
IPC_NOWAIT
,則semop函數出錯返回EAGAIN
。 - sem_flg 沒有指定
IPC_NOWAIT
,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:- 當相應的資源數可以滿足請求,此信號量的semncnt值減1,該信號量的值減去sem_op的絕對值。成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,並從信號處理函數返回,此情況下將此信號量的semncnt值減1,函數semop出錯返回EINTR
- sem_flg 指定
若
sem_op == 0
,進程阻塞直到信號量的相應值爲0:- 當信號量已經爲0,函數立即返回。
- 如果信號量的值不爲0,則依據
sem_flg
決定函數動作:- sem_flg指定
IPC_NOWAIT
,則出錯返回EAGAIN
。 - sem_flg沒有指定
IPC_NOWAIT
,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:- 信號量值爲0,將信號量的semzcnt的值減1,函數semop成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,並從信號處理函數返回,在此情況將此信號量的semncnt值減1,函數semop出錯返回EINTR
- sem_flg指定
在semctl
函數中的命令有多種,這裏就說兩個常用的:
SETVAL
:用於初始化信號量爲一個已知的值。所需要的值作爲聯合semun的val成員來傳遞。在信號量第一次使用之前需要設置信號量。IPC_RMID
:刪除一個信號量集合。如果不刪除信號量,它將繼續在系統中存在,即使程序已經退出,它可能在你下次運行此程序時引發問題,而且信號量是一種有限的資源。
3、例子
1 | #include<stdio.h> |
上面的例子如果不加信號量,則父進程會先執行完畢。這裏加了信號量讓父進程等待子進程執行完以後再執行。
五、共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。
1、特點
共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
因爲多個進程可以同時操作,所以需要進行同步。
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
2、原型
1 | #include <sys/shm.h> |
當用shmget
函數創建一段共享內存時,必須指定其 size;而如果引用一個已存在的共享內存,則將 size 指定爲0 。
當一段共享內存被創建以後,它並不能被任何進程訪問。必須使用shmat
函數連接該共享內存到當前進程的地址空間,連接成功後把共享內存區對象映射到調用進程的地址空間,隨後可像本地空間一樣訪問。
shmdt
函數是用來斷開shmat
建立的連接的。注意,這並不是從系統中刪除該共享內存,只是當前進程不能再訪問該共享內存而已。
shmctl
函數可以對共享內存執行多種操作,根據參數 cmd 執行相應的操作。常用的是IPC_RMID
(從系統中刪除該共享內存)。
3、例子
下面這個例子,使用了【共享內存+信號量+消息隊列】的組合來實現服務器進程與客戶進程間的通信。
- 共享內存用來傳遞數據;
- 信號量用來同步;
- 消息隊列用來 在客戶端修改了共享內存後 通知服務器讀取。
Server.c
1 | #include<stdio.h> |
Client.c
1 | #include<stdio.h> |
注意:當scanf()
輸入字符或字符串時,緩衝區中遺留下了\n
,所以每次輸入操作後都需要清空標準輸入的緩衝區。但是由於 gcc 編譯器不支持fflush(stdin)
(它只是標準C的擴展),所以我們使用了替代方案:
1 | while((c=getchar())!='\n' && c!=EOF); |
註釋已經很詳細了,所以代碼的其他部分我就不解釋了,下面是運行結果截圖: