[單刷APUE系列]第十五章——進程間通訊

引言

在前面章節中講解了進程的派生和常見調用,可是進程之間通訊的惟一途徑就是經過打開的文件,或者是使用進程以前信號傳輸,因爲這些技術的侷限性,Unix系統提供了一種進程間通訊模型(IPC)。
IPC是進程通訊各類方式的統稱,目前只有一些經典的IPC方式能作到移植使用:管道、FIFO、消息隊列、信號量、共享存儲。還有基於套接字技術的網絡IPC。javascript

管道

管道是很古老的進程間通訊機制了,基本全部的Unix系統或者非Unix系統都支持這種方式,管道有如下特性:java

  1. 半雙工,也就是數據只能作到單向流動
  2. 管道只能在具備公共祖先的兩個進城之間使用。

雖然具備侷限性,可是因爲它的可移植性,因此目前仍然是首選的進程間通訊技術,管道在shell中很是常見,咱們經常使用如下命令shell

> command1 | command2 ... commandn複製代碼

shell使用管道將前一個進程的標準輸出與後一條命令的標準輸入相鏈接。
開發者調用pipe函數建立管道數組

int pipe(int fildes[2]);複製代碼

filedes參數包含了兩個文件描述符,Unix手冊上這麼描述The first descriptor connects to the read end of the pipe; the second connects to the write end.,也就是說,filedes[0]是讀的一端,filedes[1]是寫入的一端,這就是一個半雙工的管道,而後能夠經過fork的方式將文件描述符分給父子進程,從而實現通訊。在fork之後,雙方都持有讀寫的端口,而父子進程能夠關閉其中兩個,每一個進程只持有一個,從而作到真正的管道。管道有如下特色:安全

  1. 讀一個寫口關閉的管道時,當緩衝區全部數據都讀取後,會返回0,表明文件結束。
  2. 寫一個讀口關閉的管道時,會產生SIGPIPE信號,若是忽略該信號或者捕獲該信號而且從信號處理函數返回,則write返回-1,errno設置爲EPIPE。

咱們實際上能夠將管道理解爲一塊內核維護的緩衝區,因此常量PIPE_BUF規定了管道的大小。服務器

#include "include/apue.h"

int main(int argc, char *argv[]) {
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if (pipe(fd) < 0)
        err_sys("pipe error");
    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
    } else {
        close(fd[1]);
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }
    exit(0);
}複製代碼

上面就是很是簡單的父子進程使用同一個管道通訊的小實例。網絡

popen和close函數

除了上面的pipe函數之外,更常見的作法是建立一個鏈接到另外一個進程的管道,而後讀其輸出或者向其輸出端發送數據,爲此標準IO庫提供了popen和pclose函數數據結構

FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);複製代碼

咱們能夠看到,這兩個函數是標準C庫提供的函數,而且返回的是FILE結構體的指針,而且這兩個函數的實現是:建立一個管道,fork一個子進程,關閉管道端,exec一個shell運行命令,而後等待命令終止。
popen函數的type參數指示返回的文件指針鏈接的類型,若是type是"r",則文件指針鏈接到cmdstring的標準輸出。若是type是"w",則文件指針鏈接到cmdstring的標準輸入。
pclose函數關閉標準IO流,而且等待命令終止,最後返回shell的終止狀態架構

協同進程

在平常Unix使用中,一般咱們會使用管道鏈接多個命令,當一個程序標準輸入輸出都鏈接到管道的時候,這就是協同進程。
下面是一個父子進程實現的協同進程函數

#include "include/apue.h"

int main(int argc, char *argv[]) {
    int n, int1, int2;
    char line[MAXLINE];

    while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
        line[n] = 0;
        if (sscanf(line, "%d%d", &int1, &int2) == 2) {
            sprintf(line, "%d\n", int1 + int2);
            n = strlen(line);
            if (write(STDOUT_FILENO, line, n) != n)
                err_sys("write error");
        } else {
            if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
                err_sys("write error");
        }
    }
    exit(0);
}複製代碼

上面的代碼很是簡單,就是標準輸入讀取,計算後輸出到標準輸出,將其編譯爲add2程序。

#include "include/apue.h"

static void sig_pipe(int);

int main(int argc, char *argv[]) {
    int n, fd1[2], fd2[2];
    pid_t pid;
    char line[MAXLINE];

    if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
        err_sys("signal error");

    if (pipe(fd1) < 0 || pipe(fd2) < 0)
        err_sys("pipe error");

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid > 0) {
        close(fd1[0]);
        close(fd2[1]);

        while (fgets(line, MAXLINE, stdin) != NULL) {
            n = strlen(line);
            if (write(fd1[1], line, n) != n)
                err_sys("write error to pipe");
            if ((n = read(fd2[0], line, MAXLINE)) < 0)
                err_sys("read error from pipe");
            if (n == 0) {
                err_msg("child closed pipe");
                break;
            }
            line[n] = 0;
            if (fputs(line, stdout) == EOF)
                err_sys("fputs error");
        }
        if (ferror(stdin))
            err_sys("fgets error on stdin");
        exit(0);
    } else {
        close(fd1[1]);
        close(fd2[0]);
        if (fd1[0] != STDIN_FILENO) {
            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
                err_sys("dup2 error to stdin");
            close(fd1[0]);
        }
        if (fd2[1] != STDOUT_FILENO) {
            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
                err_sys("dup2 error to stdout");
            close(fd2[1]);
        }
        if (execl("./add2", "add2", (char *)0) < 0)
            err_sys("execl error");
    }
    exit(0);
}複製代碼

程序建立了兩個管道,父子進程各自關閉不須要使用的管道。而後使用dup2函數將其移到標準輸入輸出,最後調用execl。

FIFO

FIFO就是First In First Out先進先出隊列,也被稱爲命名管道。未命名的管道只能在兩個相關進程使用,而命名管道就能全局使用。
建立一個FIFO就等同於建立一個文件,在前面的時候就講過,FIFO就是一種文件,而且在stat文件結構中就有st_mode成員編碼能夠知道是不是FIFO。

int mkfifo(const char *path, mode_t mode);複製代碼

還有一個mkfifoat函數在某些系統中是不可用的,其實就是一個文件描述符版本的mkfifo函數,基本同樣,其中mode參數和open函數中mode參數相同。新的FIFO用戶和組的全部權規則和前面章節中講述的同樣。
當建立完FIFO文件後,須要使用open函數打開,由於這確實是一個文件,幾乎全部的正常文件IO函數都能操做FIFO。
非阻塞標誌(O_NONBLOCK)對FIFO會有如下影響:

  1. 未指定標誌的時候,只讀的open會一直阻塞到有進程寫打開,只寫open會阻塞到有進程讀打開。
  2. 指定了標誌,則只讀open馬上返回。可是若是沒有進程爲只讀打開,只寫的open會出錯。

和管道相似,若是write一個沒有進程讀打開的FIFO則會產生SIGPIPE信號。若是讀完全部數據,read函數會返回文件結束。
因爲這是一個文件,因此多個進程讀寫是很是正常的事情,因此爲了保證讀寫安全,原子寫操做是必需要考慮的。FIFO有如下用途:

  1. shell命令使用FIFO將數據從一條管道傳輸到另外一條管道不須要建立中間文件
  2. C/S架構中,FIFO用做中間點,在客戶服務器之間傳輸數據

這裏就再也不講實例了。須要的朋友自行尋找代碼。

XSI IPC

在IPC中有三種被稱爲XSI IPC,他們有不少共同點。這裏先講解共同點。

標識符和鍵

XSI IPC在內核中存在着IPC結構,它們都用一個非負整數做爲標識符,這點很像文件描述符,可是文件描述符永遠是當前最小的開始,好比,第一個文件描述符必然是從3開始,而後這個文件描述符刪除後,再次打開一個文件,文件描述符仍然是3,而IPC結構則不會減小,會變成4,而後不斷增長直到整數的最大值,而後又迴轉到0。
標識符是IPC結構的內部名稱,爲了能全局使用,須要有一個鍵做爲外部名稱,不管什麼時候建立IPC結構,都應當指定一個鍵名,這個鍵的數據結構是基本系統數據類型key_t。而且有不少種方法使客戶進程和服務器進程在同一個IPC結構匯聚

  1. 服務器進程指定IPC_PRIVATE鍵建立一個新的IPC結構。返回的標識符被存放在一個文件中,客戶端進程讀取這個文件來參與IPC結構。
  2. 在一個公用頭文件中定義一個統一的標識符,而後服務端根據這個標識符建立新的IPC結構,可是頗有可能致使衝突
  3. 客戶端和服務端進程認同同一個路徑名和項目ID。接着調用函數ftok將這兩個值變爲一個鍵,而後在建立一個IPC結構
key_t ftok(const char *path, int id);複製代碼

path參數必須是一個現有的文件,當產生鍵的時候,只會使用id參數低八位。
ftok建立鍵通常依據以下行爲:首先根據給定的path參數得到對應文件的stat結構中st_dev和st_ino字段,而後將他們和項目ID組合。

權限結構

每一個IPC結構都關聯了一個ipc_perm結構,這個結構體關聯了權限和全部者。

struct ipc_perm
{
        uid_t           uid;            /* [XSI] Owner's user ID */
        gid_t           gid;            /* [XSI] Owner's group ID */
        uid_t           cuid;           /* [XSI] Creator's user ID */
        gid_t           cgid;           /* [XSI] Creator's group ID */
        mode_t          mode;           /* [XSI] Read/write permission */
        unsigned short  _seq;           /* Reserved for internal use */
        key_t           _key;           /* Reserved for internal use */
};複製代碼

上面是蘋果平臺的結構體內容,通常來講,都會有uid、gid、cuid、cgi、mode這些基本的內容,而其餘則是各個實現自由發揮。在建立的時候,這些字段都會被賦值,然後,若是想要修改這些字段,則必須是保證具備root權限或者是建立者。

優勢和缺點

XSI IPC一個問題就是:IPC結構是在系統範圍內使用的,可是卻沒有引用計數。若是進程使用完可是沒有對其進行刪除就終止了,那就會致使IPC依然在系統中存在,而管道有引用計數,等最後一個引用管道的進程終止便會自動回收。FIFO就算沒有刪除,可是等最後一個引用FIFO的進程終止,裏面的數據已經被刪除了。
XSI IPC還有一個問題就是它不是文件,咱們不能使用ls和rm等文件操做函數或者命令處理它們,它們也沒有文件描述符,這就限制了它們的使用,而且若是須要使用還得攜帶一大堆額外的API。

消息隊列

消息隊列,正如其名稱同樣,是消息的鏈表形式,它由內核存儲維護。而且和XSI IPC結構同樣,由消息隊列標識符標識。
msgget函數建立一個隊列或者打開一個現有隊列,msgsnd將新數據添加到消息末尾,每一個消息包含一個正的長整形字段、一個非負的長度以及實際數據字節數。msgrcv從隊列中得到消息。

struct __msqid_ds {
            struct __ipc_perm_new   msg_perm; /* [XSI] msg queue permissions */
        __int32_t       msg_first;      /* RESERVED: kernel use only */
        __int32_t       msg_last;       /* RESERVED: kernel use only */
        msglen_t        msg_cbytes;     /* # of bytes on the queue */
        msgqnum_t       msg_qnum;       /* [XSI] number of msgs on the queue */
        msglen_t        msg_qbytes;     /* [XSI] max bytes on the queue */
        pid_t           msg_lspid;      /* [XSI] pid of last msgsnd() */
        pid_t           msg_lrpid;      /* [XSI] pid of last msgrcv() */
        time_t          msg_stime;      /* [XSI] time of last msgsnd() */
        __int32_t       msg_pad1;       /* RESERVED: DO NOT USE */
        time_t          msg_rtime;      /* [XSI] time of last msgrcv() */
        __int32_t       msg_pad2;       /* RESERVED: DO NOT USE */
        time_t          msg_ctime;      /* [XSI] time of last msgctl() */
        __int32_t       msg_pad3;       /* RESERVED: DO NOT USE */
        __int32_t       msg_pad4[4];    /* RESERVED: DO NOT USE */
}複製代碼

每一個系統實現都會在SUS標準的基礎上增長本身私有的字段,因此可能和上面的有所區別。這個結構體定義了隊列的當前狀態。

int msgget(key_t key, int flag);複製代碼

msgget根據key得到已有隊列或者新隊列。

int msgctl(int msqid, int cmd, struct msqid_ds *buf);複製代碼

msgctl用於對隊列進行多種操做,其中,msqid則是消息隊列ID,cmd參數是命令參數,能夠取如下值:

  1. IPC_STAT 取隊列msqid_ds結構體,而且存放在buf參數指定的位置
  2. IPC_SET 將buf參數指定的結構體複製到這個隊列中的結構體,須要檢查root權限或者建立者權限
  3. IPC_RMID 從系統中刪除消息隊列
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);複製代碼

這個函數很好理解,就是把ptr指針對應的消息放入消息隊列中,flag的值能夠指定爲IPC_NOWAIT,這點相似於文件IO非阻塞標誌。

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

和msgsnd同樣,這個ptr參數指向一個長整形數,隨後跟隨的是存儲實際區域的緩衝區。nbytes指定數據緩衝區的長度。參數type則能夠指定想要哪一種消息。

信號量

信號量是一個計數器,針對多個進程提供對共享數據對象的訪問。信號量的使用主要是用來保護共享資源的,使得資源在一個時刻只會被一個進程(線程)擁有。
信號量的使用以下:

  1. 測試控制該資源的信號量
  2. 若是信號量爲正,則進程可使用該資源,這種狀況下,信號量會減一,表示已經使用了一個資源。
  3. 若是信號量爲0,則進程進入休眠狀態,知道信號量變爲正,進程將會喚醒。

內核爲每一個信號量集合維護着一個semid_ds結構體,根據每一個系統實現不一樣會有不一樣的字段,這裏就不列出了。當咱們想要使用信號量的時候,使用以下函數

int semget(key_t key, int nsems, int semflg);複製代碼

咱們知道,XSI IPC實際上具備其共性,因此如同消息隊列同樣,這裏將key變換爲標識符的規則也是同樣的。

int semctl(int semid, int semnum, int cmd, ...);複製代碼

就如同是前面的ioctl等函數同樣,這個函數也是用於控制信號量,其中第四個參數是可選的,取決於cmd參數。

IPC_STAT     Fetch the semaphore set's struct semid_ds, storing it in the memory pointed to by arg.buf.

     IPC_SET      Changes the sem_perm.uid, sem_perm.gid, and sem_perm.mode members of the semaphore set's struct semid_ds to match those of the struct pointed to
                  by arg.buf.  The calling process's effective uid must match either sem_perm.uid or sem_perm.cuid, or it must have superuser privileges.

     IPC_RMID     Immediately removes the semaphore set from the system.  The calling process's effective uid must equal the semaphore set's sem_perm.uid or
                  sem_perm.cuid, or the process must have superuser privileges.

     GETVAL       Return the value of semaphore number semnum.

     SETVAL       Set the value of semaphore number semnum to arg.val.  Outstanding adjust on exit values for this semaphore in any process are cleared.

     GETPID       Return the pid of the last process to perform an operation on semaphore number semnum.

     GETNCNT      Return the number of processes waiting for semaphore number semnum's value to become greater than its current value.

     GETZCNT      Return the number of processes waiting for semaphore number semnum's value to become 0.

     GETALL       Fetch the value of all of the semaphores in the set into the array pointed to by arg.array.

     SETALL       Set the values of all of the semaphores in the set to the values in the array pointed to by arg.array.  Outstanding adjust on exit values for
                  all semaphores in this set, in any process are cleared.複製代碼

上面就是cmd可選值,除了GETALL之外的全部命令,semctl都返回相應值,除此之外,還有個semop函數自動執行信號量集合上的操做數組

int semop(int semid, struct sembuf *sops, size_t nsops);複製代碼

sembuf參數是一個指針,指向了sembuf結構體,實際上這是一個數組,

共享存儲

共享存儲是一項頗有用的技術,它容許兩個或者更多的進程共享同一個給定存儲區,因爲數據不須要複製,因此這是一種最快的IPC方式,共享存儲最重要的就是資源的競爭,因此信號量通常用於共享存儲訪問。
在前面的章節中,咱們看到了一種共享存儲的方式,就是內存映射技術,可是相比存儲映射,共享存儲不須要建立中間文件。

int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);複製代碼

就如同是其餘的XSI IPC同樣,這裏也是一個建立函數,經過這個函數得到存儲標識符。而後就是shmctl函數,具體的使用直接查手冊,基本上都是差很少的。
當建立完成共享存儲段後,使用shmat將其連接到本身的地址空間中。

void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);複製代碼

實際上,因爲架構的不一樣和平臺不一樣,爲了保持可移植性,咱們不該當去指定共享存儲段的地址,而是由系統自行分配,最終返回的就是共享存儲段的地址,當操做完成後,咱們須要使用shmdt函數將其分離。

POSIX信號量

POSIX信號量是三種IPC機制之一,相比XSI標準規定的IPC方式,POSIX的方式更加簡潔好用。
POSIX信號量有兩種類型:命名的和未命名的,他們二者的區別就像是命名管道和未命名管道同樣,有了標識符的信號量就能全局使用,而沒有標識符的信號量只能在同一內存區域內使用。

sem_t *sem_open(const char *name, int oflag, ...);
The parameters "mode_t mode" and "unsigned int value" are optional.

The value of oflag is formed by or'ing the following values:

    O_CREAT         create the semaphore if it does not exist
    O_EXCL          error if create and semaphore exists複製代碼

實際上這個函數技能建立也能使用現有信號量,上面的是Unix手冊節選的內容,應該算是至關清楚了,當函數返回的時候,sem_open會返回一個指針,讓咱們傳遞到其餘的函數上,等一切結束,使用sem_close關閉信號量指針。

int sem_close(sem_t *sem);複製代碼

固然,也能使用sem_unlink函數銷燬一個命名信號量。

int sem_unlink(const char *name);複製代碼

在這裏咱們能看出來,POSIX信號量的命名信號量和文件很像。不像XSI信號量,POSIX信號量的值只能經過一個函數調用來調節,也就是sem_wait函數

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);複製代碼

後一個是sem_wait函數的嘗試版本,還有一個是超時版本,可是蘋果下好像不存在,建議少使用。
還能夠調用sem_post函數使信號量值+1,這個解鎖一個二進制信號量或者釋放一個技術信號量資源的過程是很像的。

int sem_post(sem_t *sem);複製代碼

對於未命名信號量,只能使用在單進程或者單線程中,是很是容易的,咱們只須要使用sem_init函數建立,而後使用sem_destroy函數銷燬。這裏就不在贅述。

相關文章
相關標籤/搜索