TCP/IP网络编程(9) 进程间通信
1. IPC基本概念
进程间通信(Inter process communication, IPC)以为这两个不同进程间可以交换数据,为了完成这一点,操作系统中应该提供两个进程可以同时访问的内存空间。因此,只要有两个进程可以同时访问的内存空间,就可以通过此空间来交换数据。但是,进程具有完全独立的内存结构,即使通过fork()函数创建的子进程,也不会与父进程共享内存,因此进程间通信需要通过其他的特殊方法完成。
1.1 利用管道实现进程间通信
为完成进程之间的通信,需要创建管道,管道不属于进程的资源(即不是fork复制的对象),而是和套接字一样,属于操作系统资源,因此通过管道首实现IPC的原理是,两个进程通过操作系统提供的内存进行通信。
创建管道的方式:
#include <unistd.h> /* 成功时返回0 失败时返回-1 参数: fields[0] 通过管道接收数据时使用的文件描述符 fields[1] 通过管道传输数据时使用的文件描述符 */ int pipe(int fields[2]);
父进程调用pipe函数时将创建管道,同时获取对应于管道出入口的文件描述符,此时父进程可以读写同一管道,在调用fork创建子进程之后,子进程也将同时拥有管道出入口的文件描述符:
#include <stdio.h> #include <unistd.h> #define BUFF_SIZE 30 int main(int argc, char** argv) { int fds[2]; // 用于存储管道出入口的文件描述符 char message[] = "Hello Process\n"; char buffer[BUFF_SIZE]; if (pipe(fds) == -1) { printf("Falied to create pipe.\n"); return -1; } pid_t pid = fork(); // 创建进行 if (pid == 0) { // 子进程 写入管道 write(fds[1], message, sizeof(message)); } else { // 父进程 读取管道 read(fds[0], buffer, BUFF_SIZE); fputs(buffer, stdout); } return 0; }
进程间通信的示意图图下所示:
1.2 通过管道实现进程间双向通信
方案1:通过一个管道实现两个进程间的双向通信
代码实例:
#include <stdio.h> #include <unistd.h> #include <string.h> #define BUFF_SIZE 30 int main(int argc, char** argv) { int fds[2]; // 用于存储管道出入口的文件描述符 char message1[] = "Hello Process\n"; char message2[] = "I am vac!\n"; char buffer[BUFF_SIZE]; memset(buffer, 0, BUFF_SIZE); if (pipe(fds) == -1) { printf("Falied to create pipe.\n"); return -1; } pid_t pid = fork(); // 创建进行 if (pid == 0) { // 子进程 写入管道 write(fds[1], message1, sizeof(message1)); sleep(2); read(fds[0], buffer, BUFF_SIZE); printf("Child proc received data: %s\n", buffer); memset(buffer, 0, BUFF_SIZE); } else { // 父进程 读取管道 read(fds[0], buffer, BUFF_SIZE); printf("Parent proc received data: %s\n", buffer); write(fds[1], message2, sizeof(message2)); sleep(3); memset(buffer, 0, BUFF_SIZE); } return 0; }
运行结果:
如果将子进程中的sleep方法注释掉,再运行代码:
if (pid == 0) { // 子进程 写入管道 write(fds[1], message1, sizeof(message1)); //sleep(2); read(fds[0], buffer, BUFF_SIZE); printf("Child proc received data: %s\n", buffer); memset(buffer, 0, BUFF_SIZE); }
结果如下所示:
此时父进程无法再收到数据,因为子进程在写入数据后,未经过延时,立即又读取数据,因此管道中写入的数据被子进程提前取走。按照管道数据的读取机制,数据在写入管道之后,称为无主数据,哪个进程先通过read函数读取数据,哪个进程就先得到数据。结果导致父进程通过read函数再也读取不到数据,而将无限期等待管道数据。
注:利用一个管道进行进程间数据双向通信,程序设计者需要提前预测并控制运行流程,程序实现难度很大。
方案2:通过两个管道实现进程间相互通信
通过创建两个管道,各自在进程间负责不同的数据流,即可方便的实现进程间双向通信,采用两个管道即可避免程序运行流程的预测或控制。
代码实例:
#include <stdio.h> #include <unistd.h> #include <string.h> #define BUFF_SIZE 30 int main(int argc, char** argv) { int fds1[2]; // 子进程向父进程写数据 int fds2[2]; // 父进程向子进程写数据 char message1[] = "Hello Process\n"; char message2[] = "I am vac!\n"; char buffer[BUFF_SIZE]; memset(buffer, 0, BUFF_SIZE); if (pipe(fds1) == -1) { printf("Falied to create pipe.\n"); return -1; } if (pipe(fds2) == -1) { printf("Falied to create pipe.\n"); return -1; } pid_t pid = fork(); // 创建进行 if (pid == 0) { // 子进程 写入管道 write(fds1[1], message1, sizeof(message1)); read(fds2[0], buffer, BUFF_SIZE); printf("Child proc received data: %s\n", buffer); memset(buffer, 0, BUFF_SIZE); } else { // 父进程 读取管道 sleep(1); // 可以延时更短,稍微等待一下 read(fds1[0], buffer, BUFF_SIZE); printf("Parent proc received data: %s\n", buffer); write(fds2[1], message2, sizeof(message2)); memset(buffer, 0, BUFF_SIZE); } return 0; }
运行结果:
2. 进程间通信的应用
保存消息的回声服务器: 在向客户端提供服务的同时,服务端能够将接收到的数据同步保存到文件中。
客户端代码可复用《TCP/IP网络编程(8) 基于Linux的多进程服务器》中的客户端代码
/* 客户端 create_date: 2022-7-29 */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUFF_SIZE 30 #define ADDRESS "127.0.0.1" #define PORT 13100 void readRoutine(int sock, char* buf); void writeRoutine(int sock, char* buf); int main(int argc, char** argv) { char buffer[BUFF_SIZE]; struct sockaddr_in serverAddr; // 服务端地址 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(ADDRESS); serverAddr.sin_port = htons(PORT); memset(buffer, 0, BUFF_SIZE); int socket = ::socket(PF_INET, SOCK_STREAM, 0); if (socket == -1) { printf("Failed to init socket.\n"); return -1; } if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { printf("Failed to connect to server.\n"); return -2; } printf("Successfully connect to the server.\n"); pid_t pid = fork(); if (pid == 0) { // 子进程负责发送 writeRoutine(socket, buffer); } else { // 父进程负责接收 readRoutine(socket, buffer); } close(socket); return 0; } void readRoutine(int sock, char* buf) { while (true) { memset(buf, 0, BUFF_SIZE); int str_len = read(sock, buf, BUFF_SIZE); if (str_len == 0) // EOF { printf("Receive EOF\n"); return; } printf("\nReceive data from server: %s\n", buf); } } void writeRoutine(int sock, char* buf) { while (true) { fputs("Input message(Type Q(q) to quit): ", stdout); fgets(buf, BUFF_SIZE, stdin); if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0) { shutdown(sock, SHUT_WR); return; } write(sock, buf, strlen(buf)); } }
服务端示例代码代码示例:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <iostream> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/wait.h> void readChildProcess(int signo) { int status; pid_t pid = waitpid(-1, &status, WNOHANG); // 非阻塞 if (WIFEXITED(status)) { printf("Removed child process %d\n", WEXITSTATUS(status)); } } #define BUFF_SIZE 30 #define PORT 13100 int main(int argc, char** argv) { int serverSocket; int clientSocket; struct sockaddr_in servAddr; struct sockaddr_in clientAddr; char buffer[BUFF_SIZE]; memset(buffer, 0, BUFF_SIZE); socklen_t addrSize; int fds[2]; // 管道的文件描述符 struct sigaction sigact; sigact.sa_handler = readChildProcess; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGCHLD, &sigact, 0); // 注册信号 serverSocket = socket(PF_INET, SOCK_STREAM, 0); if (serverSocket == -1) { printf("Failed to init server socket.\n"); return -1; } memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(PORT); if (bind(serverSocket, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1) { printf("Failed to bind the server socket.\n"); return -1; } if (listen(serverSocket, 5) == -1) { printf("Failed to listen client.\n"); return -1; } // 创建管道 pipe(fds); // 创建写文件进程 pid_t pid = fork(); if (pid == 0) { // 写文件子进程 FILE* fp = fopen("data.txt", "a"); char msg[100]; while (true) { /* code */ usleep(1000*40); memset(msg, 0, 100); int len = read(fds[0], msg, 100); // 解析读取到的内容 if (len <= 5) { break; } char magicChar1 = msg[0]; char magicChar2 = msg[1]; if (magicChar1 != 'M' || magicChar2 != 'F') { break; } if (msg[2] == 1) { break; } ushort dataLen = msg[3] | (msg[4] << 8); if (dataLen <= 0) { continue; } // 将解析到的内容写入文件进行保存 fwrite(&msg[5], dataLen, 1, fp); } fclose(fp); return 0; } else { // 父进程 while (true) { printf("Waiting connect from client...\n"); addrSize = sizeof(clientAddr); clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize); if (clientSocket == -1) continue; printf("Accept new connection from %s:%d.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); pid_t processId = fork(); if (processId == 0) { // 子进程 close(serverSocket); // 接受消息 int recvLen = 0; // 构造报文 char message[100]; usleep(1000*100); while ((recvLen = read(clientSocket, buffer, BUFF_SIZE)) != 0) { /* code */ write(clientSocket, buffer, recvLen); // 向管道写数据 memset(message, 0, 100); message[0] = 'M'; message[1] = 'F'; message[2] = 0; message[3] = recvLen & 0xFF; message[4] = (recvLen >> 8) & 0xFF; memcpy(&message[5], buffer, recvLen); // 注意:memcpy里面的指针不要转换成void* write(fds[1], message, recvLen + 5); memset(buffer, 0, BUFF_SIZE); } close(clientSocket); memset(message, 0, 100); // 发送结束标志 message[0] = 'M'; message[1] = 'F'; message[2] = 1; message[3] = 0; message[4] = 0; write(fds[1], (void*)message, 5); printf("Disconnecting from server...\n"); sleep(2); return 0; } else if (processId == -1) { close(clientSocket); continue; } else { // 主进程 printf("Create new process %d for client.\n", processId); close(clientSocket); memset(&clientAddr, 0, sizeof(clientAddr)); continue; } } } close(serverSocket); return 0; } /* 服务器进程和文件进程之间设计通信格式 0 M // 报文头 1 F // 报文头 2 0/1 // 报文类型 0数据 / 1 结束 3 len // 数据长度 低位 4 len // 数据长度 高位 5 x // 数据 6 x // 数据 7 x // 数据 . // . . . */
客户端运行结果:
服务端运行结果:
通过管道进行进程间通信,将服务端收到的消息发送给写文件进程,将收到的消息保存至文件:
-----------------------------------------//end//---------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)