Bootstrap

C++进程间通信

进程间通信

多进程是指在一个程序中同时运行多个独立的进程,每个进程都有自己的独立的内存空间和执行环境。进程间通信是指不同进程之间进行数据交换和共享资源的方式。

在C++中,可以使用多种方法实现多进程和进程间通信,下面介绍几种常用的方法:

1、fork()函数

fork()函数可以创建一个新的进程,新进程是原进程的副本。父进程和子进程之间共享代码段、数据段和堆,但是有各自独立的栈。可以通过fork()函数的返回值来区分父进程和子进程,父进程返回子进程的进程ID,子进程返回0。可以使用fork()函数创建多个子进程,每个子进程执行不同的任务。

  1. fork()函数的原型如下:
#include <unistd.h>

pid_t fork(void);

fork()函数没有参数,返回值是一个进程ID。在父进程中,fork()函数返回子进程的进程ID;在子进程中,fork()函数返回0;如果fork()函数调用失败,返回-1。

  1. fork()函数的工作原理:
  • 在调用fork()函数时,操作系统会创建一个新的进程,并将父进程的所有内容复制到新的进程中,包括代码、数据、堆和栈。

  • 父进程和子进程之间的区别在于fork()函数的返回值不同。父进程中,fork()函数返回子进程的进程ID;子进程中,fork()函数返回0。

  • 父进程和子进程之间共享代码段、数据段和堆,但是有各自独立的栈。

  1. fork()函数示例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;

    pid = fork();

    if (pid == -1) {
        std::cerr << "Failed to fork" << std::endl;
        return 1;
    }

    if (pid == 0) {
        // 子进程
        std::cout << "This is child process" << std::endl;
    } else {
        // 父进程
        std::cout << "This is parent process" << std::endl;
    }

    return 0;
}

2、exec()函数族

exec()函数族可以用来在一个进程中执行另一个程序。exec()函数会将当前进程的代码段替换为新程序的代码段,并开始执行新程序。exec()函数族包括多个函数,如execl()、execv()、execle()、execve()等,可以根据需要选择合适的函数。

  1. exec()函数族的原型如下:
#include <unistd.h>

int execl(const char *path, const char *arg0, ..., (char *)0);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);

exec()函数族包括多个函数,如execl()、execv()、execle()、execve()等。这些函数的参数和返回值略有不同,但是它们的作用都是执行一个新的程序。

  1. exec()函数族的工作原理:
  • 在调用exec()函数族时,操作系统会将当前进程的代码段替换为新程序的代码段,并开始执行新程序。

  • exec()函数族的参数包括要执行的程序路径、命令行参数和环境变量等。

  • exec()函数族的返回值只有在调用失败时才会返回-1,否则不会返回。

  1. exec()函数族示例:
#include <iostream>
#include <unistd.h>

int main() {
    char *const argv[] = {"ls", "-l", NULL};

    execv("/bin/ls", argv);

    std::cerr << "Failed to execute program" << std::endl;

    return 1;
}

在这个示例中,调用execv()函数执行了/bin/ls程序,并传递了命令行参数。如果execv()函数执行成功,当前进程的代码段将被替换为ls程序的代码段,并开始执行ls程序。如果execv()函数执行失败,输出"Failed to execute program"。

需要注意的是,exec()函数族的调用会替换当前进程的代码段,因此后续的代码将不会执行。如果需要在exec()函数之后执行一些操作,可以使用fork()函数创建一个子进程,在子进程中调用exec()函数。

另外,exec()函数族还可以用于执行其他类型的程序,如shell脚本、Python脚本等。根据具体的需求和场景,可以选择合适的exec()函数来执行程序。

3、pipe()函数

pipe()函数可以创建一个管道,用于实现进程间的单向通信。管道是一个字节流,可以通过文件描述符进行读写操作。一个进程可以将数据写入管道的写端,另一个进程可以从管道的读端读取数据。管道可以用于父子进程之间的通信,也可以用于兄弟进程之间的通信。

  1. pipe()函数的原型如下:
#include <unistd.h>

int pipe(int pipefd[2]);

pipe()函数接受一个整型数组pipefd作为参数,该数组包含两个文件描述符,分别表示管道的读端和写端。pipe()函数返回0表示成功,返回-1表示失败。

  1. pipe()函数的工作原理:
  • 在调用pipe()函数时,操作系统会创建一个管道,并返回两个文件描述符,一个用于读取管道数据,一个用于写入管道数据。
  • 管道是一个字节流,数据在管道中按顺序传输,先写入的数据先读取。
  • 管道的读端和写端可以通过文件描述符进行读写操作。
  1. pipe()函数示例:
#include <iostream>
#include <unistd.h>

int main() {
    int pipefd[2];
    char buffer[256];

    // 创建管道
    if (pipe(pipefd) == -1) {
        std::cerr << "Failed to create pipe" << std::endl;
        return 1;
    }

    // 创建子进程
    pid_t pid = fork();

    if (pid == -1) {
        std::cerr << "Failed to fork" << std::endl;
        return 1;
    }

    if (pid == 0) {
        // 子进程
        close(pipefd[0]); // 关闭读端

        std::string message = "Hello from child process!";
        write(pipefd[1], message.c_str(), message.length() + 1); // 写入管道

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

        return 0;
    } else {
        // 父进程
        close(pipefd[1]); // 关闭写端

        read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据

        std::cout << "Received message from child process: " << buffer << std::endl;

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

        return 0;
    }
}

在这个示例中,首先使用pipe()函数创建了一个管道,然后使用fork()函数创建了一个子进程。子进程向管道写入一条消息,父进程从管道读取消息并输出。

需要注意的是,父进程和子进程在使用管道时需要分别关闭不需要的文件描述符。父进程关闭写端,子进程关闭读端,以确保在读写操作完成后,正确关闭管道。

管道是一种简单的进程间通信方式,但是只能实现单向通信。如果需要实现双向通信或者多个进程之间的通信,可以考虑使用其他的进程间通信方式,如共享内存、消息队列等。

4、shared memory(共享内存)

共享内存是一种进程间通信的方式,可以让多个进程共享同一块内存区域。通过共享内存,多个进程可以直接读写共享内存区域,而不需要通过中间的缓冲区。在C++中,可以使用shmget()函数创建共享内存,使用shmat()函数将共享内存映射到进程的地址空间,使用shmdt()函数解除映射关系。

下面是使用共享内存进行进程间通信的详细步骤:

  1. 创建共享内存:首先,需要创建一个共享内存区域,可以使用操作系统提供的函数(如shmget)来创建共享内存。需要指定共享内存的大小和权限等参数。
  2. 连接共享内存:创建共享内存后,需要使用操作系统提供的函数(如shmat)将共享内存连接到当前进程的地址空间中。连接后,可以通过指针来访问共享内存。
  3. 写入数据:在连接共享内存后,可以通过指针来写入数据到共享内存中。
  4. 分离共享内存:在使用完共享内存后,需要使用操作系统提供的函数(如shmdt)将共享内存从当前进程的地址空间中分离。
  5. 删除共享内存:如果不再需要使用共享内存,可以使用操作系统提供的函数(如shmctl)来删除共享内存。

需要注意的是,使用共享内存进行进程间通信时,需要保证多个进程对共享内存的访问是同步的,以避免数据的不一致性。可以使用信号量等同步机制来实现进程间的同步。

下面是一个简单的示例,演示了如何使用共享内存进行进程间通信:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

int main() {
    // 创建共享内存
    int shmid = shmget(IPC_PRIVATE, sizeof(int), IPC_CREAT | 0666);
    if (shmid == -1) {
        std::cerr << "Failed to create shared memory" << std::endl;
        return 1;
    }

    // 连接共享内存
    int* sharedData = (int*)shmat(shmid, nullptr, 0);
    if (sharedData == (int*)-1) {
        std::cerr << "Failed to attach shared memory" << std::endl;
        return 1;
    }

    // 写入数据
    *sharedData = 42;

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Failed to create child process" << std::endl;
        return 1;
    }

    if (pid == 0) {
        // 子进程读取共享内存中的数据
        std::cout << "Child process: " << *sharedData << std::endl;

        // 分离共享内存
        if (shmdt(sharedData) == -1) {
            std::cerr << "Failed to detach shared memory in child process" << std::endl;
            return 1;
        }
    } else {
        // 父进程等待子进程结束
        wait(nullptr);

        // 分离共享内存
        if (shmdt(sharedData) == -1) {
            std::cerr << "Failed to detach shared memory in parent process" << std::endl;
            return 1;
        }

        // 删除共享内存
        if (shmctl(shmid, IPC_RMID, nullptr) == -1) {
            std::cerr << "Failed to delete shared memory" << std::endl;
            return 1;
        }
    }

    return 0;
}

在这个示例中,首先使用shmget函数创建了一个共享内存区域,大小为一个整数。然后使用shmat函数将共享内存连接到当前进程的地址空间中。接着,将数据42写入共享内存中。 然后,使用fork函数创建了一个子进程。在子进程中,读取共享内存中的数据并输出。在父进程中,使用wait函数等待子进程结束。 最后,分别在子进程和父进程中使用shmdt函数将共享内存从进程的地址空间中分离。在父进程中,使用shmctl函数删除共享内存。

5、message queue(消息队列)

消息队列是一种进程间通信的方式,可以实现进程之间的异步通信。一个进程可以将消息发送到消息队列,另一个进程可以从消息队列中接收消息。消息队列可以用于进程间的数据交换和同步。在C++中,可以使用msgget()函数创建消息队列,使用msgsnd()函数发送消息,使用msgrcv()函数接收消息。

下面是使用消息队列进行进程间通信的详细步骤:

  1. 创建消息队列:首先,需要创建一个消息队列,可以使用操作系统提供的函数(如msgget)来创建消息队列。需要指定消息队列的权限等参数。
  2. 发送消息:在创建消息队列后,可以使用操作系统提供的函数(如msgsnd)向消息队列发送消息。需要指定消息的类型和内容等参数。
  3. 接收消息:可以使用操作系统提供的函数(如msgrcv)从消息队列中接收消息。需要指定接收的消息类型和接收缓冲区等参数。
  4. 删除消息队列:如果不再需要使用消息队列,可以使用操作系统提供的函数(如msgctl)来删除消息队列。

需要注意的是,使用消息队列进行进程间通信时,需要定义消息的格式和类型,以便发送方和接收方能够正确地解析和处理消息。可以使用结构体来定义消息的格式,并使用消息类型来区分不同类型的消息。

msgrcv函数是一个用于接收消息队列中的消息的系统调用函数。它的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明:

  • msqid:消息队列的标识符,由msgget函数返回。

  • msgp:指向接收消息的缓冲区的指针。

  • msgsz:接收消息的缓冲区大小。

  • msgtyp:指定要接收的消息类型。如果msgtyp为0,则接收队列中的第一个消息;如果msgtyp大于0,则接收队列中类型为msgtyp的第一个消息;如果msgtyp小于0,则接收队列中类型小于或等于msgtyp绝对值的最小消息。

  • msgflg:控制接收操作的标志位,可以是0或IPC_NOWAIT。如果msgflg为0,则调用进程将被阻塞,直到接收到一个符合条件的消息;如果msgflg为IPC_NOWAIT,则调用进程不会被阻塞,如果没有符合条件的消息,则返回-1并设置errno为ENOMSG。

函数返回值为实际接收到的消息的长度,如果出错则返回-1并设置errno。

下面是一个简单的示例,演示了如何使用消息队列进行进程间通信:

#include <iostream>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>

struct Message {
    long type;
    int data;
};

int main() {
    // 创建消息队列
    int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
    if (msgid == -1) {
        std::cerr << "Failed to create message queue" << std::endl;
        return 1;
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Failed to create child process" << std::endl;
        return 1;
    }

    if (pid == 0) {
        // 子进程发送消息
        Message message;
        message.type = 1;
        message.data = 42;

        if (msgsnd(msgid, &message, sizeof(message.data), 0) == -1) {
            std::cerr << "Failed to send message in child process" << std::endl;
            return 1;
        }
    } else {
        // 父进程接收消息
        Message message;

        if (msgrcv(msgid, &message, sizeof(message.data), 1, 0) == -1) {
            std::cerr << "Failed to receive message in parent process" << std::endl;
            return 1;
        }

        std::cout << "Parent process: " << message.data << std::endl;

        // 删除消息队列
        if (msgctl(msgid, IPC_RMID, nullptr) == -1) {
            std::cerr << "Failed to delete message queue" << std::endl;
            return 1;
        }
    }

    return 0;
}

在这个示例中,首先使用msgget函数创建了一个消息队列。然后,使用fork函数创建了一个子进程。

在子进程中,创建了一个Message结构体对象,并设置了消息的类型和数据。然后,使用msgsnd函数将消息发送到消息队列中。

在父进程中,创建了一个Message结构体对象,并使用msgrcv函数从消息队列中接收消息。通过指定消息类型为1,只接收类型为1的消息。接收到消息后,输出消息的数据。

最后,在父进程中使用msgctl函数删除消息队列。

;