處理序間通訊方式有多種,包括管道、FIFO、訊息佇列、共用記憶體、訊號量等。
1. 半雙工管道
該方式只能在具有公用祖先的進程之間使用。通常,一個管道由一個進程建立,然後進程通過fork函數建立一個子進程,因此父、子進程之間就可以應用該管道作為父子之間的通訊方式。
函數pipe()建立管道:
#include <unistd.h>int pipe(int fileds[2]);
其中,參數fileds返回兩個檔案描述符:filefds[0]為讀而開啟,fileds[1]為寫而開啟,且fileds[1]的輸出作為fileds[0]的輸入。:
圖1 單進程中半雙工管道
當管道的一端被關閉後,下列兩條規則其作用(man 7 pipe):
- 當讀一個寫端已被關閉的管道時,在所有資料都被讀取後,read返回0,以指示達到了檔案結束處;
- 當寫一個讀端被關閉的管道時,則產生訊號SIGPIPE。如果忽略該訊號或捕捉該訊號並其處理常式返回,則write返回-1,errno被設定為EPIPE;
在寫管道(或FIFO)時,常量PIPE_BUF規定了核心中管道緩衝區的大小。如果對管道的資料量小於等於PIPE_BUF,則不會與其他進程對同一管道(或FIFO)的write操作穿插進行。若多個進程同時寫一個管道(或FIFO)且資料大於PIPE_BUF時,則可能出現穿插現象。
2. FIFO
FIFO是一種檔案類型。stat結構成員st_mode的編碼指明檔案是否為FIFO類型,可以使用S_ISFIFO宏對其進行測試。
函數mkfifo來建立FIFO:
#include <sys/types.h>#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
其中,pathname作為FIFO檔案的檔案名稱,mode指定FIFO檔案的許可權。
3. IPC基本知識
IPC包括訊息佇列、訊號量及共用記憶體儲存區。在核心中,每個IPC結構都用一個非負整數的標識符加以標識或引用。標識符作為內部名存在,而索引值作為外部名使得多個進程能夠在一個ipc對象上操作。因此,無論何時建立IPC結構(調用msgget、semget或shmget),都應指定一個資料類型為key_t的鍵。
多種方法使客戶進程與伺服器處理序在同一結構上會和:
(1)伺服器處理序可以指定鍵IPC_PRIVATE來建立一個新的IPC結構,將返回的標識符存在某處(如檔案)以便客戶進程使用。鍵IPC_PRIVATE保證伺服器處理序建立一個新的IPC結構。
這種方法的缺點:伺服器處理序要將整數標識符寫到檔案中,此後客戶進程又要讀取檔案取得此標識符。
(2)在一個公用的標頭檔中定義一個客戶進程和伺服器處理序都認可的鍵,然後伺服器處理序指定此鍵建立一個新的IPC結構。
這種方法的問題:若此鍵已與一個IPC結構相結合,則get函數(msgget、semget或shmget)出錯返回。因此伺服器需要刪除已存在的IPC結構,然後試著再建立該鍵。
(3)客戶進程和伺服器處理序認同一個路徑名和項目ID(項目ID為0~255之間的字元值),接著調用ftok函數將這兩個值變換為一個鍵,然後在方法(2)中使用此鍵。
ftok函數提供的唯一服務:由一個路徑名和項目ID產生一個鍵。
#include <sys/ipc.h>key_t ftok(const char *path, int id);
其中,path參數必須引用一個現存檔案。當產生鍵時,只使用id參數的低8位。
若滿足下列兩個條件之一,則建立一個新的IPC結構:
- key是IPC_PRIVATE;
- key當前未與特定類型的IPC結構相結合,並且flag中指定了IPC_CREAT位;
每一個IPC結構都將設定一個ipc_perm結構,該結構規定了許可權和所有者資訊。
在使用IPC時,需要注意的問題:
- IPC結構是在系統範圍內起作用的,沒有訪問計數;
- IPC結構在檔案系統中沒有名字。
4. 訊息佇列
訊息佇列實際上就是訊息的串連表,存放在核心中並由訊息佇列標識符來標識。
函數msgget():建立一個訊息佇列或者開啟一個現存的隊列。
#include <sys/msg.h>int msgget(key_t key, int flag); // 成功返回訊息佇列ID;若出錯則返回-1
函數msgsnd():將新訊息插入到隊列尾部。每個訊息包含一個正長整型的類型欄位,一個非負長度以及實際資料位元組。成功添加後,將更新msqid_ds結構中資訊。
#include <sys/msg.h>int msgsnd(int msqid, void *ptr, size_t nbytes, int flag); // 若成功返回0;若出錯返回-1
其中,ptr指向一個包含類型欄位和資料欄位的結構體,例如:
struct mymsg { long mtype; // positive message type char mtext[512]; // message data. of length nbytes};
函數msgrcv():從指定隊列中提取訊息。
#include <sys/msg.h>ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag); // 若成功則返回訊息資料部分的長度;若出錯則返回-1.
其中,在提取訊息時,根據type值的不同提取不同的訊息:
- type == 0:返回訊息佇列中第一個訊息;
- type > 0:返回隊列中訊息類型為type的第一個訊息;
- type < 0:返回隊列中訊息類型小於等於type絕對值的訊息。若這種訊息存在多個,則去類型值最小的訊息。
函數msgctl():用於對訊息佇列執行多種操作。
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 若成功則返回0;若出錯則返回-1
其中,該函數中參數cmd指定了要對msqid隊列執行的命令:
- IPC_STAT:取msqid標識的訊息佇列的msqid_ds結構,並將其放入到buf所指的記憶體中;
- IPC_SET:將msqid標識的訊息佇列的msqid_ds結構設定為由buf指向的新資料;
- IPC_RMID:從系統中移除該訊息佇列以及存在該隊列中的所有資料。
5. 訊號量
訊號量(核心中使用結構體semid_ds)是一個計數器,用於多進程對共用資料對象的存取控制。為了擷取共用資源,進程需要執行以下操作:
- 測試控制資源的訊號量;
- 若此訊號量的值為正,則進程可以使用該資源。進程將訊號量減1,表示它使用了一個資源單位;
- 若此訊號量的值為0,則進程進入休眠狀態,直至訊號量大於0。進程被喚醒後,返回到開始階段。
函數semget():獲得一個訊號量ID。
#include <sys/sem.h>int semget(key_t key, int nsems, int flag); // 若成功則返回訊號量ID;若出錯則返回-1。
其中,參數nsems是該集合中的訊號量數量。若建立新訊號量,則nsems > 0;若引用一個現存的訊號量,則將nsems的值設為0。
函數semop():在訊號量集合上自動執行參數semoparray數組的命令。
#include <sys/sem.h>int semop(int semid, struct sembuf semoparray[], size_t nops); // 若成功則返回0;若出錯則返回-1。
其中,結構體sembuf:
struct sembuf { unsigned short sem_num; // member # in set short sem_op; // operation (negative, 0, positve) short sem_flg; // IPC_NOWAIT, SEM_UNDO};
semop函數執行的操作主要根據sem_op的值來操作:
- sem_op > 0:進程釋放佔用的資源數,則將sem_op的值加到訊號量的值上。如果指定了undo標誌,則也從該進程的此訊號調整量中減去sem_op;
- sem_op < 0:進程要擷取由該訊號量控制的資源。
(1)若該訊號的值大於等於sem_op的絕對值(資源量滿足需求),則從該訊號量中減去sem_op的絕對值。如果指定undo標誌,則sem_op的絕對值加到該進程的此訊號量的調整值上;
(2)若訊號量小於sem_op的絕對值(資源量不能滿足需求),則:
(a)若指定IPC_NOWAIT,則semop出錯並返回EAGAIN;
(b)若未指定IPC_NOWAIT,則訊號量semid_ds結構中的semncnt加1(因為調用進程將進入休眠狀態),然後調用進程被掛起直到下列事件之一發生:
(i)此訊號量大於等於sem_op的絕對值,喚醒調用進程;
(ii)從系統中刪除了此訊號。在這種情況下,函數出錯並返回EIDRM;
(iii)進程捕捉到一個訊號,並從訊號處理常式返回。在此情況下,訊號量的semncnt減1,並且函數出錯返回EINTR。
- sem_op == 0:表示調用進程希望等待該訊號量變為0。在該情況的處理方式,與sem_op > 0的操作方式一樣,只是條件變為sem_op == 0。
函數semctl():包含了多種對訊號量的操作。
#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ... /* union semun arg */); // 傳回值參考手冊(man 2 semctl)。
其中,該函數常用的指令與訊息佇列中一樣:IPC_STAT、IPC_SET和IPC_RMID。最後一個參數是選擇性參數,其類型為semun,它是多個特定指令參數的聯合。
union semun { int val; // for SETVAL struct semid_ds *buf; // for IPC_STAT and IPC_SET unsigned short *array; // for GETALL and SETALL};
6. 共用儲存區
共用儲存區允許兩個或多個進程共用一給定的儲存區。使用共用儲存區時,利用訊號量或鎖機制來控制多進程之間對給定儲存區的同步訪問。核心為每個共用儲存區設定了一個結構體shmid_ds。
函數shmget():建立一個共用儲存區標識符。
#include <sys/shm.h>int shmget(key_t key, size_t size, int flag); // 若成功則返回共用儲存區的ID;若出錯則返回-1.
其中,size是該共性儲存區的長度,通常將其向上取為系統頁長的整數倍。如果正在建立一個新儲存區,則必須制定size;如果正在引用一個現存的儲存區,則將size值為0。
函數shmctl():對共用儲存區執行多種操作。
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf); // 若成功則返回0;若出錯則返回-1。
其中,參數cmd常用的有:IPC_STAT、IPC_SET、IPC_RMID。其餘的指令為各系統特有的,請參考書冊(linux下,man 2 shmctl)。
函數shmat():將已建立的共用儲存區串連到調用進程的地址空間上。
#include <sys/shm.h>int *shmat(int shmid, const void *addr, int flag); // 若成功則返回指向共用儲存區的指標,若出錯則返回-1。
函數shmdt():將共用儲存區與串連地址脫離。注意,這個操作並不刪除共用儲存區的標識符以及其資料結構。該標識符仍然存在,直到調用shmctl(shmid, IPC_RMID, NULL)來刪除它。
#include <sys/shm.h>int shmdt(void *addr); // 若成功返回0;若出錯則返回-1。
7. Unix域通訊端
Unix域通訊端用於在同一台機器上啟動並執行進程之間的通訊。Unix域通訊端僅僅複製資料;它們並不執行協議處理,不需要添加或刪除網路前序,無須計算校正和、不需要產生順序號、不需發送確認報文。
Unix域通訊端提供流和資料報兩種介面,Unix域資料報服務是可靠的,既不會丟失訊息也不會傳遞出錯。Unix域通訊端是通訊端和管道之間的混合物。
當建立一個命名的、相互串連的Unix通訊端:
- 使用面向網路的Unix域通訊端介面;
- 使用socketpair函數,該函數僅支援AF_UNIX域;
#include <sys/types.h>#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sockfd[2]); // 若成功則返回0;若出錯則返回-1