TCP/IP網(wǎng)絡(luò)編程之進程與進程間通信
進程間通信基本概念
進程間通信意味著兩個不同進程間可以交換數(shù)據(jù),為了完成這一點,操作系統(tǒng)中應提供兩個進程可以同時訪問的內(nèi)存空間。但我們知道,進程具有完全獨立的內(nèi)存結(jié)構(gòu),就連通過fork函數(shù)創(chuàng)建的子進程也不會和父進程共享內(nèi)存,因此,進程間通信只能通過其他特殊方法完成。
基于管道實現(xiàn)進程間通信
圖1-1表示基于管道(PIPE)的進程間通信結(jié)構(gòu)模型

圖1-1 基于管道的進程間通信模型
從圖1-1可以看到,為了完成進程間通信,需要創(chuàng)建管道。管道并非屬于進程資源,而是和套接字一樣,屬于操作系統(tǒng)資源(也就不是fork函數(shù)的復制對象)。下面介紹創(chuàng)建管道函數(shù)
- #include
- int pipe (int filedes[2]);//成功時返回0,失敗時返回-1
- filedes[0]:通過管道接收數(shù)據(jù)時使用的文件描述符,即管道出口
- filedes[1]:通過管道傳輸數(shù)據(jù)時使用的文件描述符,即管道入口
以長度為2的int數(shù)組地址值作為參數(shù)調(diào)用上述函數(shù)時,數(shù)組中存有兩個文件描述符,它們將被用作管道的出口和入口。父進程調(diào)用該函數(shù)時將創(chuàng)建管道,同時獲取對應于出入口的文件描述符,此時父進程可以讀寫同一管道。但父進程的目的是與子進程進行數(shù)據(jù)交換,因此需要將入口和出口中的一個文件描述符傳遞給子進程,如何完成傳遞呢?答案還是調(diào)用fork函數(shù)。
- pipe1.c
- #include <stdio.h>
- #include <unistd.h>
- #define BUF_SIZE 30
- int main(int argc, char *argv[])
- {
- int fds[2];
- char str[] = "Who are you?";
- char buf[BUF_SIZE];
- pid_t pid;
- pipe(fds);
- pid = fork();
- if (pid == 0)
- {
- write(fds[1], str, sizeof(str));
- }
- else
- {
- read(fds[0], buf, BUF_SIZE);
- puts(buf);
- }
- return 0;
- }
- 第12行:調(diào)用pipe函數(shù)創(chuàng)建管道,fds數(shù)組中保存用于I/O的文件描述符
- 第13行:接著調(diào)用fork函數(shù),子進程將同時擁有通過12行函數(shù)調(diào)用獲取的兩個文件描述符。注意!復制的并非管道,而是用于管道I/O的文件描述符。至此,父子進程同時擁有I/O文件描述符
- 第16、20行:子進程通過第16行代碼向管道傳遞字符串,父進程通過第20行代碼從管道接收字符串
編譯pipe1.c并運行
- # gcc pipe1.c -o pipe1
- # ./pipe1
- Who are you?
上述示例中的通信方法及路徑如圖1-2所示,重點在于,父子進程都可以訪問管道的I/O路徑,但子進程僅用輸入路徑,父進程僅用輸出路徑

圖1-2 示例pipe1.c的通信路徑
以上就是管道的基本原理及通信方法,應用管道時還有一部分內(nèi)容需要注意,通過雙向通信示例進一步說明
通過管道進行進程間雙向通信
下面創(chuàng)建兩個進程通過一個管道進行雙向數(shù)據(jù)交換的示例,其通信方式如圖1-3所示

圖1-3 雙向通信模型1
從圖1-3可以看出,通過一個管道可以進行雙向通信,但采用這種模型需格外小心,先給出示例,稍后再討論。
pipe2.c
- #include <stdio.h>
- #include <unistd.h>
- #define BUF_SIZE 30
- int 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;
- pipe(fds);
- pid = fork();
- if (pid == 0)
- {
- write(fds[1], str1, sizeof(str1));
- sleep(2);
- read(fds[0], buf, BUF_SIZE);
- printf("Child proc output: %s \n", buf);
- }
- else
- {
- read(fds[0], buf, BUF_SIZE);
- printf("Parent proc output: %s \n", buf);
- write(fds[1], str2, sizeof(str2));
- sleep(3);
- }
- return 0;
- }
- 第17~20行:子進程運行區(qū)域,通過第17行行傳輸數(shù)據(jù),通過第19行接收數(shù)據(jù)。第18行的sleep函數(shù)至關(guān)重要,這一點稍后再討論
- 第24~26行:父進程的運行區(qū)域,通過第24行接收數(shù)據(jù),這是為了接收第17行子進程傳輸?shù)臄?shù)據(jù)。另外通過第26行傳輸數(shù)據(jù),這些數(shù)據(jù)將被第19行的子進程接收
- 第27行:父進程先終止時會彈出命令提示符,這時子進程仍然在工作,故不會產(chǎn)生問題。這條語句主要是為了防止子進程終止前彈出命令提示符(故可刪除)
編譯pipe2.c并運行
- # gcc pipe2.c -o pipe2
- # ./pipe2
- Parent proc output: Who are you?
- Child proc output: Thank you for your message
運行結(jié)果和我們設(shè)想一致,不過如果嘗試將18行的代碼注釋后再運行,雖然這行代碼只將運行時間延遲了兩秒,但一旦注釋便會引發(fā)錯誤,是什么原因呢?
向管道傳遞數(shù)據(jù)時,先讀的進程會把數(shù)據(jù)取走。簡言之,數(shù)據(jù)進入管道后成為無主數(shù)據(jù),也就是通過read函數(shù)先讀取數(shù)據(jù)的進程將得到數(shù)據(jù),即使該進程將數(shù)據(jù)傳到了管道。因此,注釋第18行將產(chǎn)生問題,在第19行,子進程將讀回自己在第17行向管道發(fā)送的數(shù)據(jù)。結(jié)果父進程調(diào)用read函數(shù)后將無限期等待數(shù)據(jù)進入管道。
從上述示例可以看到,只用一個管道進行雙向通信并非易事,為了簡化在進行雙向通信時,既然一個管道很難完成的任務(wù),不如就讓兩個管道來一起完成?因此創(chuàng)建兩個管道,各自負責不同的數(shù)據(jù)流動即可。其過程如圖1-4所示

圖1-4 雙向通信模型2
由圖1-4可知,使用兩個管道可以解決單單通過一個管道來進行雙向通信的麻煩,下面采用上述模型來改進pipe2.c。
pipe3.c
- #include <stdio.h>
- #include <unistd.h>
- #define BUF_SIZE 30
- int 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;
- pipe(fds1), pipe(fds2);
- pid = fork();
- if (pid == 0)
- {
- write(fds1[1], str1, sizeof(str1));
- read(fds2[0], buf, BUF_SIZE);
- printf("Child proc output: %s \n", buf);
- }
- else
- {
- read(fds1[0], buf, BUF_SIZE);
- printf("Parent proc output: %s \n", buf);
- write(fds2[1], str2, sizeof(str2));
- sleep(3);
- }
- return 0;
- }
- 第13行:創(chuàng)建兩個管道
- 第17、33行:子進程可以通過數(shù)組fds1指向的管道向父進程傳輸數(shù)據(jù)
- 第18、25行:父進程可以通過數(shù)組fds2指向的管道向子進程傳輸數(shù)據(jù)
- 第26行:沒有太大的意義,只是為了延遲父進程終止的插入的代碼
編譯pipe3.c并運行
- # gcc pipe3.c -o pipe3
- # ./pipe3
- Parent proc output: Who are you?
- Child proc output: Thank you for your message
運用進程間通信
上一節(jié)學習了基于管道的進程間通信方法,接下來將其運用到網(wǎng)絡(luò)代碼中。如前所述,進程間通信與創(chuàng)建服務(wù)端并沒有直接關(guān)聯(lián),但有助于理解操作系統(tǒng)。
保存消息的回聲服務(wù)端
擴展TCP/IP網(wǎng)絡(luò)編程之多進程服務(wù)端(二)這一章的echo_mpserv.c,添加將回聲客戶端傳輸?shù)淖址葱虮4娴轿募小N覀兛梢詫⑦@個任務(wù)交給另外的進程,換言之,另行創(chuàng)建進程,從向客戶端服務(wù)的進程字符串信息。當然,該過程需要創(chuàng)建用于接收數(shù)據(jù)的管道。
下面給出示例,該示例可以與任意回聲客戶端配合運行,我們將用之前介紹過的echo_mpserv.c。
echo_storeserv.c
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <arpa/inet.h>
- #include <sys/socket.h>
- #define BUF_SIZE 100
- void error_handling(char *message);
- void read_childproc(int sig);
- int main(int argc, char *argv[])
- {
- int serv_sock, clnt_sock;
- struct sockaddr_in serv_adr, clnt_adr;
- int fds[2];
- pid_t pid;
- struct sigaction act;
- socklen_t adr_sz;
- int str_len, state;
- char buf[BUF_SIZE];
- if (argc != 2)
- {
- printf("Usage : %s <port>\n", argv[0]);
- exit(1);
- }
- act.sa_handler = read_childproc;
- sigemptyset(&act.sa_mask);
- act.sa_flags = 0;
- state = sigaction(SIGCHLD, &act, 0);
- serv_sock = socket(PF_INET, SOCK_STREAM, 0);
- 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");
- pipe(fds);
- pid = fork();
- if (pid == 0)
- {
- FILE *fp = fopen("echomsg.txt", "wt");
- char msgbuf[BUF_SIZE];
- int i, len;
- for (i = 0; i < 10; i++)
- {
- len = read(fds[0], msgbuf, BUF_SIZE);
- fwrite((void *)msgbuf, 1, len, fp);
- }
- fclose(fp);
- return 0;
- }
- while (1)
- {
- adr_sz = sizeof(clnt_adr);
- clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
- if (clnt_sock == -1)
- continue;
- else
- puts("new client connected...");
- pid = fork();
- if (pid == 0)
- {
- 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);
- }
- close(clnt_sock);
- puts("client disconnected...");
- return 0;
- }
- else
- close(clnt_sock);
- }
- close(serv_sock);
- return 0;
- }
- void read_childproc(int sig)
- {
- pid_t pid;
- int status;
- pid = waitpid(-1, &status, WNOHANG);
- printf("removed proc id: %d \n", pid);
- }
- void error_handling(char *message)
- {
- fputs(message, stderr);
- fputc('\n', stderr);
- exit(1);
- }
- 第47、48行:第47行創(chuàng)建管道,第48行創(chuàng)建負責保存文件的進程
- 第49~62行:第49行創(chuàng)建的子進程運行區(qū)域,該區(qū)域從管道出口fds[0]讀取數(shù)據(jù)并保存到文件中。另外,上述服務(wù)端并不終止運行,而是不斷向客戶端提供服務(wù)。因此,數(shù)據(jù)在文件中累計到一定程序即關(guān)閉文件,該過程通過第55行的循環(huán)完成
- 第80行:第73行通過fork函數(shù)創(chuàng)建的所有子進程將復制第47行創(chuàng)建的管道的文件描述符,因此,可以通過管道入口fds[1]傳遞字符串信息
編譯echo_storeserv.c并運行
- # gcc echo_storeserv.c -o echo_storeserv
- # ./echo_storeserv 8500
- new client connected...
- new client connected...
- client disconnected...
- removed proc id: 8647
- removed proc id: 8633
- client disconnected...
- removed proc id: 8644
運行結(jié)果echo_mpclient ONE:
- # ./echo_mpclient 127.0.0.1 8500
- Hello world!
- Message from server: Hello world!
- Hello Amy!
- Message from server: Hello Amy!
- Hello Tom!
- Message from server: Hello Tom!
- Hello Jack!
- Message from server: Hello Jack!
- Hello Rose!
- Message from server: Hello Rose!
- q
運行結(jié)果echo_mpclient TWO:
- # ./echo_mpclient 127.0.0.1 8500
- Hello Java!
- Message from server: Hello Java!
- Hello Python!
- Message from server: Hello Python!
- Hello Golang!
- Message from server: Hello Golang!
- Hello Spring!
- Message from server: Hello Spring!
- Hello Flask!
- Message from server: Hello Flask!
- q
打印echomsg.txt文件
- # cat echomsg.txt
- Hello world!
- Hello Amy!
- Hello Java!
- Hello Python!
- Hello Tom!
- Hello Jack!
- Hello Rose!
- Hello Golang!
- Hello Spring!
- Hello Flask!
如上運行結(jié)果所示,啟動多個客戶端向服務(wù)端傳輸數(shù)據(jù)時,文件中累計一定數(shù)量的字符串后(共調(diào)用十次fwrite函數(shù)),可以打開echomsg.txt存入字符串。