進程間通信基本概念
進程間通信意味着兩個不同進程間可以交換數據,為了完成這一點,操作系統中應提供兩個進程可以同時訪問的內存空間。但我們知道,進程具有完全獨立的內存結構,就連通過fork函數創建的子進程也不會和父進程共享內存,因此,進程間通信只能通過其他特殊方法完成
基於管道實現進程間通信
圖1-1表示基於管道(PIPE)的進程間通信結構模型
圖1-1 基於管道的進程間通信模型
從圖1-1可以看到,為了完成進程間通信,需要創建管道。管道並非屬於進程資源,而是和套接字一樣,屬於操作系統資源(也就不是fork函數的復制對象)。下面介紹創建管道函數
#include <unistd.h> int pipe (int filedes[2]);//成功時返回0,失敗時返回-1
- filedes[0]:通過管道接收數據時使用的文件描述符,即管道出口
- filedes[1]:通過管道傳輸數據時使用的文件描述符,即管道入口
以長度為2的int數組地址值作為參數調用上述函數時,數組中存有兩個文件描述符,它們將被用作管道的出口和入口。父進程調用該函數時將創建管道,同時獲取對應於出入口的文件描述符,此時父進程可以讀寫同一管道。但父進程的目的是與子進程進行數據交換,因此需要將入口和出口中的一個文件描述符傳遞給子進程,如何完成傳遞呢?答案還是調用fork函數
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行:調用pipe函數創建管道,fds數組中保存用於I/O的文件描述符
- 第13行:接着調用fork函數,子進程將同時擁有通過12行函數調用獲取的兩個文件描述符。注意!復制的並非管道,而是用於管道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的通信路徑
以上就是管道的基本原理及通信方法,應用管道時還有一部分內容需要注意,通過雙向通信示例進一步說明
通過管道進行進程間雙向通信
下面創建兩個進程通過一個管道進行雙向數據交換的示例,其通信方式如圖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行:子進程運行區域,通過第17行行傳輸數據,通過第19行接收數據。第18行的sleep函數至關重要,這一點稍后再討論
- 第24~26行:父進程的運行區域,通過第24行接收數據,這是為了接收第17行子進程傳輸的數據。另外通過第26行傳輸數據,這些數據將被第19行的子進程接收
- 第27行:父進程先終止時會彈出命令提示符,這時子進程仍然在工作,故不會產生問題。這條語句主要是為了防止子進程終止前彈出命令提示符(故可刪除)
編譯pipe2.c並運行
# gcc pipe2.c -o pipe2 # ./pipe2 Parent proc output: Who are you? Child proc output: Thank you for your message
運行結果和我們設想一致,不過如果嘗試將18行的代碼注釋后再運行,雖然這行代碼只將運行時間延遲了兩秒,但一旦注釋便會引發錯誤,是什么原因呢?
向管道傳遞數據時,先讀的進程會把數據取走。簡言之,數據進入管道后成為無主數據,也就是通過read函數先讀取數據的進程將得到數據,即使該進程將數據傳到了管道。因此,注釋第18行將產生問題,在第19行,子進程將讀回自己在第17行向管道發送的數據。結果父進程調用read函數后將無限期等待數據進入管道
從上述示例可以看到,只用一個管道進行雙向通信並非易事,為了簡化在進行雙向通信時,既然一個管道很難完成的任務,不如就讓兩個管道來一起完成?因此創建兩個管道,各自負責不同的數據流動即可。其過程如圖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行:創建兩個管道
- 第17、33行:子進程可以通過數組fds1指向的管道向父進程傳輸數據
- 第18、25行:父進程可以通過數組fds2指向的管道向子進程傳輸數據
- 第26行:沒有太大的意義,只是為了延遲父進程終止的插入的代碼
編譯pipe3.c並運行
# gcc pipe3.c -o pipe3 # ./pipe3 Parent proc output: Who are you? Child proc output: Thank you for your message
運用進程間通信
上一節學習了基於管道的進程間通信方法,接下來將其運用到網絡代碼中。如前所述,進程間通信與創建服務端並沒有直接關聯,但有助於理解操作系統
保存消息的回聲服務端
擴展TCP/IP網絡編程之多進程服務端(二)這一章的echo_mpserv.c,添加將回聲客戶端傳輸的字符串按序保存到文件中。我們可以將這個任務交給另外的進程,換言之,另行創建進程,從向客戶端服務的進程字符串信息。當然,該過程需要創建用於接收數據的管道
下面給出示例,該示例可以與任意回聲客戶端配合運行,我們將用之前介紹過的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行創建管道,第48行創建負責保存文件的進程
- 第49~62行:第49行創建的子進程運行區域,該區域從管道出口fds[0]讀取數據並保存到文件中。另外,上述服務端並不終止運行,而是不斷向客戶端提供服務。因此,數據在文件中累計到一定程序即關閉文件,該過程通過第55行的循環完成
- 第80行:第73行通過fork函數創建的所有子進程將復制第47行創建的管道的文件描述符,因此,可以通過管道入口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
運行結果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
運行結果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!
如上運行結果所示,啟動多個客戶端向服務端傳輸數據時,文件中累計一定數量的字符串后(共調用十次fwrite函數),可以打開echomsg.txt存入字符串