進程間的通信---UNIX高級環境編程

原文地址:https://www.jianshu.com/p/4989c35c9475

本文主要說明進程間的幾種通信機制,並對比起優缺點與使用場景。

I、IPC方式

進程間通信(IPC,InterProcess Communication)的主要方式包括:管道、FIFO、消息隊列、信號量、共享內存已經socket。

1.1 管道

這一節中所說的管道爲無名管道

1、管道具有以下兩種侷限性:
· 管道爲半雙工的;
· 管道只能在具有公共祖先的兩個進程之間使用,通常,一個管道由一個進程創建,在進程調用fork之後,這個管道就能在父進程和子進程之間使用了。

2、管道通過pipe函數創建:

#include<unistd.h>
int pipe(int fd[2]);      //成功返回0,出錯返回-1

經由參數fd返回兩個文件描述符:fd[0]爲讀而打開,fd[1]爲寫而打開。

3、 下圖展示了父子進程之間的管道通信關係:


父進程的fd[1]端的輸出通過管道可以連接到子進程的fd[0]端,從而實現通信。

fork之後做什麼取決於我們所需要的數據流方向。
例如對於父進程到子進程管道,父進程關閉管道的fd[0]端,子進程關閉fd[1]端,就形成了一條單工的通信方式,只能由父進程發送至子進程:

再如經典的web服務器tinyhttpd所形成的通信結構如下圖所示:


子進程負責執行cgi程序,將自己的管道端重定向(使用dup2)到標準輸入輸出,然後完成與父進程的交互。
關於tinyhttpd的源碼解析見:史上最詳細的tinyhttpd源碼註釋

1.2、FIFO

FIFO即爲命名管道,與無名管道不同的是,其可以在不相關的程序之間交換數據。

1、FIFO其實是一種文件類型,創建FIFO類似於創建文件:

#include<sys/stat.h>
int mkfifo(const char *path, mode_t mode);

int mkfifoat(int fd, const char *path, mode_t mode); //成功返回0,失敗返回-1

mkfifoat函數和mkfifo函數相似,但是mkfifoat函數可以被用來在fd文件描述符表示的目錄相關的位置創建一個FIFO。

當我們用mkfifo或者mkfifoat創建FIFO時,都需要用open來打開它。

2、FIFO有兩種用途

· shell命令使用FIFO將數據從一條管道傳送到另一條管道時,無須創建中間的臨時文件。
· 客戶進程-服務器進程應用程序中,FIFO用作匯聚點,在客戶進程和服務器進程二者之間傳遞數據。

3、FIFO應用實例

實例1 用FIFO複製輸出流:
FIFO可以用於複製一系列shell命令中的輸出流。這就防止了將數據寫向中間磁盤文件。這類似於使用管道(|)來避免中間磁盤文件。

考慮這樣一個過程,它需要對一個經過過濾的輸入流進行兩次處理。下圖展示了這種安排:

使用FIFO和UNIX程序tee就可以實現這樣的過程而無需使用臨時文件(tee程序將其標準輸入同時複製到標準輸出以及其命令行中命名的文件):

mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2

創建FIFO,在後臺啓動prog3,從FIFO中讀取數據。然後啓動prog1,用tee將其輸出發送到FIFO和prog2:

實例2 使用FIFO進行客戶進程-服務器進程通信:
如果有一個服務器進程,它與很多客戶進程有關,每個客戶進程都可以將其請求寫到一個該服務器進程創建的“衆所周知”的FIFO中(所有客戶進程都知道該FIFO的路徑名)。下圖展示了這種安排:

這類問題在於服務器進程如何將回答送回各個客戶進程。不能使用單個FIFO,因爲客戶進程不知道該何時去讀屬於它們自己的相應。

一種解決辦法就是:每個客戶進程都在其請求中包含自身的進程ID。然後服務器進程爲每個客戶進程創建一個FIFO,用於迴應:

1.3 消息隊列

1、消息隊列是消息的鏈接表,存儲在內核中,有消息隊列標識符標識。

msgget用於創建一個新的消息隊列或打開一個現有隊列。msgsnd用於將新消息添加到隊列尾端。msgrcv用於從隊列中去消息。:

#include<sys/msg.h>
int msgget(key_t key, int flag);  

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);

ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

我們並不一定以先進先出的順序取消息,可以按照消息的類型字段取消息。

1.4 信號量

1、信號量與管道,FIFO以及消息隊列不同。它是一個計數器,用於爲多個進程提供共享數據對象的訪問。

2、 爲了獲得共享資源,進程需要執行下列操作:
1) 測試控制該資源的信號量;
2) 若此信號量的值爲正,則進程可以使用該資源。在這種情況下,進程會將信號量值減1,表示它使用了一個資源單位;
3) 否則,若此信號量的值爲0,則進程進入休眠狀態,知道信號量的值大於0。進程被喚醒後,返回步驟1)。

當進程不在使用由一個信號量控制的共享資源時,該信號量值增1。如果有進程正在休眠等待此信號量,則喚醒它們。

3、 當我們想使用信號量時,首先通過函數semget來獲取一個信號量ID:

#include<sys/sem.h>
int semget(key_t key, int nsems, int flag);

semctl函數包含了多種信號量操作:

#include<sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg*/);  

semop函數自動執行信號量集合上的操作數組:

#include<sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
1.5 共享存儲

1、共享存儲允許兩個或多個進程共享一個給定的存儲區。

因爲數據不需要在客戶進程和服務器進程之間複製,所以這是最快的IPC。在多個進程同步訪問一個給定存儲區時,若服務器進程正在將數據放入存儲區,則在它做完之前,客戶進程不應該去取這些數據。

信號量用於同步共享存儲訪問。

2、shmget函數獲得一個共享存儲標識符:

#include<sys/shm.h>
int shmget(key_t key, size_t size, int flag);

shmctl函數對共享存儲段執行多種操作:

#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

一旦創建了一個共享存儲段,進程就可以調用shmat函數將其連接到它的地址空間中:

#include<sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);

當對共享存儲段的操作已經結束時,調用shmdt函數與該段分離,但這並不會刪除標識符及其相關的數據結構。直至某個進程(一般是服務器進程)調用shmctl特地刪除它爲止:

#include<sys/shm.h>
int shmdt(const void *addr);

1.6 socket

關於socket通信以後會專門寫一篇文章說明。

II、幾種IPC方式優缺點比較

1、如果用戶傳遞的信息較少,或者只是爲了出發某些行爲。信號是一種簡潔有效的通信方式。但若是進程間要求傳遞的信息量較大或者存在數據交換的要求,就需要考慮別的通信方式了。

2、無名管道與有名管道的區別在於單向通信以及有關聯的進程

3、消息隊列允許任意進程通過共享隊列來進行進程間通信。並由系統調用函數來實現消息發送和接收之間的同步。從而使得用戶在使用消息緩衝進行通信時不再需要考慮同步問題,使用相對方便。
但是消息隊列中信息的複製需要耗費CPU時間,不適宜信息量大或頻繁操作的場合。

4、消息隊列與管道方式的區別在於,消息隊列可以實現多對多,並需要在內存中實現,而管道可以在內存或磁盤上實現。

5、共享內存無須複製,信息量大是其最大的優勢。但是需要考慮同步問題

【參考】
[1] 《UNIX環境高級編程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章