進程間通信基本概念
進程間通信意味著兩個不同進程間可以交換數據,為了完成這一點,操作系統中應提供兩個進程可以同時訪問的內存空間。但我們知道,進程具有完全獨立的內存結構,就連通過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
#include <arpa>
#include
#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]);/<port>
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存入字符串
更多c/c++ linux免費視頻資料獲取 後臺私信【架構】
詳情請看課程大綱
閱讀更多 Hu先生Linux後臺開發 的文章