进程间通信的概念
进程间通信(Inter Process Communication, IPC) 意味着两个不同进程间可以交换数据。为了实现这一点,操作系统中应该提供两个进程可以同时访问的内存空间。
进程具有完全独立的内存结构,连通过 fork
函数创建的子进程也不会与父进程共享内存空间。所以进程间通信只能通过其他特殊方法完成。
基于管道实现进程间通信
进程间通信是通过管道(PIPE) 实现的。管道并非属于进程的资源,而是和套接字一样,属于操作系统(也就不是 fork
函数的复制对象)。所以两个进程通过操作系统提供的内存空间进行通信。
(一)创建管道的函数 pipe()
#include <unistd.h>
//成功时返回0,失败时返回-1
int pipe(int filedes[2]);
filedes[0]
:通过管道接收数据时使用的文件描述符,即管道出口。filedes[1]
:通过管道传输数据时使用的文件描述符,即管道入口。
父进程调用该函数时将创建管道,同时获取对应于出入口的文件描述符,之后便可通过调用 fork
函数将入口或出口中的一个文件描述符传递给子进程。
(二)pipe()
函数示例
#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];
// 创建管道,调用该函数时,fds数组中将保存两个文件描述符,fds[0]表示管道出口,[1]表示管道入口
pipe(fds);
pid_t pid = fork(); // 创建进程
if(pid == 0)
{
write(fds[1], str, sizeof(str));
}
else
{
read(fds[0], buf, BUF_SIZE);
puts(buf);
}
return 0;
}
上述示例的通信路径如下图所示:
(三)通过管道进行进程间双向通信
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30
int main(int argc, char* argv[])
{
char str1[] = "who are you?";
char str2[] = "Thank you for your message.";
char buf[BUF_SIZE];
int fds[2];
pipe(fds);
pid_t pid = fork();
if(pid == 0)
{
write(fds[1], str1, sizeof(str1));
// 向管道传递数据时,先读的进程会把数据取走, 所以延迟2秒
// 否则子进程将读回自己向管道发送的数据,而父进程调用read函数后将无限期等待数据进入管道
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;
}
双向通信模型如下图:
从图中可以看出,通过1个管道可以进行双向通信,但采用这种模型时需格外注意,将第21行的 sleep
语句注释后将出错:
此时子进程将读回自己在17行向管道发送的数据,记过父进程调用 read
函数后将无限期等待数据进入管道。即:
“向管道传递数据时,先读的进程会把数据取走。”
综上,1个管道无法完成双向通信任务,因此需要创建2个管道,各自负责不同的数据流:
#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];
pipe(fds1);
pipe(fds2);
pid_t 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;
}
运用进程间通信
拓展 “TCP/IP 网络编程(九)---多进程服务器端” 的 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(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG); // waitpid函数防止僵尸进程
printf("removed proc id: %d \n", pid);
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
int state = sigaction(SIGCHLD, &act, 0);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_adr, clnt_adr;
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) // 5表示服务器可以等待接受的最大客户数量
error_handling("listen() error");
int fds[2];
pipe(fds);
pid_t pid = fork();
if(pid == 0)
{
FILE* fp = fopen("echomsg.txt", "wt");
char msgbuf[BUF_SIZE];
for(int i = 0; i < 10; i++)
{
int len = read(fds[0], msgbuf, BUF_SIZE);
// 参数:要写入数据的指针,每个数据项的大小,要写入的数据项的数量,文件流
fwrite((void*)msgbuf, 1, len, fp);
}
fclose(fp);
return 0;
}
while(1)
{
socklen_t adr_sz = sizeof(clnt_adr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock == -1)
continue;
else
puts("new client connected...");
pid_t pid = fork();
if(pid == 0)
{
close(serv_sock);
int str_len;
char buf[BUF_SIZE];
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;
}
-
信号处理:
struct sigaction act; act.sa_handler = read_childproc; sigemptyset(&act.sa_mask); act.sa_flags = 0; int state = sigaction(SIGCHLD, &act, 0);
这部分代码设置了信号处理程序,用于处理
SIGCHLD
信号,即子进程结束时产生的信号。通过waitpid()
函数防止出现僵尸进程。 -
创建管道:
int fds[2]; pipe(fds);
管道用于在进程之间传递数据。
fds[0]
是管道的读端,fds[1]
是写端。 -
创建日志子进程:
pid_t pid = fork(); if(pid == 0) { FILE* fp = fopen("echomsg.txt", "wt"); char msgbuf[BUF_SIZE]; for(int i = 0; i < 10; i++) { int len = read(fds[0], msgbuf, BUF_SIZE); fwrite((void*)msgbuf, 1, len, fp); } fclose(fp); return 0; }
这里创建了一个子进程,它从管道中读取数据并写入文件
echomsg.txt
。每次从管道读取一条消息,并将其写入文件中。 -
主循环处理客户端连接:
while(1) { socklen_t adr_sz = sizeof(clnt_adr); int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz); if(clnt_sock == -1) continue; else puts("new client connected..."); pid_t pid = fork(); if(pid == 0) { close(serv_sock); int str_len; char buf[BUF_SIZE]; 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); }
- 接受连接:
accept()
函数用于接受客户端的连接。如果连接成功,服务器输出提示信息 “new client connected...”,并创建一个子进程来处理该客户端。 - 子进程处理:子进程关闭服务器套接字
serv_sock
(因为这个套接字在子进程中不再需要),读取客户端数据,将数据回显给客户端,并通过管道fds[1]
传递数据给日志子进程。 - 父进程关闭客户端套接字:父进程在创建子进程后关闭客户端套接字
clnt_sock
,因为子进程已经接管了该连接。
- 接受连接:
文件中累计一定数量的字符串后(共10次的 fwrite
函数调用完成后),可以打开 echomsg.txt 验证保存的字符串。
问答
(一)什么是进程间通信,分别从概念上和内存的角度进行说明。
-
概念上:IPC 是多进程环境中不同进程之间进行数据交换、同步操作、和事件通知的机制。由于进程之间的独立性和隔离性,IPC 成为进程间协作的必需手段。
-
内存角度:IPC 涉及通过共享内存直接访问数据或通过内核缓冲区间接传递数据。这些机制确保进程在保持各自地址空间独立性的同时,能够有效地进行通信和协作。
(二)进程间通信需要特殊的 IPC 机制,这是由操作系统提供的。进程间通信时为何需要操作系统的帮助?
-
操作系统隔离了进程的地址空间,确保它们彼此独立和安全。
-
操作系统管理共享资源和同步机制,以避免冲突和保证数据一致性。
-
操作系统扮演中介角色,在内核空间中管理数据传输和通信。
-
操作系统提供标准接口和模型,简化了进程间通信的实现。
-
操作系统确保安全性,通过权限控制和监控来保护进程间的通信。