日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)

發布時間:2023/12/18 linux 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux网络编程 - 在服务器端运用进程间通信之管道(pipe) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一 進程間通信的基本概念

1.1 對進程間通信的基本理解

進程間通信(Inter Process Communication,簡稱 IPC)

進程間通信意味著兩個不同進程間可以交換數據,為了實現這一點,操作系統內核需要提供兩個進程可以同時訪問的內存空間,即在內核中開辟一塊緩沖區。整個數據交換過程如下圖所示:

圖1? 進程間通信

從上圖 1-1 可以看出,只要有兩個進程可以同時訪問的內存空間,就可以通過此空間交換數據。但我們知道,進程具有完全獨立的內存結構,就連通過 fork 函數創建的子進程也不會與其父進程共享內存空間。因此,進程間通信只能在操作系統內核區開辟這種共享內存緩沖區。

拓展》關于進程間通信的機制請參見下面博文鏈接

Linux進程之進程間通信

二 Linux 的管道(pipe)

2.1 管道的基本概念

管道(pipe) 也稱為匿名管道,是Linux下最常見的進程間通信方式之一,它是在兩個進程之間實現一個數據流通的通道。

基于管道的進程間通信結構模型如下圖2所示。

圖2? 基于管道的進程間通信模型

?????????為了完成進程間通信,需要創建管道。管道并非屬于進程的資源,而是和套接字一樣,屬于操作系統(也就不是 fork 函數的復制對象)。所以,兩個進程通過操作系統內核提供的內存空間進行通信。

2.2 管道的特點

Linux 的管道具有以下特點:

  • 管道沒有名字,所以也稱為匿名管道。
  • 管道是半雙工的通信方式,數據只能向一個方向流動;需要雙向通信時,需要建立起兩個管道。(缺點1
  • 管道只能用在父子進程或兄弟進程之間(即具有親緣關系的進程)。(缺點2
  • 管道單獨構成一種獨立的文件系統,管道對于管道兩端的進程而言,就是一個文件,但它不是普通文件,它不屬于某種文件系統,而是自立門戶,單獨構成一種文件系統,并且只存在于內存中。
  • 數據的讀出和寫入:一個進程向管道中寫入的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,并且每次都是從緩沖區的頭部讀出數據。
  • 管道的緩沖區是有限的(管道只存在于內存中,在管道創建時,為緩沖區分配一個頁面大小)。
  • 管道中所傳遞的數據是無格式的字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式。例如,多少字節算作一個消息(或命令、記錄等)。

2.3 管道的實現方法

??????? 當一個進程創建一個管道時,Linux 系統內核為使用該管道準備了兩個文件描述符:一個用于管道的輸入(即進程寫操作),也就是在管道中寫入數據;另一個用于管道的輸出(即進程讀操作),也就是從管道中讀出數據,然后對這兩個文件描述符調用正常的系統調用(write、read函數),內核利用這種抽象機制實現了管道這一特殊操作。如下圖 3 所示。

圖3? 管道的結構
  • ?管道結構的說明

fd0:從管道中讀出數據時使用的文件描述符,即管道出口,用于進程的讀操作(read),稱為讀管道文件描述符。

fd1:向管道中寫入數據時使用的文件描述符,即管道入口,用于進程的寫操作(write),稱為寫管道文件描述符。

??????? 如果一個管道只與一個進程相聯系,可以實現進程自身內部的通信,這個一般用在進程內線程間的通信(自己遇到過)。

??????? 通常情況下,一個創建管道的進程接著就會創建子進程,由于子進程是復制父進程所有資源創建出的進程,因此子進程將從父進程那里繼承到讀寫管道的文件描述符,這樣父子進程間的通信管道就建立起來了。如下圖 4 所示。

圖4? 父進程與子進程之間的管道

《父子進程管道半雙工通信說明》

  • 父進程的 fd[0] = 子進程的 f[0],即表示這兩個文件描述符都是標識同一個管道的出口端。
  • 父進程的 fd[1] = 子進程的 f[1],即表示這兩個文件描述符都是標識同一個管道的入口端。

《父子進程數據傳輸方向》

父進程 —> 子進程的數據傳輸方向:父進程的 fd[1] —> 管道 —> 子進程的 fd[0]

子進程 —> 父進程的數據傳輸方向:子進程的 fd[1] —> 管道 —> 父進程的 fd[0]

??????? 例如,數據從父進程傳輸給子進程時,則父進程關閉讀管道的文件描述符 fd[0],子進程關閉寫管道的文件描述符 fd[1],這樣就建立了從父進程到子進程的通信管道,如下圖 5 所示。

圖5? 從父進程到子進程的管道

?2.4 管道的讀寫操作規則

??????? 在建立了一個管道之后即可通過相應的文件 I/O 操作函數(例如 read、write 等)來讀寫管道,以完成數據的傳遞過程。

??????? 需要注意的是由于管道的一端已經關閉,在進行相應的操作時,需要注意以下三個要點:

  • 如果從一個寫描述符(fd[1])關閉的管道中讀取數據,當讀完所有的數據后,read 函數返回0,表明已到達文件末尾。嚴格地說,只有當沒有數據繼續寫入后,才可以說到達了完末尾,所以應該分清楚到底是暫時沒有數據寫入,還是已經到達文件末尾,如果是前者,讀進程應該等待。若為多進程寫、單進程讀的情況將更加復雜。
  • 如果向一個讀描述符(fd[0])關閉的管道中寫數據,就會產生 SIGPIPE 信號。不管是否忽略這個信號,還是處理它,write 函數都將返回 -1。
  • 常數 PIPE_BUF 規定了內核中管道緩沖的大小,所以在寫管道中要注意一點。一次向管道中寫入 PIPE_BUF 或更少的字節數據時,不會和其他進程寫入的內容交錯;反之,當存在多個寫管道的進程時,向其中寫入超過 PIPE_BUF 個字節數據時,將會產生內容交錯現象,即覆蓋了管道中的已有數據。

三 管道的操作

3.1 管道的創建

Linux 內核提供了函數 pipe 用于創建一個管道,對其標準調用格式說明如下:

  • pipe() — 創建一個匿名管道。
#include <unistd.h>int pipe(int pipefd[2]);/*參數說明 pipefd[2]: 長度為2的文件描述符整型數組 pipefd[0]: 是管道讀出端的文件描述符,也就是說pipefd[0]只能為讀操作打開。 pipefd[1]: 是管道寫入端的文件描述符,也就是說pipefd[1]只能為寫操作打開。 *///返回值: 成功時返回0,失敗時返回-1。

【編程實例】使用 pipe 函數創建管道。在一個進程中使用管道的示例。

  • pipe.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h>#define BUF_SIZE 100int main(int argc, char *argv[]) {int fd[2];char write_buf[BUF_SIZE] = {0}; //寫緩沖區char read_buf[BUF_SIZE] = {0}; //讀緩沖區if(pipe(fd) < 0) //創建管道{printf("create pipe error!\n");exit(1);}printf("write data to pipe: ");fgets(write_buf, BUF_SIZE, stdin); //從控制臺輸入一行字符串write(fd[1], write_buf, sizeof(write_buf));read(fd[0], read_buf, sizeof(write_buf));printf("read data from pipe: %s", read_buf);printf("pipe read_fd: %d, write_fd: %d\n", fd[0], fd[1]);close(fd[0]); //關閉管道的讀出端文件描述符close(fd[1]); //關閉管道的寫入端文件描述符return 0; }
  • 運行結果

$ gcc pipe.c -o pipe
$ ./pipe
write data to pipe: This is a test!
read data from pipe: This is a test!
pipe read_fd: 3, write_fd: 4

注意》在關閉一個管道時,必須對管道的兩端都執行 close 操作,也就是說要對管道的兩個文件描述符都進行 close 操作。

3.2 通過管道實現進程間通信

????????當父進程調用 pipe 函數時將創建管道,同時獲取對應于管道出入口兩端的文件描述符,此時父進程可以讀寫同一管道,也就是本示例程序中那樣。但父進程的目的通常是與子進程進行數據交換,因此需要將管道入口或出口中的其中一個文件描述符傳遞給子進程。如何傳遞呢?答案就是調用 fork 函數。

  • 在父子進程中使用管道的詳細步驟

1、在父進程中調用 pipe 函數創建一個管道。

2、在父進程中調用 fork 函數創建一個子進程。

3、在父進程中關閉不使用的管道一端的文件描述符,然后調用對應的寫操作函數,例如 write,將對應的數據寫入管道。

4、在子進程中關閉不使用的管道一端的文件描述符,然后調用對應的讀操作函數,例如 read,將對應的數據從管道中讀出。

5、在父子進程中,調用 close 函數,關閉管道的文件描述符。

【編程實例】在父子進程中使用管道。在父進程中創建一個管道,并調用 fork 函數創建一個子進程,父進程將一行字符串數據寫入管道,在子進程中,從管道讀出這個字符串并打印出來。

  • pipe_fatherson.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>#define BUF_SIZE 100int main(int argc, char *argv[]) {int fds[2], len;pid_t pid;char buf[BUF_SIZE];if(pipe(fds) < 0){ //創建一個管道,兩個文件描述符存入fds數組中printf("pipe() error!\n");exit(1);}if((pid = fork()) < 0){ //創建一個子進程 printf("fork() error!\n");exit(1);}else if(pid > 0) //父進程執行區域{printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);close(fds[0]); //關閉父進程的管道讀出端描述符fgets(buf, BUF_SIZE, stdin); //終端輸入一行字符串數據 write(fds[1], buf, strlen(buf)); //向管道寫入數據}else //子進程執行區域{printf("Child Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);close(fds[1]); //關閉子進程的管道寫入端描述符len = read(fds[0], buf, BUF_SIZE); //從管道中讀出字符串數據buf[len] = '\0';printf("%s", buf);close(fds[0]); //關閉子進程的管道讀出端描述符}close(fds[1]); //關閉父進程的管道寫入端描述符return 0; }
  • 運行結果

$ gcc pipe_fatherson.c -o pipe_fatherson
[wxm@centos7 pipe]$ ./pipe_fatherson
Parent Proc, fds[0]=3, fds[1]=4
Child Proc, fds[0]=3, fds[1]=4
Who are you?
Who are you?

代碼說明

  • 第14行:在父進程中調用 pipe 函數創建管道,fds 數組中保存用于讀寫 I/O 的文件描述符。
  • 第18行:接著調用 fork 函數。子進程將同時擁有通過第14行 pipe 函數調用獲取的2個文件描述符,從上面的運行結果可以驗證這一點。注意!復制的并非管道,而是用于管道 I/O 的文件描述符。至此,父子進程同時擁有管道 I/O 的文件描述符。
  • 第27、33行:父進程通過第27行代碼,向管道寫入字符串;子進程通過第33行代碼,從管道接收字符串。
  • 第36、39行:第36行代碼,子進程結束運行前,關閉管道的讀出端文件描述符;第39行代碼,父進程(也是主進程)結束運行前,關閉管道的寫入端文件描述符。
  • 在兄弟進程中使用管道

??????? 在兄弟進程中使用管道進行數據通信的方法和在父子進程中類似,只是將對管道進行操作的兩個進程更換為兄弟進程即可,在父進程中則關閉該管道的 I/O 文件描述符。

編程實例】值兄弟進程中使用管道的應用實例。首先在主進程(也就是父進程)中創建一個管道和兩個子進程,然后在第1個子進程中將一個字符串通過管道發送給第2個子進程,第2個子進程從管道中讀出數據,然后將該數據輸出到屏幕上。

  • pipe_brother.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h>#define BUF_SIZE 100int main(int argc, char *argv[]) {int fds[2], len, status;pid_t pid, pid1, pid2;char buf[BUF_SIZE];if(pipe(fds) < 0){ //創建一個管道,兩個文件描述符存入fds數組中printf("pipe() error!\n");exit(1);}printf("Parent Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);if((pid1 = fork()) < 0){ //創建子進程1printf("fork() error!\n");exit(1);}else if(pid1 == 0) //子進程1執行區域{printf("Child1 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);close(fds[0]); //關閉子進程1的管道讀出端描述符fgets(buf, BUF_SIZE, stdin); //從終端中輸入字符串數據write(fds[1], buf, strlen(buf)); //向管道寫入數據close(fds[1]); //關閉子進程1的管道寫入端描述符exit(1);}if((pid2 = fork()) < 0){ //創建子進程2printf("fork() error!\n");exit(1);}else if(pid2 == 0) //子進程2執行區域{printf("Child2 Proc, fds[0]=%d, fds[1]=%d\n", fds[0], fds[1]);close(fds[1]); //關閉子進程2的管道寫入端描述符len = read(fds[0], buf, BUF_SIZE); //從管道中讀出字符串數據buf[len] = '\0';printf("%s", buf);close(fds[0]); //關閉子進程2的管道讀出端描述符exit(2);}else //父進程執行區域{int proc_num = 2; //子進程個數為2while(proc_num){while((pid = waitpid(-1, &status, WNOHANG)) == 0) //等待子進程結束{continue;}if(pid == pid1){ //結束的是子進程1printf("Child1 proc eixt, pid=%d\n", pid1);proc_num--;}else if(pid == pid2){ //結束的是子進程2printf("Child2 proc eixt, pid=%d\n", pid2);proc_num--;}if(WIFEXITED(status)) //獲取子進程退出時的狀態返回值printf("Child proc send %d\n", WEXITSTATUS(status));}}close(fds[0]); //關閉父進程的管道讀出端描述符close(fds[1]); //關閉父進程的管道寫入端描述符return 0; }
  • 運行結果

$ gcc pipe_brother.c -o pipe_brother
[wxm@centos7 pipe]$ ./pipe_brother
Parent Proc, fds[0]=3, fds[1]=4
Child1 Proc, fds[0]=3, fds[1]=4
Child2 Proc, fds[0]=3, fds[1]=4
Hello,I`m your brother!
Hello,I`m your brother!
Child1 proc eixt, pid=4679
Child proc send 1
Child2 proc eixt, pid=4680
Child proc send 2

代碼說明

  • 第54、58、62行:在父進程中調用 waitpid 函數,等待子進程的終止,如果沒有終止的子進程也不會進入阻塞狀態,而是返回0。當子進程1結束運行時,函數返回該子進程的進程ID,執行第58行的代碼;同理,當子進程2結束運行時,函數返回該子進程的進程ID,執行第62行的代碼。

3.3 通過管道實現進程間雙向通信

下面創建2個進程和1個管道進行雙向數據交換的示例,其通信方式如下圖6所示。

圖6? 管道雙向通信模型1

?從圖6可以看出,通過一個管道可以進行雙向數據通信。但采用這種模型時需格外注意。先給出示例,稍后再分析討論。

  • pipe_duplex.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h>#define BUF_SIZE 100int main(int argc, char *argv) {int fds[2];char str1[] = "Who are you?";char str2[] = "Thank you for your message";char buf[BUF_SIZE];pid_t pid, ret;ret = pipe(fds);if(ret < 0){perror("pipe() error");exit(1);}pid = fork();if(pid == 0) //子進程區域{write(fds[1], str1, sizeof(str1)); //向管道寫入字符串str1sleep(2); //讓子進程暫停2秒read(fds[0], buf, BUF_SIZE); //從管道讀出數據printf("Child proc output: %s\n", buf); //打印從管道讀出的字符串}else //父進程區域{read(fds[0], buf, BUF_SIZE); //從管道讀出數據printf("Parent porc output: %s\n", buf);write(fds[1], str2, sizeof(str2)); //向管道寫入字符串str2sleep(3); //讓父進程暫停3秒}return 0; }
  • 運行結果

$ gcc pipe_duplex.c -o pipe_duplex

$ ./pipe_duplex
Parent porc output: Who are you?
Child proc output: Thank you for your message

??????? 運行結果和我們預想的一樣:子進程向管道中寫入字符串 str1,父進程從管道中讀出該字符串;父進程向管道中寫入字符串 str2,子進程從管道中讀出該字符串。如果我們將第 27 行的代碼注釋掉,運行結果會是怎樣呢?

$ ./pipe_duplex
Child proc output: Who are you?

從上面的運行結果和進程狀態可以看出,進程 pipe_duplex 陷入了 死鎖狀態(<defunct>),產生的原因是什么呢?

向管道中傳遞數據時,先讀的進程會把管道中的數據取走。

????????數據進入管道后成為無主數據。也就是通過 read 函數先讀取數據的進程將得到數據,即使該進程將數據傳到了管道。因此,注釋掉第 27 行代碼將產生問題。在第 28 行,子進程將讀回自己在第 26 行向管道發送的數據。結果,父進程調用 read 函數后將無限期等待數據進入管道,導致進程陷入死鎖。

??????? 從上述示例中可以看到,只用一個管道進行進程間的雙向通信并非易事。為了實現這一點,程序需要預測并控制運行流程,這在每種系統中都不同,可以視為不可能完成的任務。既然如此,該如何進行雙向通信呢?

創建兩個管道。

??????? 非常簡單,一個管道無法完成雙向通信任務,因此需要創建兩個管道,各自負責不同的數據流動方向即可。其過程如下圖 7 所示。

圖7? 雙向通信模型2

???????? 由上圖 7 可知,使用兩個管道可以避免程序流程的不可預測或不可控制因素。下面采用上述模型改進 pipe_duplex.c 程序。

  • pipe_duplex2.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h>#define BUF_SIZE 100int main(int argc, char *argv) {int fds1[2], fds2[2];char str1[] = "Who are you?";char str2[] = "Thank you for your message";char buf[BUF_SIZE];pid_t pid, ret;ret = pipe(fds1); //創建管道1if(ret < 0){perror("pipe() error");exit(1);}ret = pipe(fds2); //創建管道2if(ret < 0){perror("pipe() error");exit(1);}pid = fork();if(pid == 0) //子進程區域{write(fds1[1], str1, sizeof(str1)); //向管道1寫入字符串str1read(fds2[0], buf, BUF_SIZE); //從管道2讀出數據printf("Child proc output: %s\n", buf); //打印從管道讀出的字符串}else //父進程區域{read(fds1[0], buf, BUF_SIZE); //從管道1讀出數據printf("Parent porc output: %s\n", buf);write(fds2[1], str2, sizeof(str2)); //向管道2寫入字符串str2sleep(3); //讓父進程暫停3秒}return 0; }
  • 運行結果

$ gcc pipe_duplex2.c -o pipe_duplex2

$ ./pipe_duplex2
Parent porc output: Who are you?
Child proc output: Thank you for your message

  • 程序說明

1、子進程 ——> 父進程:通過數組 fds1 指向的管道1進行數據交互。

2、父進程 ——> 子進程:通過數組 fds2 指向的管道2進行數據交互。

四 在網絡編程中運用管道實現進程間通信

上一節我們學習了基于管道的進程間通信方法,接下來將其運用到網絡編程代碼中。

4.1 保存消息的回聲服務器端

下面我們擴展上一篇博文中的服務器端程序 echo_mpserv.c,添加如下功能:

將回聲客戶端傳輸的字符串按序保存到文件中。

????????我們將這個功能任務委托給另外的進程。換言之,另行創建進程,從向客戶端提供服務的進程讀取字符串信息。這就涉及到進程間通信的問題。為此,我們可以使用上面講過的管道來實現進程間通信過程。下面給出示例程序。該示例可以與任意回聲客戶端配合運行,但我們將使用前一篇博文中介紹過的 echo_mpclient.c

提示】服務器端程序 echo_mpserv.c 和 客戶端程序 echo_mpclient.c,請參見下面的博文鏈接獲取。

Linux網絡編程 - 多進程服務器端(2)

  • echo_storeserv.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <signal.h> #include <sys/wait.h>#define BUF_SIZE 1024void read_childproc(int sig); void error_handling(char *message);int main(int argc, char *argv[]) {int serv_sock, clnt_sock;struct sockaddr_in serv_adr; //服務器端地址信息變量struct sockaddr_in clnt_adr; //客戶端地址信息變量int fds[2]; //管道兩端的文件描述符socklen_t clnt_adr_sz;pid_t pid;struct sigaction act;char buf[BUF_SIZE] = {0};int str_len, state;if(argc!=2) {printf("Usage: %s <port>\n", argv[0]);exit(1);}//初始化sigaction結構體變量actact.sa_handler = read_childproc;sigemptyset(&act.sa_mask);act.sa_flags = 0;state = sigaction(SIGCHLD, &act, NULL); //注冊SIGCHLD信號的信號處理函數serv_sock=socket(PF_INET, SOCK_STREAM, 0);if(serv_sock==-1)error_handling("socket() error");memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");//@override-添加將接收到的字符串數據保存到文件中的功能代碼pipe(fds);pid = fork(); //創建子進程1if(pid == 0) //子進程1運行區域{FILE *fp = fopen("echomsg.txt", "wt");char msgbuf[BUF_SIZE];int i, len;for(i=0; i<10; i++) //累計10次后關閉文件{len = read(fds[0], msgbuf, BUF_SIZE); //從管道讀出字符串數據fwrite(msgbuf, 1, len, fp); //將msgbuf緩沖區數據寫入打開的文件中}fclose(fp);close(fds[0]);close(fds[1]);return 1;}while(1){clnt_adr_sz = sizeof(clnt_adr);clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);if(clnt_sock == -1){continue;}elseprintf("New client connected from address[%s:%d], conn_id=%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port), clnt_sock);pid = fork(); //創建子進程2if(pid == -1){close(clnt_sock);continue;}else if(pid == 0) //子進程2運行區域{close(serv_sock);while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0){write(clnt_sock, buf, str_len); //接收客戶端發來的字符串write(fds[1], buf, str_len); //向管道寫入字符串數據}printf("client[%s:%d] disconnected, conn_id=%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port), clnt_sock);close(clnt_sock);close(fds[0]);close(fds[1]);return 2;}else{printf("New child proc ID: %d\n", pid);close(clnt_sock);}}close(serv_sock); //關閉服務器端的監聽套接字close(fds[0]); //關閉管道的讀出端close(fds[1]); //關閉管道的寫入端return 0; }void read_childproc(int sig) {pid_t pid;int status;pid = waitpid(-1, &status, WNOHANG); //等待子進程退出printf("remove proc id: %d\n", pid); }void error_handling(char *message) {fputs(message, stderr);fputc('\n', stderr);exit(1); }
  • 代碼說明
  • 第55、56行:第55行創建管道,第56行創建負責保存數據到文件中的子進程。
  • 第57~72行:這部分代碼是第56行創建的子進程運行區域。該代碼執行區域從管道出口端 fds[0] 讀取數據并保存到文件中。另外,上述服務器端并不終止運行,而是不斷向客戶端提供服務。因此,數據在文件中累計到一定程度即關閉文件,該過程通過第63行的 for 循環完成。
  • 第99行:第87行通過 fork 函數創建的子進程將復制第55行創建的管道的文件描述符數組 fds。因此,可以通過管道入口端 fds[1] 向管道傳遞字符串數據。
  • 運行結果
  • 服務器端:echo_storeserv.c

$ gcc echo_storeserv.c -o storeserv
[wxm@centos7 echo_tcp]$ ./storeserv 9190
New client connected from address[127.0.0.1:60534], conn_id=6
New child proc ID: 5589
New client connected from address[127.0.0.1:60536], conn_id=6
New child proc ID: 5592
remove proc id: 5586
client[127.0.0.1:60534] disconnected, conn_id=6
remove proc id: 5589
client[127.0.0.1:60536] disconnected, conn_id=6
remove proc id: 5592

  • 客戶端1:echo_mpclient.c

$ ./mpclient 127.0.0.1 9190
Connected...........
One
Message from server: One
Three
Message from server: Three
Five
Message from server: Five
Seven
Message from server: Seven
Nine
Message from server: Nine
Q

[wxm@centos7 echo_tcp]$

  • 客戶端2:echo_mpclient.c

$ ./mpclient 127.0.0.1 9190
Connected...........
Two
Message from server: Two
Four
Message from server: Four
Six
Message from server: Six
Eight
Message from server: Eight
Ten
Message from server: Ten
Q
[wxm@centos7 echo_tcp]$

  • 查看 echomsg.txt 文件內容

[wxm@centos7 echo_tcp]$ cat echomsg.txt
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
[wxm@centos7 echo_tcp]$

提示》觀察示例 echo_storeserv.c 后,可以發現在 main 函數中,代碼內容太長,有點影響代碼閱讀和理解。我們其實可以嘗試針對一部分功能以函數為模塊單位重構代碼,有興趣的話,可以試一試,讓代碼結構更加緊湊、美觀。

五 多進程并發服務器端總結

??????? 前面我們已經實現了多進程并發服務器端模型,但它只是并發服務器模型中的其中之一。如果我們有如下的想法:

我想利用進程和管道編寫聊天室程序,使多個客戶端進行對話,應該從哪著手呢?

??????? 若想僅用進程和管道構建具有復雜功能的服務器端,程序員需要具備熟練的編程技術和經驗。因此,初學者應用該模型擴展程序并非易事,希望大家不要過于拘泥。以后要說明的另外兩種并發服務器端模型在功能上更加強大,同時更容易實現我們的想法。

??????? 在實際網絡編程開發項目中,幾乎不會用到多進程并發服務器端模型,因為它并不是一種高效的并發服務器模型,不適合實際應用場景。即使我們在實際開發項目中不會利用多進程模型構建服務器端,但這些內容我們還是有必要學習和掌握的。

????????最后跟大家分享一句他人的一條學習編程經驗之談:“即使開始時只需學習必要部分,但最后也會需要掌握所有的內容。

提示》另外兩種比較高效的并發服務器端模型為:I/O 復用、多線程服務器端。

六 習題

1、什么是進程間通信?分別從概念上和內存的角度進行說明。

:從概念上講,進程間通信是指兩個進程之間交換數據的過程。從內存的角度上講,就是兩個進程共享的內存,通過這個共享的內存區域,可以進行數據交換,而這個共享的內存區域是在操作系統內核區中開辟的。

2、進程間通信需要特殊的IPC機制,這是由操作系統提供的。進程間通信時為何需要操作系統的幫助?

:兩個進程之間要想交換數據,需要一塊共享的內存,但由于每個進程的地址空間都是相互獨立的,因此需要操作系統的幫助。也就是說,兩個進程共享的內存空間必須由操作系統來提供。

3、“管道”是典型的IPC技術。關于管道,請回答如下問題。

a. 管道是進程間交換數據的路徑。如何創建該路徑? 由誰創建?

b. 為了完成進程間通信,2個進程需同時連接管道。那2個進程如何連接到同一管道?

c. 管道允許進行2個進程間的雙向通信。雙向通信中需要注意哪些內容?

  • a:在父進程(或主進程)中調用 pipe 函數創建管道。實際管道的創建主體是操作系統,管道不是屬于進程的資源,而是屬于操作系統的資源。
  • b:pipe 函數通過傳入參數返回管道的出入口兩端的文件描述符。當調用 fork 函數創建子進程時,這兩個文件描述符會被復制到子進程中,因此,父子進程可以同時訪問同一管道。
  • c:數據進入管道后就變成了無主數據。因此,只要有數據流入管道,任何進程都可以讀取數據。因此,要合理安排管道中數據的寫入和讀出順序。

4、編寫示例復習IPC技術,使2個進程相互交換3次字符串。當然,這兩個進程應具有父子關系,各位可指定任意字符串。

:問題剖析:兩個父子進程要互相交換數據,可以通過管道方式實現進程間通信,而通過創建兩個管道可以實現進程間的雙向通信。我們假設是子進程先向父進程發送消息,然后父進程回復消息,如此往復3次后結束運行。

  • pipe_procipc.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>#define BUF_SIZE 30 #define N 3int main(int argc, char *argv[]) {int fds1[2], fds2[2];pid_t pid;char buf[BUF_SIZE] = {0};int i, len;pipe(fds1); //創建管道1pipe(fds2); //創建管道2pid = fork(); //創建子進程if(pid == 0) //子進程執行區域{for(i=0; i<N; i++){printf("Child send message: ");fgets(buf, BUF_SIZE, stdin);write(fds1[1], buf, strlen(buf)); //向管道1中寫入字符串len = read(fds2[0], buf, BUF_SIZE); //從管道2中讀出字符串buf[len] = '\0'; //添加字符串結束符'\0'printf("Child recv message: %s\n", buf);}close(fds1[0]); close(fds1[1]);close(fds2[0]); close(fds2[1]);return 1;}else //父進程執行區域{for(i=0; i<N; i++){len = read(fds1[0], buf, BUF_SIZE); //從管道1中讀出字符串buf[len] = '\0'; //添加字符串結束符'\0'printf("Parent recv message: %s", buf); printf("Parent resp message: ");fgets(buf, BUF_SIZE, stdin);write(fds2[1], buf, strlen(buf)); //向管道2中寫入字符串}}close(fds1[0]); close(fds1[1]);close(fds2[0]); close(fds2[1]);return 0; }
  • 運行結果

$ gcc pipe_procipc.c -o pipe_procipc
[wxm@centos7 pipe]$ ./pipe_procipc
Child send message: Hi,I`m child proc
Parent recv message: Hi,I`m child proc
Parent resp message: Hi,I`m parent proc
Child recv message: Hi,I`m parent proc

Child send message: Nice to meet you
Parent recv message: Nice to meet you
Parent resp message: Nice to meet you, too
Child recv message: Nice to meet you, too

Child send message: Good bye!
Parent recv message: Good bye!
Parent resp message: Bye bye!
Child recv message: Bye bye!

[wxm@centos7 pipe]$

參考

《TCP-IP網絡編程(尹圣雨)》第11章 - 進程間通信

《Linux C編程從基礎到實踐(程國鋼、張玉蘭)》第9章 - Linux的進程同步機制——管道和IPC

《TCP/IP網絡編程》課后練習答案第一部分11~14章 尹圣雨

總結

以上是生活随笔為你收集整理的Linux网络编程 - 在服务器端运用进程间通信之管道(pipe)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。