進程間通信
進程間通信概念
進程間通信(IPC,Interprocess communication)是一組編程接口,讓程序員能夠協調不同的進程,使之能在一個操作系統裏同時運行,並相互傳遞、交換信息。這使得一個程序能夠在同一時間裏處理許多用戶的要求。因爲即使只有一個用戶發出要求,也可能導致一個操作系統中多個進程的運行,進程之間必須互相通話。IPC接口就提供了這種可能性。每個IPC方法均有它自己的優點和侷限性,一般,對於單個程序而言使用所有的IPC方法是不常見的。
進程間通信目的
- 數據傳輸:一個進程需要將它的數據發送給另一個進程,發送的數據量在一個字節到幾兆字節之間。
- 共享數據:多個進程想要操作共享數據,一個進程對共享數據的修改,別的進程應該立刻看到。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 資源共享:多個進程之間共享同樣的資源。爲了作到這一點,需要內核提供鎖和同步機制。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。
進程間通信方式
管道pipe
:管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係;命名管道FIFO
:命名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信;消息隊列MessageQueue
:消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點;共享內存SharedMemory
:共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信;信號量Semaphore
:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段;套接字Socket
:套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信;- 信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
管道
什麼是管道?
- 管道是Unix中最古老的進程間通信的形式
- 我們把從一個進程連接到另一個進程的一個數據流稱爲一個“管道”
- 管道具有以下幾個特點:
(1)管道是半雙工的,數據只能想一個方向流動;需要雙方通信時,需要建立起兩個管道;
(2)匿名管道只能用於父子進程活兄弟進程之間(具有親緣關係的進程);
(3)單獨構成一個獨立的文件系統:管道杜宇管道兩端的進程而言,就是一個文件,但它不是普通文件,部不屬於某種文件系統,而是自立門戶,單獨構成一個文件系統,並且只存在與內存中;
(4)管道分爲pipe(匿名管道)和FIFO(命名管道)兩種,除了建立,打開,刪除的方式不同外,這兩種管道幾乎時一樣的,他們都是通過內核緩衝區實現數據傳輸。 - 匿名管道:
用於相關進程間的通信,例如父進程和子進程,它通過pipe()系統調用來創建打開,當最後一個使用它的進程關閉對它的引用時,pipe將自動撤銷。
原型:
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
pipe的例子:父進程創建管道,並在管道中寫入數據,而子進程從管道中讀出數據;
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2]; // 兩個文件描述符
pid_t pid;
char buff[20];
if(pipe(fd) < 0) // 創建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 創建子進程
printf("Fork Error!\n");
else if(pid > 0) // 父進程
{
close(fd[0]); // 關閉讀端
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]); // 關閉寫端
read(fd[0], buff, 20);
printf("%s", buff);
}
return 0;
}
- 命名管道:
和匿名管道的主要區別
在於,命名管道有一個名字,命名管道的名字對應於一個磁盤引結點,任何進程有相應的權限都可以對它進行訪問;而匿名管道卻不同,進程只能訪問自己或祖先創建的管道,而不能任意訪問已經存在的管道,因爲沒有名字。
Linux中通過系統調用mknod或makefifo()來創建一個命名管道,最簡單的方式就是直接用shell。- mkfifo myfifo 等價於 mknod myfifo p`;這是一個在當前目錄下創建了一個名我myfifo的命名管道,用ls -p命名查看文件的類型時,可以看到命名管道對應的文件名後又一條豎線 ‘|’,表示該文件不是普通文件而是命名管道。
- 使用open()函數可以打開已經創建的命名管道,而匿名管道則不能用open()函數打開;當一個命名管道不在被任何進程打開時,它沒有消失,還可以再次被打開,就像打開一個磁盤文件一樣。
- 可以用刪除普通文件的方法將其刪除,實際刪除的是磁盤上對應的結點信息。
原型:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,出錯返回-1
例子:用命名管道實現server&client通信
serverPipe.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
if (mkfifo("mypipe", 0644) < 0){
ERR_EXIT("mkfifo");
}
int rfd = open("mypipe", O_RDONLY);
if (rfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while (1){
buf[0] = 0;
printf("Please wait...\n");
ssize_t s = read(rfd, buf, sizeof(buf)-1);
if (s > 0){
buf[s - 1] = 0;
printf("client say# %s\n", buf);
}
else if (s == 0){
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else{
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
clientPipe.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd = open("mypipe", O_WRONLY);
if (wfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while (1){
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0){
buf[s] = 0;
write(wfd, buf, strlen(buf));
}
else if (s <= 0){
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}
Makefile:
消息隊列
消息隊列是消息的鏈表,存放在內核中;一個消息隊列由一個標識符(即隊列ID)來標識;
- 特點:
(1)消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級;
(2)消息隊列獨立於發送與接收數據;進程終止時,消息隊列及其內容並不會被刪除;
(3)消息隊列可以實現消息的隨機查詢,消息不一定要已先進先出的次序讀取,也可以按消息的類型讀取; - 原型:
#include <sys/msg.h>
// 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 讀取消息:成功返回消息數據的長度,失敗返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息隊列:成功返回0,失敗返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
在以下兩種情況下,msgget將創建一個新的消息隊列:
- 如果沒有與鍵值key相對應的消息隊列,並且flag中包含了IPC_CREAT標誌位。
- key參數爲IPC_PRIVATE。
函數msgrcv在讀取消息隊列時,type參數有下面幾種情況:
- type == 0,返回隊列中的第一個消息;
- type > 0,返回隊列中消息類型爲 type 的第一個消息;
- type < 0,返回隊列中消息類型值小於或等於 type 絕對值的消息,如果有多個,則取類型值最小的消息。
可以看出,type值非 0 時用於以非先進先出次序讀消息。也可以把 type 看做優先級的權值。
例子:下面是一個簡單的使用消息隊列,服務端一直在等待特定類型的消息,當收到該類型的消息以後,發送另一個特定類型的消息作爲反饋,客戶端讀取該反饋並打印出來。
msg_serve.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/msg.h>
4
5 // 用於創建一個唯一的key
6 #define MSG_FILE "/etc/passwd"
7
8 // 消息結構
9 struct msg_form {
10 long mtype;
11 char mtext[256];
12 };
13
14 int main()
15 {
16 int msqid;
17 key_t key;
18 struct msg_form msg;
19
20 // 獲取key值
21 if((key = ftok(MSG_FILE,'z')) < 0)
22 {
23 perror("ftok error");
24 exit(1);
25 }
26
27 // 打印key值
28 printf("Message Queue - Server key is: %d.\n", key);
29
30 // 創建消息隊列
31 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
32 {
33 perror("msgget error");
34 exit(1);
35 }
36
37 // 打印消息隊列ID及進程ID
38 printf("My msqid is: %d.\n", msqid);
39 printf("My pid is: %d.\n", getpid());
40
41 // 循環讀取消息
42 for(;;)
43 {
44 msgrcv(msqid, &msg, 256, 888, 0);// 返回類型爲888的第一個消息
45 printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
46 printf("Server: receive msg.mtype is: %d.\n", msg.mtype);
47
48 msg.mtype = 999; // 客戶端接收的消息類型
49 sprintf(msg.mtext, "hello, I'm server %d", getpid());
50 msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
51 }
52 return 0;
53 }
msg_client.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/msg.h>
4
5 // 用於創建一個唯一的key
6 #define MSG_FILE "/etc/passwd"
7
8 // 消息結構
9 struct msg_form {
10 long mtype;
11 char mtext[256];
12 };
13
14 int main()
15 {
16 int msqid;
17 key_t key;
18 struct msg_form msg;
19
20 // 獲取key值
21 if ((key = ftok(MSG_FILE, 'z')) < 0)
22 {
23 perror("ftok error");
24 exit(1);
25 }
26
27 // 打印key值
28 printf("Message Queue - Client key is: %d.\n", key);
29
30 // 打開消息隊列
31 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
32 {
33 perror("msgget error");
34 exit(1);
35 }
36
37 // 打印消息隊列ID及進程ID
38 printf("My msqid is: %d.\n", msqid);
39 printf("My pid is: %d.\n", getpid());
40
41 // 添加消息,類型爲888
42 msg.mtype = 888;
43 sprintf(msg.mtext, "hello, I'm client %d", getpid());
44 msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
45
46 // 讀取類型爲777的消息
47 msgrcv(msqid, &msg, 256, 999, 0);
48 printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
49 printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
50 return 0;
51 }
共享內存
共享內存是進程間通信中最高效的一種方式,因爲它不涉及進程之間的任何數據傳輸,但同樣,這種高效仍然帶來了問題,我們必須用其他手段來同步進程對共享內存的訪問(因爲共享內存是不帶任何同步機制的),否則就會產生競態條件。因此,共享內存通常是和其他進程間通信方式一起使用的。
-
共享內存的特點
1、使用靈活,可以是無關聯的進程;
2、效率高:程序直接訪問內存,而不需要任何的書庫拷貝。對於像管道和消息隊列
等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,採用共享內存的通信方式效率是非常高的。 -
創建共享內存
int shmget(key_t key, size_t size, int shmflg);
功能:創建一段新的共享內存,或者獲取一段已經存在的共享內存。
返回值:成功時翻譯一個正整數,即共享內存的標識符。失敗時返回-1並設置errno。
參數:
- key:是一個鍵值,用來標識一段全局唯一的共享內存。
- size:指定共享內存的大小,單位是字節。如果是新創建的共享內存,則size值必須被指定。如果是獲取已經存在的共享內存,則可以把size設置爲0.
- shmflg:該參數與semget系統調用的sem_flags參數相同。
- 掛接 & 去掛接
共享內存獲取之後我們不能立即訪問,而是先將它關聯到進程的地址空間;使用完成後也需要將其從進程的地址空間上分離。
void *shmat(int shmid, const void *shmaddr, int shmflg); //掛接/關聯
int shmdt(const void *shmaddr); //去掛接/去關聯
-
shmid參數是由shmget調用返回的共享內存標識符。
-
shmaddr參數指定將共享內存關聯到進程地址空間的哪塊,最終效果還受到shmflg參數中可選標誌的SHM_RND的影響。
shmaddr爲NULL:被關聯的地址由操作系統指定。(推薦這種做法,以確保代碼的可移植性);
shmaddr爲非NULL,且SHM_RND標誌未被設置:共享內存被關聯到指定的地址處。 -
shmat的返回值:成功時返回共享內存被關聯的地址,失敗返回-1並設置errno。
shmdt函數是將共享內存從進程地址空間中分離。成功時返回0,失敗時返回-1並設置errno。 -
控制共享內存的屬性
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- cmd參數指定要執行的命令。
shmctl所支持的cmd選項:
IPC_STAT 取該共享內存的shmid_ds結構,並存在第三個參數中
IPC_SET 使用buf指定的結構設置相關屬性
IPC_RMID 刪除指定共享內存,只有當buf中的shm_nattch值爲0時才真正刪除
IPC_LOCK 在內存中對共享內存加鎖(超級用戶權限)
IPC_UNLOCK 解鎖共享內存(超級用戶權限)
返回值:成功時的返回值取決與cmd參數。失敗範湖-1並設置errno。
信號量
- 什麼是信號量
信號量的主要作用是爲了保護臨街資源,保證了在任意一個時刻內只有一個進程能進入臨界區對資源進行操作。也就是說,信號量是用來協調進程對共享資源的訪問的。 - 工作原理
信號量的本質是一種數據操作鎖。其只能進行兩種操作:等待和發送信號,即P、V操作。假設有信號量sv,
P:如果信號量的值大於0,就將其減1;如果sv爲0,就將進程掛起;—–釋放資源
V:如果有其他進程因爲等待該信號量而掛起,則該操作是將其喚醒;如果沒有,就將sv加1;—–申請資源
舉個例子:假如有進程A、B共享信號量sv,當A進程執行了P操作後,就可以獲得信號量sv並進入臨界區,並將sv的值減1;當B進程來訪問臨界區時,其試圖進行P操作,但是此時sv爲0,則B進程就會被掛起等待直到A進程離開臨界區並執行了V操作後,B進程被喚醒,然後就可以回覆執行了。
-
特點
1、保護臨界資源、協同進程;
2、有公共、私有接口;
3、生命週期隨系統;
4、創建、初始化不是原子操作,創建與初始化分開。 -
系統調用
主要有三個:semget,semop,semctl,都被設計爲操作一組信號量,即信號量集。
【創建一個新的信號量集】
int semget(key_t key, int nsems, int semflg);
返回值:成功返回一個正整數,是信號量集的標識符;失敗返回-1,並設置errno。
參數:
- key:標識一個全局唯一的信號量集。要通過信號量進行通信的進程需要使用相同的鍵值來創建/獲取信號量集。
- nsems:指定要創建/獲取的信號量集中信號量的數目。如果是創建,則改值必須被指定;若是獲取,則可將其設置爲0.
- semflg:指定一組標誌。低9位是信號量的權限。可以與IPC_CREATE按位“或”以創建新的信號量集。即使這個信號量集已經存在也不會產生錯誤。創建一組新的、唯一的信號量集—IPC_CREATE 和 IPC_EXCL,如果信號量集已經存在,則semget返回錯誤並設置errno爲EEXIST。
【對信號量集進行操作】
int semop(int semid, struct sembuf *sops, unsigned nsops);
返回值:成功返回0,失敗返回-1,且sem_ops數組中指定的所有操作都不被執行。
參數:
- semid:semget返回的信號量集標識符,用以指定被操作的目標信號量集。
- sops:指向一個sembuf結構體。
- sembuf結構體:unsigned short sem_num; 信號量集中信號量的編號
- short sem_op; 指定操作類型(正數、0、負數)
- short sem_flg; IPC_NOWAIT/IPC_UNDO
- nsops:指定要執行的操作個數,即sops中元素的個數。semop對數組中的每個成員依次執行操作,且該過程是原子的。
【對信號量集進行控制】
int semctl(int semid, int semnum, int cmd, ...);
返回值:成功時的參數取決於cmd參數;失敗返回-1且設置errno
- semid:由semget調用返回的信號量集標識符。
- semnum:指定被操作的信號量在信號量集中的編號。(從0開始訪問)
- cmd:指定要執行的指令。(IPC_RMID:立即移除信號量集,喚醒所有等待該信號量集的所有進程)