《UNIX環境高級編程》第15章 進程間通信

15.1 引言

第8章說明的進程控制原語,並且觀察瞭如何調用多個進程。但是這些進程間交互信息的唯一途徑就是傳送打開的文件,可以經由fork或exec來傳送,也可以通過文件系統來傳送。
本章討論經典IPC:管道、FIFO、消息隊列、信號量已經共享存儲。
下一章討論使用套接字機制的網絡IPC。

15.2 管道

  1. 歷史上,管道是半雙工的。現在某些系統提供了全雙工的管道,但爲了移植,我們不該假設系統支持全雙工管道。
  2. 管道只能在具有公共祖先的兩個進程間使用。通常,一個管道由一個進程創建,在進程調用fork之後,這個管道就能在父進程和子進程之間使用了。
    (FIFO沒有第二種侷限性,UNIX域套接字沒有這兩種侷限性。)

儘管有這兩種侷限,半雙工管道仍然是常用的IPC形式。每當在管道中鍵入一個命令序列,讓shell執行時,shell都會爲每一個條命令單獨創建一個進程,然後用管道將前一條命令進程的標準輸出後一條命令的標準輸入相鏈接。
管道是通過pipe函數創建的。

#include <unistd.h>
int pipe(int fd[2]);
//經由參數fd返回兩個文件描述符:fd[0]爲讀而打開,fd[1]爲寫而打開。

單個進程中的管道計劃沒有任何用處。通常,經常進程會先調用pipe,接着調用fork,從而創建從父進程到子進程的IPC通道。
這裏寫圖片描述

在寫管道(或FIFO)時,常量PIPE_BUF規定了內核的管道緩衝區大小。可以通過pathconf或fpathconf函數來確定其值。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define MAXLINE 20

int main(void)
{

    int n;
    int fd[2];
    pid_t   pid;
    char    line[MAXLINE];
    if(pipe(fd)<0)
        printf("pipe error. \n");

    if((pid=fork())<0){
        printf("fork error. \n");
    } else if(pid>0){
        close(fd[0]);
        printf("current pid is %d. \n",getpid());
        write(fd[1],"hello world.\n",12);
    } else {
        close(fd[1]);
        n=read(fd[0],line,MAXLINE);
        printf("current pid is %d. \n",getpid());
        write(STDOUT_FILENO,line,n);
    }

    exit(0);
}

這裏寫圖片描述

15.3 函數popen和pclose

常見的操作是創建一個連接到另一個進程的管道,然後讀其輸出或向其輸入端發送數據,爲此,標準IO庫提供了兩個函數popen和pclose。
這兩個函數實現的操作是:創建一個管道,fork一個子進程,關閉未使用的管道端,執行一個shell運行命令,然後等待命令終止。

#include <stdio.h>
FILE *popen(const char *cmdstring,const char *type);
int pclose(FILE *fp);

函數popen**先執行fork**,然後調用exec執行cmdstring,並且返回一個標準IO文件指針。
type參數如果是“r”則返回的文件指針是可讀的,若是“w”則返回的文件指針是可寫的。(就是說,“r”返回的是子進程的stdout,“w”返回的是子進程的stdin。)

15.4 協同進程

UNIX系統過濾程序從標準輸入讀取數據,向標準輸出寫數據。幾個過濾程序通常在shell管道中線性連接。
當一個過濾程序即產生某個過濾程序的輸入,又讀取該過濾程序的輸出時,它就變成了協同進程(coprocess)。
協同進程通常在shell的後臺運行,其標準輸入和標準輸出通過管道連接到另一個程序。
這裏寫圖片描述
popen只提供連接到另一個進程的標準輸入或標準輸出的一個單向管道,而協同進程則有連接到另一個進程的兩個單向管道:一個連接到其標準輸入,另一個則來自標準輸出。我們想將數據寫到其標準輸入,經其處理後,再從其標準輸出讀取數據。

15.5 FIFO

FIFO有時被稱爲命名管道。未命名管道只能在兩個相關的進程之間使用,而且這兩個相關的進程還要有一個共同創建了它們的祖先進程。但是,通過FIFO,不相關的進程也能交互數據
14章中已經提到,FIFO是一種文件類型。通過stat結構的st_mode成員的編碼可以知道文件是否是FIFO類型。可以使用S_ISFIFO宏對此進行測試。
創建FIFO類似於創建文件。確實,FIFO的路徑名存在於文件系統中。

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

FIFO有以下兩種用途:

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

15.6 XSI IPC

有3種稱爲XSI IPC 的IPC :消息隊列、信號量以及共享存儲器。

15.6.1 標識符和鍵

  • 每個內核中的IPC結構(消息隊列、信號量以及共享存儲器)都用一個非負整數標識符(identifier)加以引用。例如:要向一個消息隊列發送消息或者從一個消息隊列取消息,只需要知道其隊列標識符。與文件描述符不同,IPC標識符不是小整數。當一個IPC結構被創建然後又被刪除,這種結構相關的標識符連續加1,直到達到一個整形數的最大值,然後又回到0。
  • 標識符是對象的內部名。爲了使多個合作進程能夠在同一IPC對象上匯聚,需要提供一個外部命名方案。爲此,每個IPC對象都與一個鍵(key)相關聯,將這個鍵作爲該對象的外部名
  • 無論何時創建IPC結構(通過調用msgget、semget或shmget創建),都應該指定一個鍵。這個鍵的數據類型是基本數據類型key_t,在頭文件sys/types.h中被定義爲長整型。這個鍵由內核變換成標識符

有多種方法使客戶進程和服務器進程在同一IPC上匯聚。

  1. 服務器進程可以指定鍵IPC_PRIVATE創建一個新IPC結構,將返回的標識符存放在某處(如一個文件中)以便客戶進程取用。鍵IPC_PRIVATE保證服務器進程創建一個新IPC結構。這種技術的缺點是:兩個進程需要通過文件系統來傳遞IPC標識符。IPC_PRIVATE鍵也可以用於父子進程,子進程將此標識符通過exec函數做爲參數傳遞給一個新程序。
  2. 可以在一個公用頭文件中定義一個客戶進程和服務器進程都認可的鍵。然後服務器進程指定此鍵創建一個新的IPC結構。
  3. 客戶進程和服務進程認同一個路徑名項目ID(項目ID爲0-255之間的值),然後調用函數ftok將這兩個值變換爲一個鍵。然後在方法(2)中使用此鍵。ftok提供的唯一服務就是由一個路徑名和項目ID產生一個鍵。
#include <sys/ipc.h>
key_t ftok(const char *path,int id);

15.6.2 權限結構

XSI IPC爲每一個IPC結構管理了一個ipc_perm結構。該結構規定了權限所有者,它至少包括下列成員:

struct ipc_perm{
uid_t uid;  //owner's effective user id
gid_t gid;  //owner's effective group id
uid_t cuid; //creator's effective user id
gid_t cgid; //creator's effective group id
mode_t mode;    //access modes
};

在創建IPC結構時,對所有字段都賦初始值。以後可以調用msgctl、semctl後shmctl修改uid、gid和mode字段。

15.6.3 結構限制

所有3種形式的XSI IPC都有內置限制。大多數限制都可以通過重新配置內核來改變。

15.6.4 優點和缺點

APUE pg 451.

15.7 消息隊列

消息隊列是消息的連接表,存儲在內核中,由消息隊列標識符標識。
msgget用於創建一個新隊列或打開一個現有隊列。
msgsnd用於將消息添加到隊列尾端。
msgrcv用於從隊列中取消息。
每個隊列都有一個msgid_ds結構與其關聯:

struct msgid_ds{
struct ipc_perm msg_perm;   //see section 15.6.2
msgqnum_t   msg_qnum;   //隊列的消息編號
msglen_t    msg_qbytes; //隊列的最大字節數
pid_t   msg_lspid;  //最後發送者的pid
pid_t   msg_lrpid;  //最後接收者的pid
time_t  msg_stime;  //最後發送的時間
time_t  msg_rtime;  //最後接收時間
time_t  msg_ctime;  //最後改變時間
.
.
.
};

此結構定義了隊列的當前狀態。


msgget函數得到隊列ID,此值就可以被其他隊列函數所用.

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

msgctl函數對隊列執行多種操作。它和另外兩個信號量及共享存儲有關函數(semctl和shmctl)都是XSI IPC的類似於ioctl的函數(即垃圾桶函數)。

#include <sys/msg.h>
int msgctl(int msgid,int cmd,struct msgid_ds *buf);

cmd參數對msgid指定的隊列執行命令。


msgsnd將數據放到消息隊列中。

#include <sys/msg.h>
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);

msgrcv從隊列中取用消息。

#include <sys/msg.h>
int msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag);

15.8 信號量

信號量與已經介紹過的IPC結構(管道、FIFO已經消息隊列)不同。它是一個計數器,用於爲多個進程提供對共享數據對象的訪問
爲了獲得共享資源,進程需要執行下列操作:

  1. 測試控制該資源的信號量。
  2. 若此信號量爲正,則進程可以使用該資源。在這種情況下,進程會將信號值減1,表示它使用了一個資源單位。
  3. 否則,若此信號量的值爲0,則進程進入休眠狀態,直至信號量值大於0.進程被喚醒後它返回步驟(1)。
    內核爲每個信號量集合維護着一個semid_ds結構:
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short  sem_nsems;
time_t  sem_otime;
time_t  sem_ctime;
};

當我們想使用XSI信號量時,首先需要通過函數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 semum arg*/);

cmd參數擁有10種命令,不多說了;
第四個參數是可以選的,是否使用取決於所請求的命令,如果使用該參數,則其類型是semun,它是多個命令特定參數的聯合:

union semun{
int     val;
struct semid_ds *buf;
unsigned short  *array;
};

semctl同樣是類似於ioctl的垃圾桶函數。


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

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

15.9 共享存儲

共享存儲允許兩個或多個進程共享一個給定的存儲區。因爲數據不需要在客戶進程和服務器進程之間複製,所以這是最快的一種IPC。使用共享存儲時要掌握的唯一竅門是,在多個進程之間同步訪問一個給定的存儲區。若服務器進程正在講數據放入共享存儲區,則在它做完這一操作之前,客戶進程不應當去取這些數據。通常信號量用於同步共享存儲訪問。
之前我們已經看到多個進程共享存儲的一種形式,就是在多個進程將同一個文件映射到它們的地址空間的時候。XSI共享存儲和內存映射的文件不同之處在於,前者沒有相關的文件。XSI共享存儲段是內存的匿名段。
內核爲每個共享存儲段維護着一個結構,該結構至少包含以下成員:

struct shmid_ds{
struct ipc_perm shm_perm;   //see section 15.6.2
size_t  shm_segsz;  //存儲區大小
pid_t   shm_lpid;   //最後shmop()進程的pid
pid_t   shm_cpid;   //最後接收者的pid
shmatt_t    shm_nattch;//本次附加號
time_t  shm_atime;  //最後附加時間
time_t  shm_dtime;  //最後分離時間
time_t  shm_ctime;  //最後改變時間
.
.
.
};

調用的第一個函數通常是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);

參數addr設爲0時,由系統選擇地址。
shmat返回值是該段所連接的實際地址,如果出錯返回-1.


當對共享存儲段的操作已經結束時,則調用shmdt與該段分離。注意,這並不從系統中刪除其標識符以及其相關的數據結構。該標識符仍然存在,直至某個進程(一般是服務器進程)帶IPC_RMID命令的調用shmctl特地刪除它爲止。

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

addr參數是以前調用shmat時的返回值。如果成功,shmdt將使相關shmid_ds結構中的shm_nattch計數器值減1.


共享存儲區的佈局:
這裏寫圖片描述

15.10 POSIX 信號量

POSIX信號量有兩種形式:命名的和未命名的。它們的差異在於創建和銷燬的形式上,但其他工作一樣。

  • 未命名信號量 只存在於內存中,並要求能使用信號量的進程必須可以訪問內存。這意味着它們只能應用在同一進程中的線程,或者不同進程中已經映射相同內存內容到它們的地址空間中的線程。
  • 命名信號量 可以通過名字訪問,因此可以被任何已知它們名字的進程中的線程使用。

我們可以調用sem_open函數來創建一個新的命名信號量或者使用一個現有信號量

#include <semaphore.h>
sem_t *sem_open(const char *name,int oflag,.../*mode_t mode,unsigned int value */);

當完成信號量操作時,可以調用sem_close函數來釋放任何信號量相關的資源。

#include <semaphore.h>
int sem_close(sem_t *sem);

可以使用sem_unlink函數來銷燬一個命名信號量。

#include <semaphore.h>
int sem_unlink(const char *name);

sem_unlink函數刪除信號量的名字。如果沒有打開信號量的引用,則信號量會被銷燬。否則,銷燬延遲到最後一個打開的引用關閉。


可以使用sem_wait或sem_trywait函數來實現信號量的減1操作。

#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_timedwait(sem_t *sem,const struct timespec *restrict tsptr);

使用sem_wait函數時,如果信號量計數是0就會發生阻塞。而sem_trywait則不會。sem_timewait則可以指定一段阻塞的時間。


可以使用sem_post函數使信號量值增1。這和解鎖一個二進制信號量或者釋放一個計數信號量相關資源的過程是類似的。

#include <semaphore.h>
int sem_post(sem_t *sem);

當我們想在單個進程中使用POSIX信號量時,使用未命名信號量更容易。這僅僅需要改變創建和銷燬信號量的方式。使用sem_init函數來創建一個未命名信號量。

#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigent int value);

pshared 非0時,表示可以在多個進程中使用。
value參數指定了信號量的初始值。
如果要在兩個進程之間使用信號量,需要確保sem參數指向兩個進程之間共享的內存範圍。


對命名信號量的使用以及完成時,可以用sem_destroy函數丟棄它。

#include <semaphore.h>
int sem_destroy(sem_t *sem);

調用sem_destroy之後不能再使用任何帶有sem的信號量函數,除非通過調用sem_init重新初始化它。


sem_getvalue函數可以用來檢索信號量值。

#include <semaphore.h>
int sem_getvalue(sem_t *restrict  sem,int *restrict valp);

成功之後,valp指向的整數值將包含信號量值。但我們試圖使用剛取出來的值的時候,信號量的值可能已經變了。除非使用額外的同步機制來避免這種競爭,否則sem_getvalue函數只能用於調試。

15.11 客戶進程-服務器進程屬性

15.12 小結

本章詳細說明了進程間通信的多種新式:管道、命名管道(FIFO)、稱爲XSI IPC的3種形式的IPC(消息隊列、信號量和共享存儲),以及POSIX提供的代替信號量機制。
信號量實際上是同步原語而不是IPC,常用於共享資源(如共享存儲段)的同步訪問。

發佈了55 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章