一.IPC机制介绍
IPC(Inter-Process Communication,进程间通信)是计算机系统中不同进程之间交换数据和同步操作的机制。由于现代计算机系统中,程序通常会由多个进程组成,这些进程可能需要相互通信以完成任务,因此IPC非常重要。
IPC主要功能:
- 数据交换:允许不同进程共享数据或传递信息。
- 同步:协调多个进程之间的操作,以避免竞态条件和资源冲突。
- 互斥:控制多个进程对共享资源的访问,确保同一时间只有一个进程能够访问资源
IPC主要机制:
- 管道(Pipes):匿名管道:用于相关进程之间的单向通信,如父子进程或兄弟进程。
命名管道(FIFO):用于不相关进程之间的双向或单向通信,具有一个路径名,可以在不同的进程之间共享。 - 消息队列(Message Queues):允许进程以消息的形式发送和接收数据,消息可以按照队列的顺序进行传递。
- 共享内存(Shared Memory):允许多个进程映射同一块物理内存区域,从而实现高速的数据共享和通信。
- 信号量(Semaphores):用于实现进程间的同步和互斥,以控制对共享资源的访问。
- 套接字(Sockets):尽管最常用于网络通信,套接字也可以用于同一台计算机上的进程间通信。
二.匿名与命名管道
1.匿名管道
管道是进程间通信的一种机制,通常用于将一个进程的输出数据传递给另一个进程的输入。管道可以分为两种主要类型:匿名管道和命名管道。
管道原理简易演示图:
上图所示,我们就将通信信道建立好了。那么我们具体应该如何编码实现呢?
pipe函数介绍:
pipe()函数创建一个管道,该管道提供了一对文件描述符:一个用于读操作,另一个用于写操作。数据从写端流向读端。
返回值:
- 成功:返回0。
- 失败:返回-1,并将errno设置为错误代码。
下面我们写一段代码用匿名管道简单实现父子进程的通信:
#include <iostream> // 包含输入输出流库
#include <cstdio> // 包含标准输入输出库
#include <string> // 包含C++字符串库
#include <cstring> // 包含C字符串处理库
#include <cstdlib> // 包含C标准库
#include <unistd.h> // 包含POSIX操作系统API
#include <sys/types.h> // 包含数据类型定义
#include <sys/wait.h> // 包含进程等待函数
#define N 2 // 定义常量N为2
#define NUM 1024 // 定义常量NUM为1024,表示缓冲区大小
using namespace std; // 使用标准命名空间
// Writer函数,向管道写数据
void Writer(int wfd)
{
string s = "i am father"; // 定义字符串s
pid_t self = getpid(); // 获取当前进程ID
int number = 0; // 定义一个计数器number,初始值为0
char buffer[NUM]; // 定义缓冲区buffer
while (true) // 无限循环
{
sleep(1); // 休眠1秒
buffer[0] = 0; // 将缓冲区第一个位置置为0
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++); // 格式化字符串并存入缓冲区
cout << buffer << endl; // 输出缓冲区内容
write(wfd, buffer, strlen(buffer)); // 将缓冲区内容写入管道
if(number>=5) // 如果计数器number大于等于5,退出循环
break;
}
}
// Reader函数,从管道读数据
void Reader(int rfd)
{
char buffer[NUM]; // 定义缓冲区buffer
while(true) // 无限循环
{
buffer[0] = 0; // 将缓冲区第一个位置置为0
ssize_t n = read(rfd, buffer, sizeof(buffer)); // 从管道读取数据存入缓冲区
if(n > 0) // 如果读取到的数据长度大于0
{
buffer[n] = 0; // 将缓冲区第n个位置置为0,表示字符串结束
cout << "child get a message[" << getpid() << "]# " << buffer << endl; // 输出读取到的内容
}
else if(n == 0) // 如果读取到的数据长度为0,表示管道已关闭
{
printf("child read file done!\n"); // 输出读取完成信息
break; // 退出循环
}
else break; // 如果读取错误,退出循环
}
}
int main()
{
int pipefd[N] = {
0}; // 定义一个数组pipefd,用于存放管道的文件描述符
int n = pipe(pipefd); // 创建管道,返回值n小于0表示创建失败
if (n < 0)
return 1; // 如果创建管道失败,返回1
pid_t id = fork(); // 创建子进程,返回值id小于0表示创建失败
if (id < 0)
return 2; // 如果创建子进程失败,返回2
if (id == 0)
{
// 子进程代码
close(pipefd[1]); // 关闭管道的写端
// IPC代码
Reader(pipefd[0]); // 调用Reader函数,从管道读取数据
close(pipefd[0]); // 关闭管道的读端
exit(0); // 退出子进程
}
// 父进程代码
close(pipefd[0]); // 关闭管道的读端
// IPC代码
Writer(pipefd[1]); // 调用Writer函数,向管道写入数据
close(pipefd[1]); // 关闭管道的写端
pid_t rid = waitpid(id, nullptr, 0); // 等待子进程结束
if(rid < 0) return 3; // 如果等待失败,返回3
sleep(5); // 休眠5秒
return 0; // 返回0,表示程序成功结束
}
运行起来后我们可以观察到,父进程每隔一秒格式化字符串输入缓冲区,并打印缓冲区内容,接着写进管道,这时子进程就能读到管道的数据,并将内容打印出来,父进程写五次后退出,并且关闭了写端,这是读端读到结尾后退出进程后被父进程等待回收。
我们再来验证些特殊情况:
void Reader(int rfd)
{
char buffer[NUM]; // 定义缓冲区buffer
int _count =0;
while(true) // 无限循环
{
buffer[0] = 0; // 将缓冲区第一个位置置为0
ssize_t n = read(rfd, buffer, sizeof(buffer)