C++多进程之间通讯

在 C++ 中,线程和进程是有区别的:

  • 进程:是操作系统分配资源的基本单位,包括代码、数据和系统资源。每个进程都有自己独立的地址空间,因此进程之间不能直接访问彼此的内存。

  • 线程:是操作系统能够进行运算调度的最小单位。线程是进程的一个实体,是进程中的实际运行单位,线程拥有自己的堆栈和局部变量。线程之间可以直接访问彼此的内存。

在 C++ 中,std::thread 用于创建和管理线程,而不是进程。如果你需要创建进程,你可以使用操作系统提供的 API,例如在 Unix 系统上使用 fork() 函数,或者在 Windows 系统上使用 CreateProcess() 函数。

1、创建一个子进程:fork()

fork() 函数是一个标准的系统调用,用于在 Unix 和类 Unix 系统中创建新的进程。

以下是一个简单的例子,展示了如何使用 fork() 函数:

#include <iostream>
#include <unistd.h>

int main() {
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        // fork() 失败
        std::cerr << "fork() failed\n";
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout << "This is the child process, pid: " << getpid() << "\n";
    } else {
        // 父进程
        std::cout << "This is the parent process, pid: " << getpid() << ", child pid: " << pid << "\n";
    }

    return 0;
}

输出为:

xo@shahis1:~/demo$ vim fork_test.cpp
xo@shahis1:~/demo$ g++ fork_test.cpp -o fork_test
xo@shahis1:~/demo$ ./fork_test 
This is the parent process, pid: 1821956, child pid: 1821957
This is the child process, pid: 1821957

在这个例子中,fork() 函数会创建一个新的进程,并返回两次:一次在父进程中返回子进程的 PID,一次在子进程中返回 0。你可以使用这个返回值来区分父进程和子进程,并执行不同的操作。

如果你在使用 fork() 函数时遇到了问题,你可能需要检查以下几点:

  1. 编译器和操作系统:确保你的代码在支持 fork() 函数的系统上编译和运行。fork() 函数是 Unix 和类 Unix 系统的标准库函数,不支持 Windows。

  2. 权限:确保你有足够的权限来创建新的进程。在某些系统上,你可能需要管理员权限才能使用 fork() 函数。

  3. 错误处理:确保你正确处理了 fork() 函数的错误情况。在上面的例子中,我们检查了 pid 的值是否小于 0,这表示 fork() 函数调用失败。

一个进程操作公共容器的例子: 

#include <iostream>
#include <unistd.h>
#include <vector>
#include <mutex>
#include <sys/wait.h>

// 模拟一个容器
struct Container {
    int value;
};

// 全局变量
std::vector<Container> containers;
std::mutex mtx;

// 进程A创建若干db container
void processA() {
    std::cout << &containers << std::endl;
    for (int i = 0; i < 10; ++i) {
        Container c;
        c.value = i;
        containers.push_back(c);
    }
    std::cout << "copy after " << &containers << std::endl;
    std::cout << containers.size() << std::endl;
}

// 进程B尝试读取共享container
void processB() {
    sleep(1);
    std::cout << &containers << std::endl;
    std::lock_guard<std::mutex> lock(mtx);

    for (const auto& c : containers) {
        std::cout << "Container value: " << c.value << "\n";
    }
    std::cout << containers.size() << std::endl;
}

int main() {
    // 1. 客户端和服务端 initialization
    // 这里可以添加你的初始化代码

    // 2. 由进程A创建若干db container
    pid_t pidA = fork();
    if (pidA == 0) {
        // 子进程A
        processA();
        return 0;
    } else if (pidA > 0) {
        // 父进程
        std::cout << "Parent process: " << getpid() << "\n";
        std::cout << "Child process A ID: " << pidA << "\n";

        // 3. 进程B尝试读取共享container
        pid_t pidB = fork();
        if (pidB == 0) {
            // 子进程B
            processB();
            return 0;
        } else if (pidB > 0) {
            // 父进程
            std::cout << "Child process B ID: " << pidB << "\n";

            // 4. 测试同步原语,在A中进行加锁操作,测试进程B是否被阻塞
            // 这里已经通过 std::lock_guard 实现了加锁操作

            // 5. 销毁退出测试
            // 等待子进程A和B结束
            int statusA, statusB;
            waitpid(pidA, &statusA, 0);
            waitpid(pidB, &statusB, 0);

            // 打印子进程的退出状态
            std::cout << "Child process A exited with status: " << statusA << "\n";
            std::cout << "Child process B exited with status: " << statusB << "\n";

            // 这里可以添加你的销毁代码
        } else {
            std::cerr << "Failed to create process B" << "\n";
        }
    } else {
        std::cerr << "Failed to create process A" << "\n";
    }

    return 0;
}

输出为:

Parent process: 1971493
Child process A ID: 1971494
Child process B ID: 1971495
0x55a86ad2f280
copy after 0x55a86ad2f280
10
0x55a86ad2f280
0
Child process A exited with status: 0
Child process B exited with status: 0

在这个示例中,我们使用了 fork() 函数来创建两个子进程,分别模拟进程A和进程B。进程A创建若干个容器,进程B尝试读取这些容器。我们使用 std::mutex 来实现同步原语,并使用 std::lock_guard 来自动管理锁的生命周期。waitpid 函数用于等待子进程结束,并获取子进程的退出状态。这是一种常见的资源清理方法,确保子进程在结束后释放相关资源。

注意哈!从结果看出我们代码的目的并没有实现,这是为啥呢?

在 Unix 系统中,fork 创建的子进程与父进程之间的资源共享情况取决于具体的资源类型。以下是关于 fork 创建的子进程与父进程之间资源共享的一些关键点:

  1. 代码段和数据段:代码段和数据段是共享的。子进程和父进程共享相同的代码段和数据段,因此它们可以访问相同的内存地址。

  2. 堆区:堆区是私有的。子进程和父进程有各自独立的堆区。堆区中的内存分配(例如使用 malloc 或 new)在子进程中不会影响父进程的堆区。

  3. 栈区:栈区是私有的。子进程和父进程有各自独立的栈区。栈区中的内存分配(例如函数调用)在子进程中不会影响父进程的栈区。

  4. 打开的文件描述符:打开的文件描述符是共享的。子进程和父进程共享相同的文件描述符表,因此它们可以访问相同的文件。

  5. 信号处理:信号处理是共享的。子进程和父进程共享相同的信号处理程序。

  6. 进程 ID 和父进程 ID:子进程和父进程有各自独立的进程 ID 和父进程 ID。

  7. 进程状态:子进程和父进程有各自独立的进程状态。例如,父进程和子进程可以有不同的运行状态(运行、暂停、停止等)。

总结来说,fork 创建的子进程与父进程之间的资源共享取决于具体的资源类型。代码段和数据段、打开的文件描述符、信号处理和进程状态是共享的,而堆区和栈区是私有的。

我们再看个例子:

#include <stdio.h>
#include <unistd.h>

int global_var = 10;

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        printf("子进程 ID: %d, 全局变量值: %d\n", getpid(), global_var);
        global_var = 20;
        printf("子进程修改后的全局变量值: %d\n", global_var);
	    printf("global_var_addr = %p\n", &global_var);
    } else if (pid > 0) {
        // 父进程
        printf("父进程 ID: %d, 全局变量值: %d\n", getpid(), global_var);
        sleep(1);  // 确保子进程先执行完毕
        printf("父进程修改后的全局变量值: %d\n", global_var);
	    printf("global_var_addr = %p\n", &global_var);
    } else {
        // fork 失败
        perror("fork 失败");
    }

    return 0;
}

输出结果为:

父进程 ID: 1978290, 全局变量值: 10
子进程 ID: 1978291, 全局变量值: 10
子进程修改后的全局变量值: 20
global_var_addr = 0x55830dff6010
父进程修改后的全局变量值: 10
global_var_addr = 0x55830dff6010

 发现了没有!地址虽然是那个地址,但是值不一样。这是为啥呢?

在 Unix 系统中,fork 创建的子进程与父进程之间的全局变量共享情况取决于具体的资源类型。以下是关于 fork 创建的子进程与父进程之间全局变量共享的一些关键点:

  1. 全局变量的内存位置:全局变量在内存中的位置是共享的。子进程和父进程共享相同的全局变量内存地址。这意味着任何对全局变量的修改,无论是在父进程还是子进程中,都会影响到共享的内存位置。

  2. 全局变量的值:全局变量的值在 fork 时是共享的。子进程和父进程在 fork 时会有相同的全局变量值。任何对全局变量的修改都会影响到共享的内存位置,从而影响到另一个进程的全局变量值。

  3. 写时复制(Copy-on-Write)机制:在某些情况下,操作系统可能会使用写时复制(Copy-on-Write, COW)机制来优化内存使用。当子进程试图修改全局变量时,操作系统会自动为子进程创建一个全局变量的私有副本,以避免父进程和子进程共享同一块内存。

需要注意的是,写时复制机制并不总是启用的,具体取决于操作系统的实现和配置。

总结来说,fork 创建的子进程与父进程之间的全局变量共享是通过共享内存位置和值来实现的。任何对全局变量的修改都会影响到共享的内存位置,并且可能会触发写时复制机制来创建私有副本。

2、pipe管道通信

管道是一种进程间通信(IPC)机制,它允许两个进程之间传递数据。管道的文件描述符是一个数组,包含两个元素:pipefd[0] 是读端,pipefd[1] 是写端。在使用管道时,通常需要关闭不需要的文件描述符,以避免混淆和资源泄漏。

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>

void childProcess(int pipefd[]) {
    // 关闭不需要的文件描述符
    close(pipefd[0]); // 关闭读端

    // 向管道写入数据
    const char* message = "Hello from child process!";
    write(pipefd[1], message, strlen(message) + 1);

    // 关闭写端
    close(pipefd[1]);
}

void parentProcess(int pipefd[]) {
    // 关闭不需要的文件描述符
    close(pipefd[1]); // 关闭写端

    // 从管道读取数据
    char buffer[100];
    int bytesRead = read(pipefd[0], buffer, sizeof(buffer) - 1);
    if (bytesRead > 0) {
        buffer[bytesRead] = '\0'; // 添加字符串终止符
        std::cout << "Message from child process: " << buffer << std::endl;
    }

    // 关闭读端
    close(pipefd[0]);
}

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        childProcess(pipefd);
    } else {
        // 父进程
        parentProcess(pipefd);

        // 等待子进程结束
        int status;
        waitpid(pid, &status, 0);
    }

    return 0;
}

输出结果为:

Message from child process: Hello from child process!

如果你需要通过管道传递多个数据,可以通过多次写入和读取操作来实现。以下是一个示例,展示了如何通过管道传递多个数据:

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <cstring>

void childProcess(int pipefd[]) {
    // 关闭不需要的文件描述符
    close(pipefd[0]); // 关闭读端

    // 向管道写入多个消息
    const char* messages[] = {"Hello", "from", "child", "process!"};
    for (const char* message : messages) {
        write(pipefd[1], message, strlen(message) + 1);
        // 添加延时,确保子进程有足够的时间向管道写入消息
	    sleep(1);
    }

    // 关闭写端
    close(pipefd[1]);
}

void parentProcess(int pipefd[]) {
    // 关闭不需要的文件描述符
    close(pipefd[1]); // 关闭写端

    // 从管道读取多个消息
    char buffer[100];
    while (true) {
        int bytesRead = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (bytesRead > 0) {
            buffer[bytesRead] = '\0'; // 添加字符串终止符
            std::cout << "Message from child process: " << buffer << std::endl;
        } else {
            break; // 读取到文件结束符,退出循环
        }
    }

    // 关闭读端
    close(pipefd[0]);
}

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        childProcess(pipefd);
    } else {
        // 父进程
        parentProcess(pipefd);

        // 等待子进程结束
        int status;
        waitpid(pid, &status, 0);
    }

    return 0;
}

结果输出为·:

Message from child process: Hello
Message from child process: from
Message from child process: child
Message from child process: process!

以下是代码的详细解释:

  1. 创建管道:在main函数中,使用pipe函数创建一个管道,并将管道的文件描述符存储在pipefd数组中。

  2. 创建子进程:使用fork函数创建一个子进程。如果fork函数调用成功,父进程会返回子进程的PID,子进程会返回0。

  3. 子进程:在子进程中,关闭不需要的管道端,然后向管道写入多个消息。需要注意的是这里必须sleep,否则只会输出一个信息Hello。

  4. 父进程:在父进程中,关闭不需要的管道端,然后从管道读取消息。每次读取一个消息后,都会添加一个字符串终止符'\0'。如果读取到的字节数小于0,表示读取到文件结束符,此时退出循环。

  5. 等待子进程结束:在父进程中,使用waitpid函数等待子进程结束,并获取子进程的退出状态。

还有一种方法就是一次性输入一个拼接好的字符串,然后再父进程分割。

管道的最大容量通常由操作系统的文件系统和内核参数决定。以下是一些常见的限制因素:

  1. 文件系统限制:不同的文件系统有不同的最大文件大小限制。例如,在某些文件系统中,单个文件的最大大小可能为4GB或更大。

  2. 内核参数:内核参数fs.pipe-max-size定义了管道的最大容量。在Linux系统中,可以使用以下命令查看当前的最大管道大小:

sysctl fs.pipe-max-size

在某些系统中,管道的最大容量可能被限制为65536字节(64KB)。

        3.实际限制:

        1)内存限制:如果管道的数据量过大,可能会导致内存不足,从而影响系统性能。

        2)读写速度:如果读取和写入管道的速度过快,可能会导致数据丢失或其他问题。

在实际应用中,管道的使用应该避免大量数据的写入和读取,以避免可能的性能问题。如果需要传输大量数据,建议使用其他方式,如文件、套接字或内存映射文件。

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值