POSIX 程序設(shè)計(jì) __ 多進(jìn)程和進(jìn)程通信

UNIX-like 操作系統(tǒng),有一個(gè)強(qiáng)勁的功能:可以同時(shí)運(yùn)行多個(gè)進(jìn)程,并且讓進(jìn)程們共享 CPU,內(nèi)存,和其他的資源.我們所開(kāi)發(fā)的程序,往往會(huì)從一個(gè)進(jìn)程演變?yōu)槎鄠€(gè)進(jìn)程.的確,許多時(shí)候線程是不錯(cuò)的選擇,不過(guò)多進(jìn)程和多線程使用有許多是相通的:如何啟動(dòng)和停止進(jìn)程,如何在進(jìn)程間通信,如何同步進(jìn)程.

什么是 UNIX 進(jìn)程?

談?wù)撨M(jìn)程前,需要正確的理解"什么是進(jìn)程":

進(jìn)程是一個(gè)正在運(yùn)行的程序?qū)嵗瑘?zhí)行給定的代碼,有自己的"執(zhí)行棧內(nèi)存",有自己的"內(nèi)存頁(yè)集合",有自己的"文件描述符號(hào)碼表",有一個(gè)唯一的進(jìn)程號(hào)碼.

從這個(gè)定義可以知道,一個(gè)進(jìn)程并不代表一個(gè)程序,幾個(gè)進(jìn)程可能是同一時(shí)刻同一個(gè)程序的多個(gè)實(shí)例,他們的用戶可能是相同的,也可能是不同的.

使用系統(tǒng)調(diào)用 fork() 創(chuàng)建進(jìn)程

系統(tǒng)調(diào)用 pid_t fork(void) 用來(lái)創(chuàng)建新的進(jìn)程.這是一個(gè)非常奇特的調(diào)用,返回兩次!聽(tīng)起來(lái)很奇怪.首先,在一個(gè)正在運(yùn)行的進(jìn)程中調(diào)用 fork(),會(huì)創(chuàng)建一個(gè)新進(jìn)程,新進(jìn)程是調(diào)用進(jìn)程的子進(jìn)程.當(dāng) fork() 時(shí),調(diào)用進(jìn)程的所有正在使用的"內(nèi)存頁(yè)"會(huì)被復(fù)制,調(diào)用進(jìn)程正在使用的"文件描述符號(hào)碼表"也會(huì)被復(fù)制,這些復(fù)制被放入新進(jìn)程中使用.因此,新進(jìn)程和調(diào)用進(jìn)程的這些內(nèi)容變成相互獨(dú)立的.

fork
fork

注意:

"寫時(shí)復(fù)制",許多現(xiàn)代系統(tǒng)采用了這些技術(shù).當(dāng) fork() 時(shí),系統(tǒng)并不立刻復(fù)制"內(nèi)存頁(yè)"和"文件描述符號(hào)碼表",只是在新進(jìn)程中做一個(gè)映射,這樣會(huì)節(jié)省很多復(fù)制開(kāi)銷.當(dāng)新進(jìn)程中有代碼修改"內(nèi)存頁(yè)"和"文件描述符號(hào)碼表"時(shí),系統(tǒng)會(huì)立刻復(fù)制一個(gè)副本,用于修改.

還有一個(gè)需要注意的:

"文件描述符號(hào)碼表":操作系統(tǒng)每打開(kāi)一個(gè)文件,會(huì)在內(nèi)核緩存中保存文件資源,并為每一個(gè)文件建立一些散列表,記錄文件信息,同時(shí)為開(kāi)發(fā)者提供一個(gè)非負(fù)整數(shù)的號(hào)碼(文件表的索引),用于引用內(nèi)存中的文件資源.因此,多個(gè)進(jìn)程的"文件描述符號(hào)碼表"雖然是獨(dú)立的,但是號(hào)碼指向的文件卻可能是同一個(gè)文件,對(duì)同一個(gè)文件的修改是可以影響到多個(gè)進(jìn)程.由此,出現(xiàn)了另一項(xiàng)技術(shù):"同步",防止多進(jìn)程同時(shí)修改文件資源.

fork() 返回兩次,返回值有 3 種情況:

  1. 如果返回的是 0 ,那么返回值位于新進(jìn)程中
  2. 如果返回的是 >0,那么返回值位于調(diào)用進(jìn)程中,返回值是新進(jìn)程的進(jìn)程號(hào)碼.返回值 pid_t 定義在 sys/types.h 頭文件中,通常是一個(gè)整數(shù)
  3. 如果返回的是 -1,那么返回值位于調(diào)用進(jìn)程中,創(chuàng)建新進(jìn)程失敗

例子:

#include <stdio.h>
#include <stdlib.h>

#define MAX_COUNT 200

void doChild(void); 
void doParent(void);

void main(void) {
    pid_t pid = fork();

    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }
}

void doChild(void) {
    int i;
    for (i = 1; i <= MAX_COUNT; i++)
        printf("This line is from child, value = %d\n", i);
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    int i;
    for (i = 1; i <= MAX_COUNT; i++)
        printf("This line is from parent, value = %d\n", i);
    printf("*** Parent is done ***\n");
}

對(duì)于這個(gè)例子,我準(zhǔn)備了一個(gè) Nim 語(yǔ)言版本的:

import posix, os

const maxCount = 200

proc doChild()
proc doParent()

proc main() =
    var pid = fork()
    case pid
    of -1:
        raise newException(OsError, osLastError().osErrorMsg())
    of 0:
        doChild()
        quit(QuitSuccess)
    else:
        doParent() 

proc doChild() =
    for i in 1..maxCount:
        echo "This line is from child, value = ", i
    echo "*** Child process is done ***"

proc doParent() =
    for i in 1..maxCount:
        echo "This line is from parent, value = ", i
    echo "*** Parent process is done ***"

main()

子進(jìn)程結(jié)束

一旦創(chuàng)建了一個(gè)新進(jìn)程,其就成為調(diào)用進(jìn)程的子進(jìn)程.有兩種可能存在的情況:父進(jìn)程先于子進(jìn)程退出,子進(jìn)程先于父進(jìn)程退出:

  • 當(dāng)一個(gè)子進(jìn)程退出時(shí),并不立刻清空進(jìn)程表,而是向父進(jìn)程發(fā)送一個(gè)信號(hào).父進(jìn)程需要對(duì)此應(yīng)答,然后系統(tǒng)會(huì)完全清除子進(jìn)程.假設(shè)父進(jìn)程沒(méi)有應(yīng)答,或者應(yīng)答之前子進(jìn)程退出,子進(jìn)程會(huì)被系統(tǒng)設(shè)置為"僵尸"狀態(tài).

  • 當(dāng)一個(gè)父進(jìn)程退出時(shí),如果有幾個(gè)子進(jìn)程仍在運(yùn)行,這些子進(jìn)程會(huì)變成"孤兒進(jìn)程"."孤兒進(jìn)程"會(huì)立刻被 "init" 超級(jí)進(jìn)程接管,作為其父進(jìn)程."init" 進(jìn)程能夠確保這些子進(jìn)程在退出時(shí)不會(huì)變?yōu)椋⒔┦M(jìn)程",因?yàn)?"init" 進(jìn)程總是應(yīng)答子進(jìn)程的退出.("init" 進(jìn)程是隨操作系統(tǒng)啟動(dòng)的第一個(gè)進(jìn)程,進(jìn)程號(hào)碼是 1)

使用系統(tǒng)調(diào)用 wait() 應(yīng)答子進(jìn)程

應(yīng)答子進(jìn)程的簡(jiǎn)單方法,是使用系統(tǒng)調(diào)用 pid_t wait(int * status).當(dāng)調(diào)用這個(gè)函數(shù)時(shí),有以下可能:

  1. 進(jìn)程中沒(méi)有子進(jìn)程,立刻返回
  2. 進(jìn)程中的子進(jìn)程已經(jīng)退出,并且是"僵尸"狀態(tài),立刻應(yīng)答并返回,取得子進(jìn)程狀態(tài)(通過(guò)參數(shù))
  3. 進(jìn)程中的子進(jìn)程都在運(yùn)行,立刻阻塞,直到有一個(gè)子進(jìn)程退出或者收到一個(gè) "SIGCHILD" 信號(hào)時(shí)返回,取得子進(jìn)程狀態(tài)(通過(guò)參數(shù))
wait
wait

例子:

#include <stdio.h>
#include <stdlib.h>

#define MAX_COUNT 200

void doChild(void); 
void doParent(void);

void main(void) {
    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }

    int status;
    pid = wait(&status);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(void) {
    int i;
    for (i = 1; i <= MAX_COUNT; i++)
        printf("This line is from child, value = %d\n", i);
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    int i;
    for (i = 1; i <= MAX_COUNT; i++)
        printf("This line is from parent, value = %d\n", i);
    printf("*** Parent is done ***\n");
}

注:當(dāng)一個(gè)進(jìn)程正常或者異常結(jié)束時(shí),內(nèi)核就向其父進(jìn)程發(fā)送 "SIGCHILD" 信號(hào).子進(jìn)程結(jié)束是一個(gè)異步事件,(可以在父進(jìn)程運(yùn)行的任何時(shí)候發(fā)生),所以這種信號(hào)由內(nèi)核向父進(jìn)程發(fā)送異步通知.父進(jìn)程可以選擇忽略該信號(hào),也可以提供一個(gè)信號(hào)處理函數(shù):

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#define MAX_COUNT 200

void catchChild(int sigNumber);
void doChild(void); 
void doParent(void);

void main(void) {
    signal(SIGCHLD, catchChild);
    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }

    sleep(1);
    printf("*** Parent exits ***\n");
    exit(0);
}

void catchChild(int sigNumber) {
    int childStatus;
    pid_t pid = wait(&childStatus);
    printf("*** Parent detects process %d is done ***\n", pid);
}

void doChild(void) {
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    printf("*** Parent is done ***\n");
}

也可以:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#define MAX_COUNT 200

void catchChild(int sigNumber);
void doChild(void); 
void doParent(void);

void main(void) {
    struct sigaction action;
    struct sigaction oaction;
    action.sa_flags = 0;
    action.sa_handler = catchChild;
    sigaction(SIGCHLD, &action, &oaction);
    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }

    sleep(1);
    printf("*** Parent exits ***\n");
    exit(0);
}

void catchChild(int sigNumber) {
    int childStatus;
    pid_t pid = wait(&childStatus);
    printf("*** Parent detects process %d is done ***\n", pid);
}

void doChild(void) {
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    printf("*** Parent is done ***\n");
}

使用系統(tǒng)調(diào)用 exec() 執(zhí)行程序

有時(shí)候,創(chuàng)建一個(gè)子進(jìn)程,但是并不想運(yùn)行和父進(jìn)程同樣的程序.這時(shí)候可以使用 exec 家族執(zhí)行另一個(gè)二進(jìn)制文件.exec 家族函數(shù)包括: execl()execlp()execle()execv()execvp()execve().這里介紹一下 int execvp(const char *file, char *const argv[])

  • 第一個(gè)參數(shù)是一個(gè)字符串,表示可執(zhí)行文件的路徑
  • 第二個(gè)參數(shù)是傳遞給可執(zhí)行程序的參數(shù),同 int main(int argc, char **argv) 中的 argv

當(dāng) execvp() 調(diào)用時(shí),給出的二進(jìn)制程序文件會(huì)被加載進(jìn)內(nèi)存,該子進(jìn)程 fork() 出來(lái)的所有"內(nèi)存頁(yè)"和"文件描述符號(hào)碼表"都會(huì)清空.子進(jìn)程的進(jìn)程號(hào)碼不變.然后子進(jìn)程運(yùn)行新的二進(jìn)制程序.

如果 execvp() 執(zhí)行程序成功,該函數(shù)不會(huì)返回,其下面的代碼不會(huì)繼續(xù)運(yùn)行,因?yàn)橐呀?jīng)轉(zhuǎn)入新的程序中.如果執(zhí)行程序失敗,返回 -1.

exec
exec
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#define MAX_COUNT 200

void doChild(char **argv); 
void doParent(void);

void main(int argc, char **argv) {
    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild(argv);
        exit(0);
    default:
        doParent();
    }

    int status;
    pid = wait(&status);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(char **argv) {
    execvp(argv[1], NULL);
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    printf("*** Parent is done ***\n");
}

使用系統(tǒng)調(diào)用 pipe() 創(chuàng)建管道,在進(jìn)程間通信

一旦創(chuàng)建了多個(gè)進(jìn)程后,我們突然意識(shí)到?jīng)]有辦法在進(jìn)程之間通信.在多進(jìn)程的程序中,我們常常需要進(jìn)程之間共同完成一些任務(wù).在父-子進(jìn)程這樣的關(guān)系進(jìn)程,可以使用管道(匿名管道)進(jìn)行通信.

系統(tǒng)調(diào)用 int pipe(int fds[2]),創(chuàng)建一對(duì)管道,第一個(gè)用于讀,第二個(gè)用于寫,返回兩個(gè)對(duì)應(yīng)的文件描述符號(hào)碼.

管道非常受限:

  1. 每一個(gè)都是單向的,要么只能讀,要么只能寫
  2. 管道只能在有父-子關(guān)系的進(jìn)程中使用,不相關(guān)的進(jìn)程之間無(wú)法使用.

管道也有一些優(yōu)點(diǎn):.

  1. 管道中傳輸?shù)氖亲止?jié)流,寫入的順序和讀取的順序是一致的
  2. 管道中不會(huì)丟失數(shù)據(jù),除非讀取端提前關(guān)閉讀取管道
PIPE
PIPE
#include <stdio.h>
#include <stdlib.h>

#define BUFFSIZE 6

void doChild(int pipefds[2]); 
void doParent(int pipefds[2]);

void main(void) {
    int pipefds[2];
    pipe(pipefds);

    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild(pipefds);
        exit(0);
    default:
        doParent(pipefds);
    }

    int status;
    pid = wait(&status);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(int pipefds[2]) {
    close(pipefds[1]);
    char buff[BUFFSIZE];
    read(pipefds[0], buff, sizeof(buff));
    printf("Recv %s.\n", buff);
    printf("*** Child process is done ***\n");
}

void doParent(int pipefds[2]) {
    close(pipefds[0]);
    char buff[BUFFSIZE] = "Hello";
    write(pipefds[1], buff, sizeof(buff));
    printf("*** Parent is done ***\n");
}

如果 pipe() 調(diào)用成功,將會(huì)創(chuàng)建一對(duì)管道.pipefds[0] 用于讀取,pipefds[1] 用于寫入.我們首先使用 pipe() 創(chuàng)建了管道,然后調(diào)用 fork() 創(chuàng)建一個(gè)子進(jìn)程,fork() 會(huì)把 pipe() 創(chuàng)建的管道描述符做一份復(fù)制給新進(jìn)程.因此,在新進(jìn)程(子進(jìn)程)也存在同樣的"文件描述符號(hào)碼表",他們都指向操作系統(tǒng)內(nèi)核緩沖中存放的管道文件資源.另外,我們?cè)诟缸舆M(jìn)程分別關(guān)閉了不需要的"文件描述符號(hào)碼表",雖然不是必須的,但是"打開(kāi)的文件"是有上限的,盡可能的關(guān)閉不使用的總是好的.

close() 只會(huì)減少內(nèi)核緩沖中保存的"文件資源"的引用計(jì)數(shù),而不是真的關(guān)閉"文件資源".當(dāng)引用計(jì)數(shù)變?yōu)?0 的時(shí)候,才會(huì)真正關(guān)閉"文件資源".同樣的,每次 open() 或者以其他方式打開(kāi)一個(gè)"文件資源"時(shí),也會(huì)把引用計(jì)數(shù)加 1.

從上面的代碼也可以看出,管道實(shí)際上是屬于文件性質(zhì),對(duì)管道的操作跟對(duì)文件的操作是相同的.這意味著你也可以把管道設(shè)置為非阻塞的(請(qǐng)關(guān)注我的后續(xù)文字: POSIX 程序設(shè)計(jì) __ IO),可以使用文件操作的常用函數(shù).

在更復(fù)雜的程序中,你可能需要一個(gè)雙向通信的管道,那么你必須建立兩對(duì)管道:每一個(gè)進(jìn)程使用兩個(gè)端進(jìn)行讀和寫.這時(shí)候你必須要小心,因?yàn)檫@樣使用管道很容易"死鎖".我們沒(méi)有用鎖,怎么會(huì)出現(xiàn)"死鎖"?事實(shí)上,這種情況只是跟"死鎖"相同的表現(xiàn).

    進(jìn)程 A                  進(jìn)程 B

pipefds_a[1] ----------> pipefds_a[0]
pipefds_b[0] <---------- pipefds_b[1]

比如在上圖中,進(jìn)程 A 和進(jìn)程 B 分別通過(guò)一對(duì)管道進(jìn)行讀寫.

  1. 假設(shè)進(jìn)程 A 使用 pipefds_b[0,阻塞在讀取時(shí),進(jìn)程 B 也使用 pipefds_b[1] 阻塞在讀取.這時(shí)候,兩邊都沒(méi)有字節(jié)流發(fā)送,進(jìn)程 A 和進(jìn)程 B 就會(huì)一直阻塞在那里,什么都不做了.

  2. 假設(shè)進(jìn)程 A 使用 pipefds_a[1] 寫入.基于字節(jié)流的讀寫往往都是基于內(nèi)核緩沖區(qū),管道也不例外.當(dāng)創(chuàng)建管道的時(shí)候,內(nèi)核同時(shí)為管道分配了一塊內(nèi)存緩沖,管道寫入的時(shí)候會(huì)寫入到這塊內(nèi)核緩沖區(qū)上,管道讀取的時(shí)候也會(huì)從這塊內(nèi)核緩沖區(qū)讀走數(shù)據(jù).如果寫入的數(shù)據(jù)很大,內(nèi)核緩沖區(qū)已經(jīng)填滿了,那么進(jìn)程 A 就會(huì)阻塞在那,等待內(nèi)核緩沖區(qū)清空,繼續(xù)寫入.然而此時(shí),進(jìn)程 B 并沒(méi)有讀取數(shù)據(jù),也在使用 pipefds_b[1] 寫入管道的內(nèi)核緩沖區(qū),也阻塞在那里.這樣,兩個(gè)進(jìn)程也同時(shí)阻塞,什么都不做了.

建議:可能的話,把管道讀寫設(shè)置為非阻塞的.

使用系統(tǒng)調(diào)用 mkfifo() 創(chuàng)建命名管道,在進(jìn)程間通信

管道 pipe() 有一個(gè)限制:只能在父-子進(jìn)程之間使用(祖父-孫子也可以).在不相干的進(jìn)程之間通信,可以使用命名管道 FIFO.int mkfifo(const char * filename, mode_t mode) 打開(kāi)一個(gè)文件,然后進(jìn)行寫入數(shù)據(jù),寫入完畢關(guān)閉,另外一個(gè)進(jìn)程就可以打開(kāi)同一個(gè)文件進(jìn)行讀取數(shù)據(jù).

說(shuō)白了,管道是使用內(nèi)存交換數(shù)據(jù),命名管道使用一個(gè)實(shí)際的文件交換數(shù)據(jù).操作一個(gè)命名管道和操作一個(gè)普通文件非常相似.另外,命名管道不能夠同時(shí)讀和寫,要么讀,要么寫,每一個(gè)操作只能等待另一個(gè)的完成.因?yàn)槭菍?duì)文件的操作,需要有文件的讀寫權(quán)限.

從一個(gè)命名管道讀取時(shí),如果沒(méi)有數(shù)據(jù),會(huì)阻塞,而不是立刻返回 EOF(這與普通文件的讀不同).當(dāng)命名管道寫入時(shí),如果內(nèi)核緩沖區(qū)塞滿了,但是沒(méi)有消費(fèi)者(讀取),進(jìn)程會(huì)阻塞在那,直到內(nèi)核緩沖區(qū)的數(shù)據(jù)被讀取清空.

建議:可能的話,把命名管道讀寫設(shè)置為非阻塞的.

FIFO
FIFO
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

#define BUFFSIZE 6

void doChild(void); 
void doParent(void);

void main(void) {
    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }

    int status;
    pid = wait(&status);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(void) {
    mkfifo("./fifo_file", S_IFIFO | 0666);

    char buff[BUFFSIZE];
    int fd = open("./fifo_file", O_RDONLY);
    read(fd, buff, sizeof(buff));
    printf("Recv %s.\n", buff);

    printf("*** Child process is done ***\n");
}

void doParent(void) {
    mkfifo("./fifo_file", S_IFIFO | 0666);

    char buff[BUFFSIZE] = "Hello";
    int fd = open("./fifo_file", O_WRONLY);
    write(fd, buff, sizeof(buff));
    printf("Write %s.\n", buff);

    printf("*** Parent is done ***\n");
}

使用系統(tǒng)調(diào)用 socket() 創(chuàng)建套接字,在進(jìn)程間通信

這是一個(gè)廣博的話題,應(yīng)該占用單獨(dú)一個(gè)章節(jié).

SOCKET
SOCKET

這里我們單獨(dú)介紹一下 UNIX Domain Socket,這是一個(gè)非常好用的通信方式,跟管道是非常相似,甚至工作方式接近相同,但是擁有非常棒的優(yōu)點(diǎn):進(jìn)程之間可以是不相關(guān)的,通信方式是雙向的(管道是單向的).

我們可以使用普通的 int socket(int domain, int type, int protocol) 系統(tǒng)調(diào)用,并設(shè)置 domain 為 AF_UNIX,采用本機(jī)通信來(lái)建立本機(jī)多進(jìn)程之間的通信.不過(guò),UNIX 系統(tǒng)為這種場(chǎng)景提供了一個(gè)更簡(jiǎn)便可行的方案:int socketpair(int domain, int type, int protocol, int sockfd[2])

使用 socketpair(),會(huì)創(chuàng)建一對(duì)管道,每一端都可以讀寫,是雙向的,跨進(jìn)程的.除此之外,跟管道是相同的工作方式.

建議:對(duì)于進(jìn)程間通信,這是一個(gè)非常效率穩(wěn)定的方式,你也可以將其設(shè)置為非阻塞的.

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BUFFSIZE 6

void doChild(int pipefds[2]); 
void doParent(int pipefds[2]);

void main(void) {
    int pipefds[2];
    socketpair(AF_UNIX, SOCK_STREAM, 0, pipefds);

    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild(pipefds);
        exit(0);
    default:
        doParent(pipefds);
    }

    int status;
    pid = wait(&status);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(int pipefds[2]) {
    close(pipefds[1]);

    char buff[BUFFSIZE];
    read(pipefds[0], buff, sizeof(buff));
    printf("Child recv %s.\n", buff);

    char buff2[BUFFSIZE] = "OK";
    write(pipefds[0], buff2, sizeof(buff2));
    printf("Child send %s.\n", buff2);

    printf("*** Child process is done ***\n");
}

void doParent(int pipefds[2]) {
    close(pipefds[0]);

    char buff[BUFFSIZE] = "Hello";
    write(pipefds[1], buff, sizeof(buff));
    printf("Parent send %s.\n", buff); 

    char buff2[BUFFSIZE];
    read(pipefds[1], buff2, sizeof(buff2));
    printf("Parent recv %s.\n", buff2);

    printf("*** Parent is done ***\n");
}

使用系統(tǒng)調(diào)用 mmap() 創(chuàng)建內(nèi)存映射,在進(jìn)程間通信

系統(tǒng)調(diào)用 void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset) 在虛擬內(nèi)存空間創(chuàng)建一個(gè)內(nèi)存映射,映射的內(nèi)存可以與其他進(jìn)程共享:

  • 映射同一個(gè)文件同一個(gè)區(qū)域,共享相同的"物理內(nèi)存頁(yè)"
  • 通過(guò) fork() 創(chuàng)建的子進(jìn)程,會(huì)復(fù)制父進(jìn)程的"內(nèi)存頁(yè)",可以在這些內(nèi)存頁(yè)中映射共享內(nèi)存
MMAP
MMAP
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>

#define BUFFSIZE 6

void doChild(void); 
void doParent(void);

struct Data {
    pthread_mutex_t mutex;
    int state;
};

static struct Data *data;

void main(void) {
    int fd;
    pthread_mutexattr_t mattr;

    fd = open("/dev/zero", O_RDWR, 0);
    data = (struct Data *)mmap(0, sizeof(struct Data), 
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&data->mutex, NULL);
    data->state = 0;

    pid_t pid = fork();
    
    switch (pid) {
    case -1:
        perror("Counld not fork");
        exit(1);
    case 0:
        doChild();
        exit(0);
    default:
        doParent();
    }

    int status;
    pid = wait(&status);
    printf("Parent state %d\n", data->state);
    printf("*** Parent detects process %d is done ***\n", pid);
    printf("*** Parent exits ***\n");
    exit(0);
}

void doChild(void) {
    pthread_mutex_lock(&data->mutex);
    data->state = 1;
    pthread_mutex_unlock(&data->mutex);
    printf("*** Child process is done ***\n");
}

void doParent(void) {
    printf("*** Parent is done ***\n");
}

在這個(gè)程序中,我們首先定義了一個(gè)全局共享數(shù)據(jù) data,在后面的代碼中使用 mmap() 對(duì)其申請(qǐng)內(nèi)存,并映射內(nèi)存頁(yè).open() 打開(kāi)了一個(gè) /dev/zero 的設(shè)備,當(dāng)從這個(gè)設(shè)備讀取數(shù)據(jù)時(shí),獲得的是字符 \0,可以用來(lái)初始化一個(gè)文件(也可以向這個(gè)字符設(shè)備寫入,字符設(shè)備會(huì)丟棄數(shù)據(jù),什么也不做).然后我們?cè)O(shè)置共享數(shù)據(jù)的 state 為 0,初始化線程鎖,通過(guò) pthread_mutexattr_setpshared 系統(tǒng)調(diào)用設(shè)置鎖的共享屬性是進(jìn)程共享 PTHREAD_PROCESS_SHARED.然后調(diào)用 fork() 創(chuàng)建子進(jìn)程,在子進(jìn)程中修改共享數(shù)據(jù) datastate 字段為 1.最后在父進(jìn)程中 wait 等待并應(yīng)答子進(jìn)程,打印最后的共享數(shù)據(jù) datastate 值.你會(huì)發(fā)現(xiàn),state 已經(jīng)變?yōu)?1,這表示父進(jìn)程和子進(jìn)程確實(shí)共享了 data

這種數(shù)據(jù)通信,是基于內(nèi)存映射的,所以操作非常快.

更多的進(jìn)程通信

還有一些進(jìn)程通信方式,包括但不限于:

  • 消息隊(duì)列
  • 信號(hào)量
  • 共享內(nèi)存(類似于 mmap,但是可移植較差)

友情提示:這些方式無(wú)法使用 select() 或者 poll(),進(jìn)程等待消息只能通過(guò)消息隊(duì)列,不能通過(guò)其他類似通知的方式知道消息到來(lái).除非你沒(méi)有更好的法子的時(shí)候,選擇這些通信方式.

常用編程 API

pid_t fork(void)
      // 創(chuàng)建一個(gè)子進(jìn)程
pid_t wait(int *status)
      // 等待并應(yīng)答一個(gè)隨機(jī)子進(jìn)程 
pid_t waitpid(pid_t pid, int * status, int options)
      // 等待并應(yīng)答一個(gè)特定子進(jìn)程

int   execve(const char * filename, char const *argv, char const *envp)
int   execl( const char *path, const char *arg, ...)
int   execlp(const char *file, const char *arg, ...)
int   execle(const char *path, const char *arg, ..., char * const envp[])
int   execv( const char *path, char *const argv[])
int   execvp(const char *file, char *const argv[])
      // 在一個(gè)子進(jìn)程中,執(zhí)行一個(gè)二進(jìn)制可執(zhí)行文件,運(yùn)行新程序

void  exit(int status)
      // 退出進(jìn)程

pid_t getpid(void)         // 獲取當(dāng)前進(jìn)程號(hào)碼
pid_t getppid(void)        // 獲取當(dāng)前進(jìn)程的父進(jìn)程號(hào)碼
pid_t getpgrp(void)        // 獲取當(dāng)前進(jìn)程的進(jìn)程組號(hào)碼
pid_t getpgid(pid_t pid)   // 獲取特定進(jìn)程的進(jìn)程組號(hào)碼
pid_t getsid(pid_t pid)    // 獲取特定進(jìn)程的進(jìn)程組首進(jìn)程號(hào)碼

int   setpgid(pid_t pid, pid_t pgid)  
      // 設(shè)置特定進(jìn)程的進(jìn)程組號(hào)碼
pid_t setsid(void)                    
      // 使當(dāng)前進(jìn)程創(chuàng)建一個(gè)新會(huì)話,并成為新進(jìn)程組首進(jìn)程

uid_t getuid(void)         // 獲取當(dāng)前進(jìn)程的用戶號(hào)碼
uid_t geteuid(void)        // 獲取當(dāng)前進(jìn)程的有效用戶號(hào)碼
gid_t getgid(void)         // 獲取當(dāng)前進(jìn)程的用戶組號(hào)碼
gid_t getegid(void)        // 獲取當(dāng)前進(jìn)程的有效用戶組號(hào)碼

int   setuid(uid_t uid)    // 設(shè)置當(dāng)前進(jìn)程的用戶號(hào)碼
int   seteuid(uid_t uid)   // 設(shè)置當(dāng)前進(jìn)程的有效用戶號(hào)碼
int   setgid(gid_t gid)    // 設(shè)置當(dāng)前進(jìn)程的用戶組號(hào)碼
int   setegid(gid_t gid)   // 設(shè)置當(dāng)前進(jìn)程的有效用戶組號(hào)碼

int   pipe(int fds[2])
      // 創(chuàng)建一對(duì)匿名管道
int   mkfifo(const char * filename, mode_t mode)
      // 創(chuàng)建一對(duì)命名管道
int   socketpair(int domain, int type, int protocol, int sockfd[2])
      // 創(chuàng)建一對(duì)本機(jī)套接字管道
void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset)
      // 創(chuàng)建一個(gè)內(nèi)存映射

友情提示:出于演示簡(jiǎn)潔,本文字中的函數(shù)調(diào)用,大部分沒(méi)有檢查創(chuàng)建的正確性,實(shí)際項(xiàng)目中,應(yīng)該有嚴(yán)格的檢查.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,407評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,981評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,759評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,204評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,415評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,650評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,892評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,675評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • 一.管道機(jī)制(pipe) 1.Linux的fork操作 在計(jì)算機(jī)領(lǐng)域中,尤其是Unix及類Unix系統(tǒng)操作系統(tǒng)中,...
    Geeks_Liu閱讀 3,708評(píng)論 1 9
  • 又來(lái)到了一個(gè)老生常談的問(wèn)題,應(yīng)用層軟件開(kāi)發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個(gè)問(wèn)題開(kāi)始,來(lái)談?wù)劜?..
    tangsl閱讀 4,145評(píng)論 0 23
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,785評(píng)論 18 139
  • 今天去聽(tīng)講了孩子一年級(jí)的公開(kāi)課,一節(jié)課下來(lái),仔細(xì)聆聽(tīng)老師的講課方法突然恍然大悟,這些不就是學(xué)習(xí)最核心的一些方法嗎?...
    遠(yuǎn)山歸舟閱讀 846評(píng)論 2 14
  • 有沒(méi)有那么一個(gè)人,即使已經(jīng)分開(kāi)了很久,如今走到大街上,看到有穿著藍(lán)色或者白色襯衫的清瘦身影,眼睛就會(huì)不由自主的去追...
    箋心抒閱讀 298評(píng)論 1 1