Linux 進程間通信:深度剖析與實戰指南

Linux 這片充滿無限可能的操作系統天地裏,進程間通信猶如一條隱祕而強大的紐帶,將一個個獨立運行的進程緊密相連,編織出複雜而精妙的軟件生態網絡。想象一下,在一個龐大的 Linux 系統中,衆多進程如同忙碌的小工匠,各自專注於特定的任務。有的進程負責處理用戶輸入,有的在後臺默默管理着系統資源,還有的在進行復雜的數據運算。然而,若沒有進程間通信,這些進程就只是孤立的個體,無法協同工作,整個系統也將陷入混亂與低效。

進程間通信賦予了進程們相互交流、共享信息、協調行動的能力。它讓數據能夠在不同的進程空間中自由穿梭,使得一個進程的成果可以爲其他進程所用,一個進程的需求能夠被其他進程感知並響應。無論是簡單的文本數據傳遞,還是複雜的共享資源同步與協作,進程間通信都提供了多樣化的解決方案。

一、簡介

Linux 進程間通信(Inter-Process Communication,IPC)是指在多道程序環境下,進程間進行數據交換和信息傳遞的一種機制或方法。在現代操作系統中,進程是系統資源分配的基本單位,不同進程之間需要相互合作和通信,才能完成各種任務。進程間通信是實現進程間協作的重要手段。

進程間通信在 Linux 系統中至關重要。每個進程在 Linux 環境下都有獨立的用戶地址空間,一般情況下,進程間的進程空間不能相互訪問。但在很多實際應用場景中,進程與進程之間需要進行通信,以共同完成特定的功能需求。例如,一個進程需要將數據發送給另一個進程進行進一步處理,或者多個進程需要共享同一個資源等。

1.1 爲什麼要通信

在軟件體系中,進程間通信的原因與人類通信有相似之處。首先,存在需求是關鍵因素。在軟件系統中,多個進程協同完成任務、服務請求或提供消息等情況時有發生。例如,在一個複雜的分佈式系統中,不同的進程可能分別負責數據採集、處理和存儲等任務,它們之間需要進行通信以確保整個系統的正常運行。其次,進程間存在隔離。每個進程都有獨立的用戶空間,互相看不到對方的內容。這就如同人與人之間如果身處不同的房間,沒有溝通渠道的話就無法交流信息。所以,爲了實現信息的傳遞和任務的協同,進程間通信就顯得尤爲必要。

通信方式與人類類似,取決於需求、通信量大小和客觀實現條件。在人類社會中,有烽火、送信鴿、寫信、發電報、打電話、發微信等多種通信方式。在軟件中,也對應着不同的進程間通信方式。比如,對於小量的即時信息傳遞,可以類比爲打電話的方式,採用信號這種通信方式;對於大量的數據傳輸,可以類比爲寫信的方式,採用消息隊列或共享內存等通信方式。

我們先拿人來做個類比,人與人之間爲什麼要通信,有兩個原因。首先是因爲你有和對方溝通的需求,如果你都不想搭理對方,那就肯定不用通信了。其次是因爲有空間隔離,如果你倆在一起,對方就站在你面前,你有話直說就行了,不需要通信。此時你非要給對方打個電話或者發個微信,是不是顯得非常奇怪、莫名其妙。如果你倆不在一塊,還有事需要溝通,此時就需要通信了。通信的方式有點烽火、送信鴿、寫信、發電報、打電話、發微信等。採取什麼樣的通信方式跟你的需求、通信量的大小、以及客觀上能否實現有關。

同樣的,軟件體系中爲什麼會有進程間通信呢?首先是因爲軟件中有這個需求,比如有些任務是由多個進程一起協同來完成的,或者一個進程對另一個進程有服務請求,或者有消息要向另一方提供。其次是因爲進程間有隔離,每個進程都有自己獨立的用戶空間,互相看不到對方,所以才需要通信。

1.2 爲什麼能通信

內核空間是共享的,雖然多個進程有多個用戶空間,但內核空間只有一個。就像一個公共的資源庫,雖然每個進程都有自己獨立的 “房間”(用戶空間),但它們都可以通過特定的通道訪問這個公共資源庫(內核空間)。

爲什麼能通信呢?那是因爲內核空間是共享的,雖然 N 個進程都有 N 個用戶空間,但是內核空間只有一個,雖然用戶空間之間是完全隔離的,但是用戶空間與內核空間並不是完全隔離的,他們之間有系統調用這個通道可以溝通。所以兩個用戶空間就可以通過內核空間這個橋樑進行溝通了。

雖然用戶空間之間完全隔離,但用戶空間與內核空間並非完全隔離,它們之間有系統調用這個通道可以溝通。Linux 使用兩級保護機制:0 級供內核使用,3 級供用戶程序使用。每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的。最高的 1GB 字節虛擬內核空間則爲所有進程以及內核所共享。內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。雖然內核空間佔據了每個虛擬空間中的最高 1GB 字節,但映射到物理內存卻總是從最低地址(0x00000000)開始。

通過一副圖講解進程間通信的原理,進程之間雖然有空間隔離,但都和內核連着,可以通過特殊的系統調用和內核溝通,從而達到和其它進程通信的目的。就像不同的房間雖然相互獨立,但都通過管道與一箇中央控制室相連。進程就如同各個房間,內核就如同中央控制室。進程雖然不能直接訪問其他進程的用戶空間,但可以通過系統調用與內核進行交互,內核再將信息傳遞給其他進程,從而實現進程間通信。例如,當一個進程需要向另一個進程發送數據時,它可以通過系統調用將數據寫入內核空間的特定區域,內核再通知目標進程從該區域讀取數據。

我們再借助一副圖來講解一下。

雖然這個圖是講進程調度的,但是大家從這個圖裏面也能看出來進程之間爲什麼要通信,因爲進程之間都是有空間隔離的,它們之間要想交流信息是沒有辦法的。但是也不是完全沒有辦法,好在它們都和內核是連着的,雖然它們不能隨意訪問內核,但是還有系統調用這個大門,進程之間可以通過一些特殊的系統調用和內核溝通從而達到和其它進程通信的目的。

二、進程間通信的框架

2.1 進程間通信機制的結構

進程間通信機制由存在於內核空間的通信中樞和存在於用戶空間的通信接口組成,兩者關係緊密。通信中樞就如同郵局或基站,爲通信提供核心機制;通信接口則像信紙或手機,爲用戶提供使用通信機制的方法。

爲了更直觀地理解進程間通信機制的結構,我們可以通過以下圖示來展示:

用戶通過通信接口讓通信中樞建立通信信道或傳遞通信信息。例如,在使用共享內存進行進程間通信時,用戶通過特定的系統調用接口(通信接口)請求內核空間的通信中樞爲其分配一塊共享內存區域,並建立起不同進程對該區域的訪問路徑。

2.2 進程間通信機制的類型

⑴共享內存式

通信中樞建立好通信信道後,通信雙方之後的通信不需要通信中樞的協助。這就如同兩個房間之間打開了一扇門,雙方可以直接通過這扇門進行交流,而不需要中間人的幫忙。

但是,由於通信信息的傳遞不需要通信中樞的協助,通信雙方需要進程間同步,以保證數據讀寫的一致性。否則,就可能出現數據踩踏或者讀到垃圾數據的情況。比如,多個進程同時對共享內存進行讀寫操作時,需要通過信號量等機制來確保在同一時間只有一個進程能夠進行寫操作,避免數據衝突。

⑵消息傳遞式

通信中樞建立好通信信道後,每次通信還都需要通信中樞的協助。這種方式就像一箇中間人在兩個房間之間傳遞信息,每次傳遞都需要經過中間人。

消息傳遞式又分爲有邊界消息和無邊界消息。無邊界消息是字節流,發過來是一個一個的字節,要靠進程自己設計如何區分消息的邊界。有邊界消息的發送和接收都是以消息爲基本單位,類似於一封封完整的信件,接收方可以明確地知道每個消息的開始和結束位置。

2.3 進程間通信機制的接口設計

按照通信雙方的關係,可分爲對稱型通信和非對稱型通信:

進程間通信機制一般要實現三類接口:

如何建立通信信道,誰去建立通信信道。對於對稱型通信來說,誰去建立通信信道無所謂,有一個人去建立就可以了,後者直接加入通信信道。對於非對稱型通信,一般是由服務端、消費者建立通信信道,客戶端、生產者則加入這個通信信道。不同的進程間通信機制,有不同的接口來創建信道。例如,在使用共享內存時,可以通過特定的系統調用(如 shmget)來創建共享內存區域,建立通信信道。

後者如何找到並加入這個通信信道。一般情況是,雙方通過提前約定好的信道名稱找到信道句柄,通過信道句柄加入通信信道。但是有的是通過繼承把信道句柄傳遞給對方,有的是通過其它進程間通信機制傳遞信道句柄,有的則是通過信道名稱直接找到信道,不需要信道句柄。

如何使用通信信道。一旦通信信道建立並加入成功,進程就需要知道如何正確地使用通信信道進行數據的讀寫操作。例如,在使用管道進行通信時,進程需要明確知道哪個文件描述符是用於讀,哪個是用於寫,以及在讀寫過程中的各種規則和特殊情況的處理。

三、Linux 中的進程間通信機制

3.1 管道(Pipe)

⑴匿名管道:匿名管道通常用於臨時的、簡單的數據傳輸,僅用於有親緣關係的進程。當使用 fork 函數創建子進程時,子進程會繼承父進程的文件描述符表。父進程通過 pipe 函數自動以讀寫的方式打開同一個管道文件,並將文件描述符返回給一個數組。其中,數組的一個元素存儲以讀的方式打開管道文件所返回的文件描述符,另一個元素存儲以寫的方式打開管道文件所返回的文件描述符。

站在文件描述符角度深度理解管道,子進程拷貝父進程後,就不需要再以讀或者寫的方式打開管道文件了。確保管道通信的單向性,父子進程要分別關閉讀端和寫端。例如,如果希望數據從父進程流向子進程,就關閉父進程的讀端,子進程的寫端;如果希望數據從子進程流向父進程,就關閉父進程的寫端,子進程的讀端。

下面是驗證管道通信的代碼示例:

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdlib.h>

#define MAX 100

int main() {
    // 創建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n == 0);
    std::cout << "pipefd[0]:" << pipefd[0] << ",pipefd[1]:" << pipefd[1] << std::endl;

    // 創建子進程
    pid_t id = fork();
    // 判斷是否創建失敗
    if (id < 0) {
        perror("fork");
        return 1;
    }
    // 子進程
    else if (id == 0) {
        close(pipefd[0]);     // 關閉讀通道
        int cnt = 10;
        while (cnt) {
            char message[MAX];
            snprintf(message, sizeof(message), "hello father, I am child, Mypid:%d, cnt: %d", getpid(), cnt);
            cnt--;
            // 將字符串 message 寫入到管道中
            write(pipefd[1], message, strlen(message));
            sleep(1);   // 讓子進程寫慢些
        }
        exit(0);
    }
    // 父進程
    close(pipefd[1]);   // 關閉寫通道
    // TODO
    char buffer[MAX];
    while (true) {
        sleep(1);
        // 從文件描述符對應的管道里讀取數據,並將數據存儲到 buffer 中
        size_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n > 0) {
            buffer[n] = 0;
            std::cout << getpid() << " -> " << "child sends: " << buffer << " to me!" << std::endl;
        } else if (n == 0) {
            std::cout << "father return val(n):" << n << std::endl;
            std::cout << "child quit, me too!!!" << std::endl;
            sleep(1);
            break;
        }
    }
    std::cout << "finish reading..." << std::endl;
    // 寫端已經退出,讀完後關閉讀端
    close(pipefd[0]);
    pid_t rid = waitpid(id, nullptr, 0);
    if (rid == id) {
        std::cout << "wait success" << std::endl;
    }
    return 0;
}

⑵有名管道 FIFO:有名管道允許不相關的進程通過文件系統中的一個路徑名進行通信。創建有名管道可以使用 mkfifo 函數,判斷有名管道是否已存在,若尚未創建,則以相應的權限創建。以下是創建有名管道和使用有名管道進行通信的代碼示例:

發送端:

#include"name_fifo.hpp"
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<limits.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>

#define MYFIFO   "/tmp/myfifo"/* 有名管道文件名*/
#define MAX_BUFFER_SIZE   PIPE_BUF/*常量PIPE_BUF 定義在於limits.h中*/

int main() {
    char buff[MAX_BUFFER_SIZE];
    int  fd;
    int  nread;
    /* 判斷有名管道是否已存在,若尚未創建,則以相應的權限創建*/
    if (access(MYFIFO, F_OK) == -1) {
        if ((mkfifo(MYFIFO, 0666) < 0) && (errno!= EEXIST)) {
            printf("Cannot create fifo file\n");
            exit(1);
        }
        /* 以只讀阻塞方式打開有名管道 */
        fd = open(MYFIFO, O_RDONLY);
        if (fd == -1) {
            printf("Open fifo file error\n");
            exit(1);
        }
        while (1) {
            memset(buff, 0, sizeof(buff));
            if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) {
                printf("Read '%s' from FIFO\n", buff);
            }
            close(fd);
            exit(0);
        }
    }
}

接收端:

#include"name_fifo.hpp"
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<limits.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>

#define MYFIFO   "/tmp/myfifo"/* 有名管道文件名*/
#define MAX_BUFFER_SIZE   PIPE_BUF/*常量PIPE_BUF 定義在於limits.h中*/

int main() {
    char buff[MAX_BUFFER_SIZE];
    int  fd;
    int  nread;
    /* 判斷有名管道是否已存在,若尚未創建,則以相應的權限創建*/
    if (access(MYFIFO, F_OK) == -1) {
        if ((mkfifo(MYFIFO, 0666) < 0) && (errno!= EEXIST)) {
            printf("Cannot create fifo file\n");
            exit(1);
        }
        /* 以只讀阻塞方式打開有名管道 */
        fd = open(MYFIFO, O_RDONLY);
        if (fd == -1) {
            printf("Open fifo file error\n");
            exit(1);
        }
        while (1) {
            memset(buff, 0, sizeof(buff));
            if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) {
                printf("Read '%s' from FIFO\n", buff);
            }
            close(fd);
            exit(0);
        }
    }
}

3.2 信號(Signals)

信號是一種軟件中斷,是操作系統用來通知進程某個事件已經發生的一種方式。可以使用 signal 函數註冊信號處理函數。以下是註冊信號處理函數的代碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

// 信號處理函數
void sighandler(int signo) {
    printf("signo==[%d]\n", signo);
}

int main() {
    // 註冊信號處理函數
    signal(SIGINT, sighandler);
    while (1) {
        sleep(10);
    }
    return 0;
}

3.3 文件(Files)

文件是一種持久化存儲機制,可用於進程間通信。寫進程將數據寫入文件,讀進程從文件中讀取數據。以下是寫進程和讀進程的代碼示例:

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

int main() {
    // 寫進程
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        perror("Error opening file for writing");
        return 1;
    }
    char *data = "Hello from write process!";
    fputs(data, fp);
    fclose(fp);

    // 讀進程
    fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("Error opening file for reading");
        return 1;
    }
    char buffer[100];
    fgets(buffer, sizeof(buffer), fp);
    printf("Read from file: %s\n", buffer);
    fclose(fp);

    return 0;
}

3.4 信號量(Semaphores)

信號量是一種計數器,用於控制對共享資源的訪問。可以使用信號量控制對共享內存的訪問。以下是使用信號量控制對共享內存訪問的代碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>

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

int set_semvalue(int sem_id) {
    union semun sem_union;
    sem_union.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
        return 0;
    return 1;
}

void del_semvalue(int sem_id) {
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore\n");
}

int semaphore_p(int sem_id) {
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        fprintf(stderr, "semaphore_p failed\n");
        return 0;
    }
    return 1;
}

int semaphore_v(int sem_id) {
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        fprintf(stderr, "semaphore_v failed\n");
        return 0;
    }
    return 1;
}

int main() {
    int shmid, sem_id;
    void *shm = NULL;
    struct shared_use_st *shared;

    // 創建共享內存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1) {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }

    // 將共享內存連接到當前進程的地址空間
    shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1) {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }

    // 新建信號量
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

    // 信號量初始化
    if (!set_semvalue(sem_id)) {
        fprintf(stderr, "init failed.\n");
        exit(EXIT_FAILURE);
    }

    // 操作共享內存
    shared = (struct shared_use_st *) shm;
    // 寫入數據
    strcpy(shared->text, "Data to be shared");
    semaphore_v(sem_id);

    // 讀取數據
    semaphore_p(sem_id);
    printf("Read from shared memory: %s\n", shared->text);

    // 刪除信號量
    del_semvalue(sem_id);

    // 把共享內存從當前進程中分離
    if (shmdt(shm) == -1) {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }

    // 刪除共享內存
    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        fprintf(stderr, "shmctl(IPC_RMID) failed");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

3.5 共享內存(Shared Memory)

共享內存允許多個進程訪問同一內存區域,是一種高效的 IPC 機制。可以使用 shmget、shmat、shmdt 和 shmctl 函數來創建共享內存、寫入數據和讀取數據。以下是創建共享內存、寫入數據和讀取數據的代碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define TEXT_SZ 2048

struct shared_use_st {
    char text[TEXT_SZ];
};

int main() {
    int shmid;
    void *shm = NULL;
    struct shared_use_st *shared;

    // 創建共享內存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1) {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }

    // 將共享內存連接到當前進程的地址空間
    shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1) {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }

    // 設置共享內存
    shared = (struct shared_use_st *) shm;

    // 寫入數據
    strcpy(shared->text, "Data to be shared");

    // 讀取數據
    printf("Read from shared memory: %s\n", shared->text);

    // 把共享內存從當前進程中分離
    if (shmdt(shm) == -1) {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }

    // 刪除共享內存
    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        fprintf(stderr, "shmctl(IPC_RMID) failed");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

3.6 消息隊列(Message Queues)

消息隊列允許進程發送和接收消息。可以使用 msgget、msgsnd 和 msgrcv 函數來創建消息隊列、發送消息和接收消息。以下是創建消息隊列、發送消息和接收消息的代碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 100

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    int msgid;
    key_t key = ftok(".", 'm');

    // 創建消息隊列
    msgid = msgget(key, 0666 | IPC_CREAT);
    if (msgid == -1) {
        perror("msgget");
        exit(1);
    }

    // 發送消息
    struct msgbuf sendbuf;
    sendbuf.mtype = 1;
    strcpy(sendbuf.mtext, "Hello from message queue!");
    if (msgsnd(msgid, &sendbuf, strlen(sendbuf.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(1);
    }

    // 接收消息
    struct msgbuf recvbuf;
    if (msgrcv(msgid, &recvbuf, MSG_SIZE, 1, 0) == -1) {
        perror("msgrcv");
        exit(1);
    }
    printf("Received message: %s\n", recvbuf.mtext);

    // 刪除消息隊列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(1);
    }

    return 0;
}

3.7 套接字(Sockets)

套接字是一種網絡通信機制,也可用於本地 IPC。可以使用 socket、bind、listen、accept 和 connect 函數來實現服務端和客戶端的通信。以下是服務端和客戶端的代碼示例:

服務端:

#include <iostream>
#include <string.h>
#include <unistd.h> // for close()
#include <arpa/inet.h> // for sockaddr_in, inet_addr
#include <sys/types.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    
    // 創建 socket 文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 將套接字綁定到端口
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 接受任何地址
    address.sin_port = htons(PORT); // 轉換爲網絡字節序

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    
    // 開始監聽
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    
    std::cout << "Server is listening on port " << PORT << std::endl;

    // 接受客戶端連接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
                             (socklen_t*)&addrlen))<0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

   // 接收數據
   read(new_socket , buffer, BUFFER_SIZE);
   std::cout << "Message from client: " << buffer << std::endl;

   const char *hello = "Hello from server";
   send(new_socket , hello , strlen(hello) , 0 );
   std::cout << "Hello message sent" << std::endl;

   close(new_socket);
   close(server_fd);

   return 0;
}

客戶端代碼:

#include <iostream>
#include <string.h>
#include <unistd.h> // for close()
#include <arpa/inet.h> // for sockaddr_in, inet_addr

#define PORT 8080

int main() {
     int sock = 0; 
     struct sockaddr_in serv_addr; 
     char *hello = "Hello from client"; 
     char buffer[1024] = {0};

     // 創建套接字文件描述符 
     if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 
         std::cerr << "\n Socket creation error \n"; 
         return -1; 
     } 

     serv_addr.sin_family = AF_INET; 
     serv_addr.sin_port = htons(PORT); 

     // 將 IPv4 地址從文本轉換爲二進制形式 
     if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) { 
         std::cerr << "\nInvalid address/ Address not supported \n"; 
         return -1; 
     } 

     // 發起連接請求  
     if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { 
         std::cerr << "\nConnection Failed \n"; 
         return -1; 
     } 

     send(sock , hello , strlen(hello) , 0 );  
     std::cout << "Hello message sent from client" << std::endl; 
    
     read(sock , buffer, sizeof(buffer));  
     std::cout << "Message from server: " << buffer << std::endl;

      close(sock);

      return 0; 
}

編譯與運行

首先,編譯服務端和客戶端代碼:g++ server.cpp -o server g++ client.cpp -o client
在一個終端中運行服務端:./server
在另一個終端中運行客戶端:./client

四、應用場景

4.1 數據傳輸與共享

文件處理與轉換:管道常被用於將一個進程的輸出作爲另一個進程的輸入,從而實現數據的傳輸與處理。比如在 Shell 腳本中,使用 ls | grep "txt" 命令,通過管道將 ls 命令列出的文件列表傳輸給 grep 命令,篩選出文件名包含 "txt" 的文件 2。

數據庫操作:多個進程可能需要共享數據庫連接或對數據庫進行協同操作。例如,一個進程負責向數據庫寫入數據,另一個進程負責從數據庫讀取數據並進行分析,它們之間可以通過共享內存或消息隊列來傳遞數據庫操作的指令和數據。

多媒體處理:在多媒體應用中,不同的進程可能負責音頻、視頻的採集、編碼、解碼、播放等不同環節。進程間通過共享內存或管道等方式傳輸音頻視頻數據,實現多媒體數據的流暢處理。

4.2 資源共享與同步

打印機等設備共享:多個進程可能需要同時訪問打印機、掃描儀等外部設備。通過信號量來控制對設備的訪問權限,確保同一時刻只有一個進程能夠使用設備,避免衝突 134。

文件鎖機制:當多個進程需要對同一個文件進行讀寫操作時,使用信號量或文件鎖來實現互斥訪問,防止數據損壞或不一致。例如,一個進程正在寫入文件時,其他進程需要等待,直到寫入操作完成。

4.3 通知與事件傳遞

進程狀態通知:父進程創建子進程後,子進程的終止、暫停等狀態變化需要及時通知父進程。信號機制常用於這種場景,子進程可以通過發送特定信號告知父進程其狀態 14。

系統事件通知:當系統發生某些事件,如磁盤空間不足、網絡連接變化等,內核會向相關進程發送信號,進程接收到信號後可以採取相應的處理措施。

4.4 任務協作與並行處理

分佈式計算:在分佈式系統中,不同的計算機節點上的進程需要協同工作來完成複雜的計算任務。消息隊列可用於在節點間傳遞任務指令和中間結果,實現任務的分發和結果的彙總。

多線程編程:在同一個進程中的多個線程之間也需要進行通信和協作。雖然線程共享進程的地址空間,但也需要通過信號量、互斥量等機制來實現同步和互斥,確保線程安全地訪問共享資源 。

4.5 構建複雜應用架構

客戶端 / 服務器模型:服務器進程和多個客戶端進程之間需要進行通信。套接字是實現這種通信的常用方式,它不僅可以用於本地進程間通信,還支持網絡通信,使得客戶端可以通過網絡連接到服務器。

微服務架構:在微服務架構中,不同的微服務進程之間需要進行高效的通信和協作。可以根據具體需求選擇合適的進程間通信方式,如消息隊列用於異步通信、HTTP 接口基於套接字實現服務間的調用等。

五、案例分析

5.1 管道通信案例

案例描述:父進程創建一個管道,然後創建子進程。父進程向管道寫入數據,子進程從管道讀取數據並打印。

代碼示例:

#include <stdio.h> 
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int pipe_fd[2];
    char data[] = "Hello from parent";
    char buffer[20];

    // 創建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        return 1;
    }

    // 創建子進程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {  // 子進程
        // 關閉寫端
        close(pipe_fd[1]);
        // 從管道讀取數據
        read(pipe_fd[0], buffer, sizeof(buffer));
        printf("Child received data: %s\n", buffer);
        // 關閉讀端
        close(pipe_fd[0]);
    } else {  // 父進程
        // 關閉讀端
        close(pipe_fd[0]);
        // 寫入數據到管道
        write(pipe_fd[1], data, sizeof(data));
        // 關閉寫端
        close(pipe_fd[1]);
        // 等待子進程結束
        wait(NULL);
    }

    return 0;
}

通過管道實現了父子進程間的單向數據傳輸,父進程寫入的數據被子進程成功讀取,管道在這種親緣關係進程間通信簡單高效,但要注意及時關閉不需要的管道端,避免資源浪費和潛在的阻塞問題。

5.2 消息隊列通信案例

案例描述:創建一個消息隊列,一個進程向消息隊列發送消息,另一個進程從消息隊列接收消息並打印。

代碼示例:

#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h> 
#include <sys/msg.h> 

struct msg_buffer {
    long msg_type;
    char msg_text[100];
};

int main() {
    key_t key;
    int msg_id;
    struct msg_buffer msg;

    // 創建唯一的key
    key = ftok("/tmp", 65);
    // 創建消息隊列
    msg_id = msgget(key, 0666 | IPC_CREAT);
    // 設置消息類型
    msg.msg_type = 1;
    // 消息內容
    strcpy(msg.msg_text, "Hello, Message Queue!");
    // 發送消息
    msgsnd(msg_id, &msg, sizeof(msg), 0);

    // 接收消息
    msgrcv(msg_id, &msg, sizeof(msg), 1, 0);
    printf("Received message: %s\n", msg.msg_text);
    // 刪除消息隊列
    msgctl(msg_id, IPC_RMID, NULL);

    return 0;
}

消息隊列允許不同進程異步通信,發送和接收進程不需要同時運行。通過消息類型可以區分不同的消息,實現有針對性的消息處理,但要注意及時刪除不再使用的消息隊列,避免資源佔用。

5.3 共享內存通信案例

案例描述:創建一塊共享內存,一個進程向共享內存寫入數據,另一個進程從共享內存讀取數據並打印。

代碼示例:

#include <stdio.h> 
#include <stdlib.h>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <string.h>

int main() {
    key_t key;
    int shm_id;
    char *shm_data;

    // 創建唯一的key
    key = ftok("/tmp", 65);
    // 創建共享內存
    shm_id = shmget(key, 1024, 0666 | IPC_CREAT);
    // 連接共享內存
    shm_data = (char *)shmat(shm_id, NULL, 0);
    // 寫入數據到共享內存
    strcpy(shm_data, "Hello, Shared Memory!");
    printf("Data written to shared memory: %s\n", shm_data);
    // 分離共享內存
    shmdt(shm_data);
    // 刪除共享內存
    shmctl(shm_id, IPC_RMID, NULL);

    return 0;
}

共享內存允許多個進程直接訪問同一塊物理內存區域,數據傳輸效率高。但需要注意進程間的同步問題,避免同時讀寫導致數據衝突。

5.4 信號通信案例

案例描述:一個進程可以向另一個進程發送自定義信號,接收進程根據接收到的信號執行相應的操作。例如,進程 A 向進程 B 發送 SIGUSR1 信號,進程 B 接收到信號後打印一條消息。

代碼示例:

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

void signal_handler(int signal_num) {
    if (signal_num == SIGUSR1) {
        printf("Received SIGUSR1 signal\n");
    }
}

int main() {
    // 註冊信號處理程序
    signal(SIGUSR1, signal_handler);
    printf("Waiting for SIGUSR1 signal...\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

通過自定義信號和信號處理函數,進程間可以實現簡單的交互和通知,適用於一些簡單的事件驅動場景,但要注意信號處理函數的編寫要儘可能簡潔,避免長時間阻塞導致系統響應問題。

5.5 套接字通信案例

案例描述:創建一個簡單的服務器進程和客戶端進程,服務器監聽指定端口,客戶端連接服務器後發送消息,服務器接收消息並回復。——代碼示例:

服務器端:

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

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_address, client_address;
    socklen_t client_address_length;
    char buffer[1024];

    // 創建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);

    // 設置服務器地址
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    server_address.sin_addr.s_addr = INADDR_ANY;

    // 綁定套接字
    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    // 監聽套接字
    listen(server_socket, 5);

    // 接受客戶端連接
    client_address_length = sizeof(client_address);
    client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_length);

    // 接收客戶端消息
    recv(client_socket, buffer, sizeof(buffer), 0);
    printf("Received message from client: %s\n", buffer);

    // 發送回覆消息
    strcpy(buffer, "Message received successfully!");
    send(client_socket, buffer, sizeof(buffer), 0);

    // 關閉套接字
    close(client_socket);
    close(server_socket);

    return 0;
}

客戶端:

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

int main() {
    int client_socket;
    struct sockaddr_in server_address;
    char buffer[1024];

    // 創建套接字
    client_socket = socket(AF_INET, SOCK_STREAM, 0);

    // 設置服務器地址
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 連接服務器
    connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    // 發送消息
    strcpy(buffer, "Hello, Server!");
    send(client_socket, buffer, sizeof(buffer), 0);

    // 接收服務器回覆
    recv(client_socket, buffer, sizeof(buffer), 0);
    printf("Received reply from server: %s\n", buffer);

    // 關閉套接字
    close(client_socket);

    return 0;
}

套接字通信不僅可以用於本地進程間通信,還支持網絡通信。通過客戶端 / 服務器模式,實現了不同主機或同一主機上不同進程間的可靠通信,但要注意網絡編程中的錯誤處理和資源管理,如套接字的正確關閉等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vvN1B6t-WbGZdK8z91l4KQ