多进程是指在一个程序中同时运行多个独立的进程,每个进程都有自己的独立的内存空间和执行环境。进程间通信是指不同进程之间进行数据交换和共享资源的方式。
进程:是操作系统分配资源的基本单位,包括代码、数据和系统资源。每个进程都有自己独立的地址空间,因此进程之间不能直接访问彼此的内存。
线程:是操作系统能够进行运算调度的最小单位。线程是进程的一个实体,是进程中的实际运行单位,线程拥有自己的堆栈和局部变量。线程之间可以直接访问彼此的内存。
C++ 中,std::thread用于创建和管理线程,而不是进程。如果你需要创建进程,你可以使用操作系统提供的API,例如在Unix系统上使用fork()函数,或者在Windows系统上使用CreateProcess()函数。
目录
1、fork()函数
fork()函数可以创建一个新的进程,新进程是原进程的副本。父进程和子进程之间共享代码段、数据段和堆,但是有各自独立的栈。可以通过fork()函数的返回值来区分父进程和子进程,父进程返回子进程的进程ID,子进程返回0。可以使用fork()函数创建多个子进程,每个子进程执行不同的任务。fork()函数的原型如下:
#include <unistd.h>
pid_t fork(void);
fork()函数没有参数,返回值是一个进程ID。在父进程中,fork()函数返回子进程的进程ID;在子进程中,fork()函数返回0;如果fork()函数调用失败,返回-1。
fork()函数的工作原理:
在调用fork()函数时,操作系统会创建一个新的进程,并将父进程的所有内容复制到新的进程中,包括代码、数据、堆和栈。
父进程和子进程之间的区别在于fork()函数的返回值不同。父进程中,fork()函数返回子进程的进程ID;子进程中,fork()函数返回0。
父进程和子进程之间共享代码段、数据段和堆,但是有各自独立的栈。
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) {
cout << "This is child process" << std::endl; // 子进程
} else {
cout << "This is parent process" << std::endl;// 父进程
}
return 0;
}
在 Unix 系统中,fork 创建的子进程与父进程之间的资源共享情况取决于具体的资源类型。以下是关于 fork 创建的子进程与父进程之间资源共享的一些关键点:
代码段和数据段:代码段和数据段是共享的。子进程和父进程共享相同的代码段和数据段,因此它们可以访问相同的内存地址。
堆区:堆区是私有的。子进程和父进程有各自独立的堆区。堆区中的内存分配(例如使用 malloc 或 new)在子进程中不会影响父进程的堆区。
栈区:栈区是私有的。子进程和父进程有各自独立的栈区。栈区中的内存分配(例如函数调用)在子进程中不会影响父进程的栈区。
打开的文件描述符:打开的文件描述符是共享的。子进程和父进程共享相同的文件描述符表,因此它们可以访问相同的文件。
信号处理:信号处理是共享的。子进程和父进程共享相同的信号处理程序。
进程 ID 和父进程 ID:子进程和父进程有各自独立的进程 ID 和父进程 ID。
进程状态:子进程和父进程有各自独立的进程状态。例如,父进程和子进程可以有不同的运行状态(运行、暂停、停止等)。
2、exec()函数族
exec()函数族可以用来在一个进程中执行另一个程序。exec()函数会将当前进程的代码段替换为新程序的代码段,并开始执行新程序。exec()函数族包括多个函数,如execl()、execv()、execle()、execve()等,可以根据需要选择合适的函数。
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()等。这些函数的参数和返回值略有不同,但是它们的作用都是执行一个新的程序。
exec()函数族的工作原理:
在调用exec()函数族时,操作系统会将当前进程的代码段替换为新程序的代码段,并开始执行新程序。
exec()函数族的参数包括要执行的程序路径、命令行参数和环境变量等。
exec()函数族的返回值只有在调用失败时才会返回-1,否则不会返回。
exec()函数族示例:
#include <iostream>
#include <unistd.h>
int main() {
char *const argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
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是一种半双工的通信方式,适用于具有亲缘关系的两个进程之间的通信。管道分为匿名管道和命名管道。
它允许两个进程之间传递数据。管道的文件描述符是一个数组,包含两个元素:pipefd[0] 是读端,pipefd[1] 是写端。在使用管道时,通常需要关闭不需要的文件描述符,以避免混淆和资源泄漏。
上述代码中,先调用pipe()函数创建了一个管道,并在fork()函数中创建了一个子进程。父进程向管道中写入一条消息,子进程从管道中读取该消息并输出到屏幕上。
需要注意的是,管道是半双工的通信方式,在本例中父进程和子进程各自拥有管道的一端,因此需要分别关闭对方不需要使用的端口,以避免出现死锁等问题。另外,当进程间通信结束后需要及时关闭管道,并在父进程中等待子进程结束。
pipe()函数可以创建一个管道,用于实现进程间的单向通信。管道是一个字节流,可以通过文件描述符进行读写操作。一个进程可以将数据写入管道的写端,另一个进程可以从管道的读端读取数据。管道可以用于父子进程之间的通信,也可以用于兄弟进程之间的通信。
pipe()函数的原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
pipe()函数接受一个整型数组pipefd作为参数,该数组包含两个文件描述符,分别表示管道的读端和写端。pipe()函数返回0表示成功,返回-1表示失败。
pipe()函数的工作原理:
在调用pipe()函数时,操作系统会创建一个管道,并返回两个文件描述符,一个用于读取管道数据,一个用于写入管道数据。
管道是一个字节流,数据在管道中按顺序传输,先写入的数据先读取。
管道的读端和写端可以通过文件描述符进行读写操作。
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()函数解除映射关系。
共享内存可以被多个进程同时访问,因此需要使用信号量等同步机制来保证数据的正确性。另外,在进程间通信结束后需要及时解除共享内存的映射,并删除该共享内存对象。
下面是使用共享内存进行进程间通信的详细步骤:
创建共享内存:首先,需要创建一个共享内存区域,可以使用操作系统提供的函数(如shmget)来创建共享内存。需要指定共享内存的大小和权限等参数。
连接共享内存:创建共享内存后,需要使用操作系统提供的函数(如shmat)将共享内存连接到当前进程的地址空间中。连接后,可以通过指针来访问共享内存。
写入数据:在连接共享内存后,可以通过指针来写入数据到共享内存中。
分离共享内存:在使用完共享内存后,需要使用操作系统提供的函数(如shmdt)将共享内存从当前进程的地址空间中分离。
删除共享内存:如果不再需要使用共享内存,可以使用操作系统提供的函数(如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()函数接收消息。需要注意的是,消息队列是可靠的通信方式,在多个进程之间传递数据时不会出现丢失或错位等情况。另外,在进程间通信结束后需要及时删除消息队列。
下面是使用消息队列进行进程间通信的详细步骤:
创建消息队列:首先,需要创建一个消息队列,可以使用操作系统提供的函数(如msgget)来创建消息队列。需要指定消息队列的权限等参数。
发送消息:在创建消息队列后,可以使用操作系统提供的函数(如msgsnd)向消息队列发送消息。需要指定消息的类型和内容等参数。
接收消息:可以使用操作系统提供的函数(如msgrcv)从消息队列中接收消息。需要指定接收的消息类型和接收缓冲区等参数。
删除消息队列:如果不再需要使用消息队列,可以使用操作系统提供的函数(如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) {
cerr << "Failed to send message in child process" << endl;
return 1;
}
} else {
// 父进程接收消息
Message message;
if (msgrcv(msgid, &message, sizeof(message.data), 1, 0) == -1) {
cerr << "Failed to receive message in parent process" << 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函数删除消息队列。
6、套接字(Socket)
是一种网络编程技术,但也可用于实现进程间通信,支持不同主机上的进程进行通信。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
using namespace std;
int main() {
pid_t pid;
int sockfd, newsockfd, portno;
socklen_t clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
// 创建套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
cerr << "Failed to create socket." << endl;
return 1;
}
// 设置服务器地址结构体
bzero((char*)&serv_addr, sizeof(serv_addr));
portno = 8888;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
// 绑定套接字到端口号
if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
cerr << "Failed to bind socket." << endl;
return 1;
}
// 监听套接字
listen(sockfd, 5);
// 创建子进程
if ((pid = fork()) < 0) {
cerr << "Failed to create child process." << endl;
return 1;
} else if (pid > 0) { // 父进程
cout << "Parent process is waiting for child process..." << endl;
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, &clilen); //等待客户端连接
bzero(buffer, 256);
read(newsockfd, buffer, 255); // 从套接字中读取消息
cout << "Message from child process: " << buffer << endl;
string message = "Hello, child process.";
write(newsockfd, message.c_str(), message.length()); // 向套接字中写入消息
close(newsockfd); // 关闭新的套接字
wait(NULL); // 等待子进程结束
} else { // 子进程
sleep(1); // 等待父进程执行
// 连接服务器
if(connect(sockfd,(struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
cerr << "Failed to connect to server." << endl;
return 1;
}
string message = "Hello, parent process.";
write(sockfd, message.c_str(), message.length()); // 向套接字中写入消息
bzero(buffer, 256);
read(sockfd, buffer, 255); // 从套接字中读取消息
cout << "Message from parent process: " << buffer << endl;
close(sockfd); // 关闭套接字
}
return 0;
}
先调用socket()函数创建了一个套接字。然后设置服务器地址结构体,并使用bind()函数将套接字绑定到指定的端口号上。调用listen()函数开始监听套接字,等待客户端连接。父进程在等待子进程连接之前一直处于阻塞状态,子进程连接上服务器后即可继续执行。需要注意的是,在多个进程间使用套接字进行通信时,需要指定不同的端口号来区分各个进程之间的连接。