CPP进程间的通讯

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

总结

除上述方法可以实现进程间通讯,还有
互斥锁:一种信号量,用于保护共享内存数据结构,防止多个进程同时访问。
条件变量:与互斥锁配合使用,用于进程间的同步,等待某些条件成立。
具体使用方法请查看对应的帮助文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值