CPP进程间的通讯
一、管道
在C++中,使用管道(pipe)进行进程间通信(IPC, Inter-process Communication)是一种常见的方式。管道允许一个进程将数据传递到另一个进程。管道分为两类:匿名管道和命名管道。
匿名管道
匿名管道用于父子进程之间的通信。它们在文件描述符中提供了一种双向通信机制。匿名管道没有名称,因此只能用于有亲缘关系的进程(例如父子进程之间)。
在C++中创建管道使用pipe()系统调用创建匿名管道
代码如下(示例):
int pipefd[2];
char readbuf[100];
// 创建管道
if (pipe(pipefd) == -1) {
std::cerr << "Pipe failed!" << std::endl;
return 1;
}
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
std::cerr << "Fork failed!" << std::endl;
return 1;
}
if (pid == 0) { // 子进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], readbuf, sizeof(readbuf)); // 从管道读取数据
std::cout << "Child process received: " << readbuf << std::endl;
close(pipefd[0]);
} else { // 父进程
close(pipefd[0]); // 关闭读端
const char* msg = "Hello from parent!";
write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据
close(pipefd[1]);
}
pipe(pipefd):创建一个管道,并将文件描述符放入pipefd数组,pipefd[0]是读端,pipefd[1]是写端。
由父进程写入数据,子进程读取管道中数据。
注意:
1.管道是单向通讯,因此需要两个管道进行双向通讯,或者一个管道用于父子间的通讯。
2.父进程和子进程应该分别关闭不适用的管道端。
3.管道的数据是字节流,所以要自己管理数据的格式和边界
命名管道
命名管道与匿名管道的不同之处在于,命名管道有一个名字,可以在没有父子关系的进程间进行通信。命名管道是由 mkfifo() 系统调用创建的。
代码如下(示例):
const char *fifo = "/tmp/myfifo";
// 创建命名管道
if (mkfifo(fifo, 0666) == -1) {
std::cerr << "Failed to create FIFO!" << std::endl;
return 1;
}
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
std::cerr << "Fork failed!" << std::endl;
return 1;
}
if (pid == 0) { // 子进程
char buffer[100];
int fd = open(fifo, O_RDONLY); // 打开管道进行读取
if (fd == -1) {
std::cerr << "Failed to open FIFO!" << std::endl;
return 1;
}
read(fd, buffer, sizeof(buffer)); // 读取数据
std::cout << "Child process received: " << buffer << std::endl;
close(fd);
} else { // 父进程
const char *msg = "Hello from parent through FIFO!";
int fd = open(fifo, O_WRONLY); // 打开管道进行写入
if (fd == -1) {
std::cerr << "Failed to open FIFO!" << std::endl;
return 1;
}
write(fd, msg, strlen(msg) + 1); // 写入数据
close(fd);
}
使用mkfifo()创建一个命名管道。
父进程向命名管道写入数据,子进程从命名管道读取数据。
open()用于打开命名管道,read()和weite()用于数据交换。
二、消息队列
当前使用较多的 消息队列 有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ 等,而部分 数据库 如 Redis、MySQL 以及 phxsql 也可实现消息队列的功能。
ZeroMQ:高性能、低延迟,适合实时系统和高吞吐量场景。
RabbitMQ:可靠的消息代理,适用于高可靠性和企业级消息传递。
Kafka:适合流数据处理和分布式日志收集,支持大规模消息处理。
ActiveMQ:功能丰富的消息中间件,支持多协议。
Amazon SQS:托管的消息队列服务,适合云应用。
ZeroMQ
ZeroMQ (ØMQ) 是一个高性能的消息传递库,允许进程间进行异步通信。它的最大特点是灵活性和高吞吐量,可以跨进程、甚至跨网络进行通信。ZeroMQ 的通信方式不依赖于传统的消息队列代理,它直接在应用程序之间建立连接,提供了许多模式,如:请求-响应(REQ-REP)、发布-订阅(PUB-SUB)、推送-拉取(PUSH-PULL) 等。
基本概念
Socket:ZeroMQ 使用 socket 来发送和接收消息。ZeroMQ 的 socket 与传统套接字不同,ZeroMQ 提供了很多抽象的套接字类型。
Context:ZeroMQ 使用一个上下文(zmq::context_t)来管理套接字的创建和销毁。
消息模式:ZeroMQ 提供了多种消息传递模式,如 REQ-REP、PUB-SUB、PUSH-PULL 等。
实例
使用ZeroMQ是线程进程间的通讯(PUSH-PULL),其中一个进程(发送端)推送消息到队列,另一个进程(接收端)从队列中拉取消息
1.推送端(PUSH)
推送端向ZeroMQ套接字发送消息。消息会被发送到一个队列中,等待拉取端(PULL)处理。
2.拉取端
拉取从ZeroMQ套接字中接受消息,进行相应的处理。
代码如下(示例):
推送端(PUSH)
zmq::context_t context(1); // 创建 ZeroMQ 上下文
// 创建 PUSH socket,推送消息
zmq::socket_t socket(context, ZMQ_PUSH);
socket.bind("tcp://*:5555"); // 绑定到端口 5555
std::cout << "Push server started, waiting for messages..." << std::endl;
for (int i = 0; i < 5; ++i) {
std::string message = "Message #" + std::to_string(i + 1);
zmq::message_t msg(message.size());
memcpy(msg.data(), message.c_str(), message.size());
// 发送消息
socket.send(msg, zmq::send_flags::none);
std::cout << "Sent: " << message << std::endl;
sleep(1); // 等待 1 秒,模拟发送的延迟
}
拉取端(PULL)
zmq::context_t context(1); // 创建 ZeroMQ 上下文
// 创建 PULL socket,接收消息
zmq::socket_t socket(context, ZMQ_PULL);
socket.connect("tcp://localhost:5555"); // 连接到推送端
std::cout << "Pull server started, waiting for messages..." << std::endl;
for (int i = 0; i < 5; ++i) {
zmq::message_t received_msg;
// 接收消息
socket.recv(received_msg, zmq::recv_flags::none);
std::string message(static_cast<char*>(received_msg.data()), received_msg.size());
std::cout << "Received: " << message << std::endl;
}
推送端(PUSH):
1.zmq::socket_t socket(context, ZMQ_PUSH);:创建一个 PUSH 类型的套接字,表示推送消息。
2.socket.bind(“tcp://*:5555”);:将套接字绑定到端口 5555,使其他进程可以连接到该端口。
3.socket.send(msg, zmq::send_flags::none);:发送一条消息到消息队列。
拉取端(PULL):
1.zmq::socket_t socket(context, ZMQ_PULL);:创建一个 PULL 类型的套接字,表示从队列中拉取消息。
2.socket.connect(“tcp://localhost:5555”);:连接到推送端的地址。
3.socket.recv(received_msg, zmq::recv_flags::none);:接收来自推送端的消息。
其他通讯模式
REQ-REP(请求-响应模式):
客户端通过 REQ 套接字发送请求,服务器通过 REP 套接字发送响应。
适用于客户端-服务器通信。
PUB-SUB(发布-订阅模式):
发布端通过 PUB 套接字广播消息,多个订阅端通过 SUB 套接字接收消息。
适用于广播消息或事件通知。
PUSH-PULL(推送-拉取模式):
推送端通过 PUSH 套接字发送消息,拉取端通过 PULL 套接字接收消息。
适用于任务队列和负载均衡场景。
XPUB-XSUB(扩展发布-订阅模式):
类似于 PUB-SUB,但带有更复杂的订阅控制,适用于更高级的发布-订阅模式。
RabbitMQ
RabbitMQ 是一个流行的开源消息中间件,基于 AMQP(高级消息队列协议),提供了可靠、可扩展的消息队列服务。RabbitMQ 支持多种消息传递模式,包括 点对点(Queue) 和 发布-订阅(Pub/Sub) 模式。在进程间通信时,RabbitMQ 提供了可靠的消息传递、消息确认机制和消息持久化等功能。
在 RabbitMQ 中,消息的传递通过 队列(Queue) 完成。
生产者将消息发送到交换机(Exchange)。
交换机根据路由规则将消息传递到队列中。
消费者从队列中接收并处理消息。
代码如下(示例):
生产者(Producer)代码
生产者将消息发送到 RabbitMQ 队列中。它首先创建一个连接,然后声明一个队列,最后将消息发送到该队列。
// 创建连接和通道
amqp_connection_state_t conn;
amqp_socket_t *socket = nullptr;
amqp_rpc_reply_t res;
conn = amqp_new_connection();
socket = amqp_tcp_socket_new(conn);
// 连接到 RabbitMQ 服务器
res = amqp_socket_open(socket, "localhost", 5672); // 默认 RabbitMQ 端口
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to open socket!" << std::endl;
return 1;
}
// 创建一个频道
amqp_channel_open(conn, 1);
res = amqp_get_rpc_reply(conn);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to open channel!" << std::endl;
return 1;
}
// 声明一个队列
const char *queue_name = "hello_queue";
amqp_queue_declare(conn, 1, amqp_cstring_bytes(queue_name), 0, 0, 0, 1, amqp_empty_table);
res = amqp_get_rpc_reply(conn);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to declare queue!" << std::endl;
return 1;
}
// 发送消息到队列
std::string message = "Hello from RabbitMQ!";
amqp_basic_publish(conn, 1, amqp_empty_bytes, amqp_cstring_bytes(queue_name), 0, 0, NULL, amqp_cstring_bytes(message.c_str()));
std::cout << "Sent message: " << message << std::endl;
// 关闭连接
amqp_channel_close(conn, 1, AMQP_REPLY_SUCCESS);
amqp_connection_close(conn, AMQP_REPLY_SUCCESS);
amqp_destroy_connection(conn);
消费者(Consumer)代码
消费者从队列中获取消息并处理。它首先创建一个连接和频道,然后声明队列,最后从队列中接收消息。
// 创建连接和通道
amqp_connection_state_t conn;
amqp_socket_t *socket = nullptr;
amqp_rpc_reply_t res;
conn = amqp_new_connection();
socket = amqp_tcp_socket_new(conn);
// 连接到 RabbitMQ 服务器
res = amqp_socket_open(socket, "localhost", 5672);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to open socket!" << std::endl;
return 1;
}
// 创建一个频道
amqp_channel_open(conn, 1);
res = amqp_get_rpc_reply(conn);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to open channel!" << std::endl;
return 1;
}
// 声明一个队列
const char *queue_name = "hello_queue";
amqp_queue_declare(conn, 1, amqp_cstring_bytes(queue_name), 0, 0, 0, 1, amqp_empty_table);
res = amqp_get_rpc_reply(conn);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to declare queue!" << std::endl;
return 1;
}
// 启动消费者并接收消息
amqp_basic_consume(conn, 1, amqp_cstring_bytes(queue_name), amqp_empty_bytes, 0, 1, 0, amqp_empty_table);
res = amqp_get_rpc_reply(conn);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to start consumer!" << std::endl;
return 1;
}
std::cout << "Waiting for messages..." << std::endl;
// 接收消息
while (true) {
amqp_rpc_reply_t res = amqp_consume_message(conn, &message, NULL, 0);
if (res.reply_type != AMQP_RESPONSE_NORMAL) {
std::cerr << "Failed to consume message!" << std::endl;
return 1;
}
std::string received_message(static_cast<char*>(message.body), message.bodylen);
std::cout << "Received message: " << received_message << std::endl;
// 确认消息已处理
amqp_basic_ack(conn, 1, message.delivery_tag, 0);
}
// 关闭连接
amqp_channel_close(conn, 1, AMQP_REPLY_SUCCESS);
amqp_connection_close(conn, AMQP_REPLY_SUCCESS);
amqp_destroy_connection(conn);
代码说明:
连接到 RabbitMQ:
使用 amqp_socket_open 函数连接到 RabbitMQ 服务器。
声明队列:
amqp_queue_declare 用于声明一个队列(hello_queue)。如果队列不存在,RabbitMQ 会创建它。
生产者发送消息:
amqp_basic_publish 用于将消息发布到指定队列。消息内容是 “Hello from RabbitMQ!”。
消费者接收消息:
amqp_basic_consume 用于开始消费队列中的消息。
amqp_consume_message 用于接收消息并处理。
消费者处理完消息后使用 amqp_basic_ack 向 RabbitMQ 发送确认消息,表示该消息已经处理。
关闭连接:
在消息处理完成后,通过 amqp_channel_close 和 amqp_connection_close 关闭通道和连接。
Kafka
Kafka 作为一个高吞吐量的分布式消息队列,确实可以用于进程间的通信(IPC)。它的主要特点是分布式、高可用性、可扩展性以及高性能,适用于需要高效异步消息传递的场景。通常,Kafka 更常见于 微服务架构 中的数据流、日志系统、实时分析等用途,但它也可以用于多个进程之间的通讯。
Kafka 实现进程间通信的基本流程
Producer(生产者)进程:负责将消息发送到 Kafka 中的主题(topic)。
Kafka Broker(中间件):负责存储消息,并将消息传递给需要消费消息的消费者。
Consumer(消费者)进程:从 Kafka 中的主题订阅并接收消息。
这两类进程可以在同一台机器上运行,也可以分布在不同的服务器上,Kafka 会自动处理消息的传递和存储。
消息队列有很多种需要的请再自行查看相关帮助文档
信号
信号 是一种异步通知机制,通常用于向进程发送特定事件的通知。信号是操作系统提供的一种轻量级的进程间通信方式。通过信号,一个进程可以通知另一个进程某个事件的发生,而不需要等待响应。
信号的特点:
异步性:信号的发送和接收是异步的。即发送信号的进程不需要等待接收信号的进程做出回应。
有限的信号种类:常见的信号包括 SIGINT(中断信号),SIGTERM(终止信号),SIGKILL(强制终止信号),SIGSEGV(段错误信号)等。
信号的处理:进程可以选择如何处理信号,主要有三种方式:
默认行为:操作系统定义的默认行为(例如,SIGINT 默认终止进程)。
忽略信号:进程选择忽略某些信号。
自定义处理函数:进程通过信号处理器(signal handler)来定义对特定信号的响应行为。
信号的常用应用:
中断/终止操作:例如,用户按下 Ctrl+C 时,发送 SIGINT 信号终止当前进程。
进程同步:通过发送信号通知另一个进程某个状态发生了变化,例如,生产者进程通知消费者进程数据已经准备好。
代码如下(示例):
// 信号处理函数
void signal_handler(int sig) {
printf("Received signal: %d\n", sig);
}
int main() {
// 捕获 SIGINT 信号
signal(SIGINT, signal_handler);
while(1) {
// 主进程持续运行
printf("Waiting for signals...\n");
sleep(1);
}
进程等待 SIGINT 信号(例如用户按下 Ctrl+C),并且接收到信号后,调用 signal_handler 函数进行处理。
信号量的基本原理:
二值信号量(Binary Semaphore):只有两种状态,0 或 1,类似于互斥锁。常用于实现进程间的互斥,确保同一时刻只有一个进程能访问共享资源。
计数信号量(Counting Semaphore):可以取任意非负整数值,表示允许访问资源的进程数量。当信号量的值为 0 时,表示没有可用资源;当信号量的值为正时,表示有一定数量的资源可以供进程使用。
信号量
信号量是一种用于进程同步和互斥的计数器,它可以控制对共享资源的访问,确保多个进程在访问共享资源时不会发生冲突。信号量常用于解决并发访问的问题,尤其是在多进程或多线程环境中。
信号量操作:
信号量提供两个主要操作:
P 操作(等待操作):如果信号量的值大于 0,进程将信号量的值减 1;如果信号量的值为 0,进程将被阻塞,直到信号量的值变为正数。
V 操作(释放操作):进程释放资源时,信号量的值加 1。如果有其他进程在等待信号量的值,操作系统会唤醒其中一个等待进程。
信号量的常用应用:
互斥锁:确保某一时刻只有一个进程能够访问某个共享资源。
同步:确保进程按照特定顺序执行。例如,一个进程在另一个进程完成某个任务后才能继续执行。
代码如下(示例):
sem_t sem;
void* process(void* arg) {
// 等待信号量
sem_wait(&sem);
printf("Process %ld is running...\n", (long)arg);
// 释放信号量
sem_post(&sem);
return NULL;
}
int main() {
pthread_t threads[5];
// 初始化信号量,初值为 1
sem_init(&sem, 0, 1);
// 创建多个线程
for (long i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, process, (void*)i);
}
// 等待线程结束
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
// 销毁信号量
sem_destroy(&sem);
return 0;
}
使用信号量来确保每次只有一个线程能够执行 process 函数,从而避免多个线程同时访问共享资源导致冲突。
共享内存
共享内存(Shared Memory)是进程间通信(IPC)的一种方式,它允许多个进程共享一块内存区域,进程之间可以直接读写这块内存,从而实现高效的通信。共享内存是一种非常高效的通信方式,因为它避免了数据的复制和传递,大大提高了数据交换的速度。与管道、消息队列等其他 IPC 方式相比,共享内存的开销较小,但也带来了同步和互斥的问题。
共享内存的基本概念
共享内存区:这是一个由操作系统提供的内存区域,可以被多个进程访问。每个进程都可以映射这块内存区域到自己的地址空间,并在上面读写数据。
同步问题:由于多个进程可以同时访问共享内存,必须确保数据一致性和同步性,否则会导致数据竞争和错误。通常通过信号量(semaphore)或者互斥锁(mutex)来协调对共享内存的访问。
共享内存的工作流程
创建共享内存:一个进程创建共享内存区并将其映射到自己的地址空间。
映射共享内存:其他进程将共享内存区映射到它们的地址空间,能够访问和操作这块内存。
访问共享内存:进程可以对共享内存进行读写操作。
同步控制:为了避免竞态条件,多个进程需要使用信号量、互斥锁等机制来保证对共享内存的安全访问。
卸载共享内存:进程结束时需要解除映射,并在不再使用时删除共享内存区。
代码如下(示例):
Writer
#define SHM_NAME "/my_shared_memory" // 共享内存名称
#define SIZE 4096 // 共享内存大小
int main() {
// 创建共享内存
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 设置共享内存的大小
if (ftruncate(shm_fd, SIZE) == -1) {
perror("ftruncate");
exit(1);
}
// 映射共享内存
void *addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 写入数据到共享内存
const char *message = "Hello from writer process!";
memcpy(addr, message, strlen(message) + 1); // 写入字符串
printf("Writer: Data written to shared memory: %s\n", message);
// 程序等待一段时间,让读者进程可以读取数据
sleep(10);
// 卸载共享内存
if (munmap(addr, SIZE) == -1) {
perror("munmap");
exit(1);
}
// 关闭共享内存文件描述符
close(shm_fd);
return 0;
}
Reader
#define SHM_NAME "/my_shared_memory" // 共享内存名称
#define SIZE 4096 // 共享内存大小
int main() {
// 打开共享内存
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 映射共享内存
void *addr = mmap(NULL, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 从共享内存读取数据
printf("Reader: Data read from shared memory: %s\n", (char *)addr);
// 卸载共享内存
if (munmap(addr, SIZE) == -1) {
perror("munmap");
exit(1);
}
// 关闭共享内存文件描述符
close(shm_fd);
return 0;
}
创建共享内存:
shm_open(SHM_NAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR):创建一个共享内存对象,O_CREAT 表示如果没有该对象就创建它,O_RDWR 表示以读写模式打开。
ftruncate(shm_fd, SIZE):设置共享内存的大小。
映射共享内存:
mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0):将共享内存映射到当前进程的地址空间。PROT_READ | PROT_WRITE 表示映射的内存是可读写的,MAP_SHARED 表示共享内存区域。
写入和读取共享内存:
memcpy(addr, message, strlen(message) + 1):将字符串数据写入共享内存。
读取共享内存时直接将地址转换为字符串并输出。
清理:
munmap() 用于卸载共享内存,close() 用于关闭文件描述符,shm_unlink(SHM_NAME) 可以在共享内存不再需要时删除共享内存对象。
Socket套接字
使用 Socket 套接字 进行进程间通信(IPC)是一种常见的方式,尤其是在分布式系统或网络通信的场景中。通过套接字,进程可以跨机器或在同一台机器上通过网络进行通信。
在 C++ 中,可以使用 TCP/IP 套接字 或 UNIX 域套接字 来实现进程间通信。
TCP/IP 套接字:用于不同机器上的进程通信(网络通信)。
UNIX 域套接字:用于同一台机器上的进程通信,通信效率较高。
1.引入库
代码如下(示例):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
总结
除上述方法可以实现进程间通讯,还有
互斥锁:一种信号量,用于保护共享内存数据结构,防止多个进程同时访问。
条件变量:与互斥锁配合使用,用于进程间的同步,等待某些条件成立。
具体使用方法请查看对应的帮助文档