C++ 多进程编程实践指南
C++ 多进程编程基础
进程与多进程概念
在操作系统中,进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的地址空间、内存、数据栈以及其他记录其运行状态的辅助数据。
多进程编程允许一个程序创建多个进程,这些进程并行运行,每个进程都可以独立执行不同的任务。这种编程方式在许多场景下非常有用,比如提高程序的执行效率(利用多核 CPU 的优势)、实现高并发处理任务以及提高程序的稳定性(一个进程崩溃不会影响其他进程)。
C++ 多进程编程接口
在 C++ 中,进行多进程编程主要依赖于操作系统提供的接口。在 Unix - like 系统(如 Linux、macOS)中,主要使用 fork()
函数来创建新进程;而在 Windows 系统中,使用 CreateProcess()
函数。下面我们先以 Unix - like 系统为例介绍 fork()
函数。
fork()
函数由 <unistd.h>
头文件提供,其原型为:
#include <unistd.h>
pid_t fork(void);
fork()
函数的作用是创建一个新的进程,称为子进程。子进程是父进程的副本,它从 fork()
调用处开始执行,几乎与父进程相同。fork()
函数调用一次,返回两次。在父进程中返回子进程的进程 ID(PID),在子进程中返回 0。如果 fork()
失败,在父进程中返回 -1。
简单的 fork() 示例
下面是一个简单的使用 fork()
函数的 C++ 代码示例:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
std::cerr << "fork() failed" << std::endl;
return 1;
} else if (pid == 0) {
// 子进程
std::cout << "I am the child process. My PID is " << getpid()
<< " and my parent's PID is " << getppid() << std::endl;
} else {
// 父进程
std::cout << "I am the parent process. My PID is " << getpid()
<< " and my child's PID is " << pid << std::endl;
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
std::cout << "Child process exited with status " << WEXITSTATUS(status) << std::endl;
}
}
return 0;
}
在上述代码中,首先调用 fork()
创建子进程。如果 fork()
失败,输出错误信息并退出程序。在子进程中,输出子进程自身的 PID 和父进程的 PID。在父进程中,输出父进程自身的 PID 和子进程的 PID,并使用 waitpid()
函数等待子进程结束。waitpid()
函数会阻塞父进程,直到指定的子进程结束。WIFEXITED(status)
宏用于判断子进程是否正常结束,WEXITSTATUS(status)
宏用于获取子进程的退出状态。
进程间通信(IPC)
进程间通信的必要性
当多个进程在系统中运行时,它们之间往往需要进行数据交换和同步。例如,一个进程可能需要将计算结果传递给另一个进程,或者多个进程需要协调对共享资源的访问。进程间通信(IPC)机制就是为了解决这些问题而设计的。
常见的 IPC 机制
管道(Pipe)
管道是一种最基本的 IPC 机制,它用于在具有亲缘关系(如父子进程)的进程之间进行通信。管道分为匿名管道和命名管道。
匿名管道由 pipe()
函数创建,其原型为:
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd
是一个长度为 2 的整数数组,pipefd[0]
用于读管道,pipefd[1]
用于写管道。数据从写端写入管道,从读端读出。
下面是一个父子进程通过匿名管道通信的示例:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#define BUFFER_SIZE 1024
int main() {
int pipefd[2];
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;
close(pipefd[0]);
close(pipefd[1]);
return 1;
} else if (pid == 0) {
// 子进程
close(pipefd[0]); // 子进程关闭读端
const char *message = "Hello from child";
if (write(pipefd[1], message, strlen(message)) != strlen(message)) {
std::cerr << "write() failed in child" << std::endl;
}
close(pipefd[1]);
} else {
// 父进程
close(pipefd[1]); // 父进程关闭写端
char buffer[BUFFER_SIZE];
ssize_t bytesRead = read(pipefd[0], buffer, BUFFER_SIZE - 1);
if (bytesRead == -1) {
std::cerr << "read() failed in parent" << std::endl;
} else {
buffer[bytesRead] = '\0';
std::cout << "Parent received: " << buffer << std::endl;
}
close(pipefd[0]);
int status;
waitpid(pid, &status, 0);
}
return 0;
}
在这个示例中,首先创建一个匿名管道。然后创建子进程,子进程关闭读端,向管道写端写入消息;父进程关闭写端,从管道读端读取消息。
命名管道(FIFO)与匿名管道类似,但它可以在不相关的进程之间进行通信。命名管道通过 mkfifo()
函数创建,其原型为:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname
是命名管道的路径名,mode
用于指定管道的访问权限。
消息队列(Message Queue)
消息队列是一种以消息为单位进行数据传输的 IPC 机制。在 Unix - like 系统中,消息队列由 msgget()
、msgsnd()
和 msgrcv()
等函数操作。
msgget()
函数用于创建或获取一个消息队列,其原型为:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
key
是一个唯一标识消息队列的键值,msgflg
用于指定创建或访问消息队列的方式。
msgsnd()
函数用于向消息队列发送消息,其原型为:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid
是消息队列的标识符,msgp
是指向消息结构的指针,msgsz
是消息的大小,msgflg
用于指定发送消息的方式。
msgrcv()
函数用于从消息队列接收消息,其原型为:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
下面是一个简单的消息队列使用示例:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <cstring>
#include <unistd.h>
#define MSG_SIZE 1024
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
key_t key = ftok(".", 'a');
if (key == -1) {
std::cerr << "ftok() failed" << std::cerr;
return 1;
}
int msqid = msgget(key, IPC_CREAT | 0666);
if (msqid == -1) {
std::cerr << "msgget() failed" << std::endl;
return 1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "fork() failed" << std::endl;
msgctl(msqid, IPC_RMID, nullptr);
return 1;
} else if (pid == 0) {
// 子进程
msgbuf sendbuf;
sendbuf.mtype = 1;
std::strcpy(sendbuf.mtext, "Hello from child");
if (msgsnd(msqid, &sendbuf, std::strlen(sendbuf.mtext), 0) == -1) {
std::cerr << "msgsnd() failed in child" << std::endl;
}
} else {
// 父进程
msgbuf recvbuf;
ssize_t bytesRead = msgrcv(msqid, &recvbuf, MSG_SIZE, 1, 0);
if (bytesRead == -1) {
std::cerr << "msgrcv() failed in parent" << std::endl;
} else {
recvbuf.mtext[bytesRead] = '\0';
std::cout << "Parent received: " << recvbuf.mtext << std::endl;
}
msgctl(msqid, IPC_RMID, nullptr);
int status;
waitpid(pid, &status, 0);
}
return 0;
}
在这个示例中,首先使用 ftok()
函数生成一个唯一的键值,然后使用 msgget()
函数创建消息队列。子进程向消息队列发送消息,父进程从消息队列接收消息。最后,父进程使用 msgctl()
函数删除消息队列。
共享内存(Shared Memory)
共享内存是一种高效的 IPC 机制,它允许多个进程共享同一块物理内存区域,从而实现数据的快速交换。在 Unix - like 系统中,共享内存由 shmget()
、shmat()
和 shmdt()
等函数操作。
shmget()
函数用于创建或获取一个共享内存段,其原型为:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key
是共享内存段的键值,size
是共享内存段的大小,shmflg
用于指定创建或访问共享内存段的方式。
shmat()
函数用于将共享内存段附加到进程的地址空间,其原型为:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
是共享内存段的标识符,shmaddr
通常为 NULL
,表示由系统自动选择合适的地址,shmflg
用于指定附加的方式。
shmdt()
函数用于将共享内存段从进程的地址空间分离,其原型为:
int shmdt(const void *shmaddr);
下面是一个简单的共享内存使用示例:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#define SHM_SIZE 1024
int main() {
key_t key = ftok(".", 'b');
if (key == -1) {
std::cerr << "ftok() failed" << std::endl;
return 1;
}
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
std::cerr << "shmget() failed" << std::endl;
return 1;
}
void *shmptr = shmat(shmid, nullptr, 0);
if (shmptr == (void *)-1) {
std::cerr << "shmat() failed" << std::endl;
shmctl(shmid, IPC_RMID, nullptr);
return 1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "fork() failed" << std::endl;
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
return 1;
} else if (pid == 0) {
// 子进程
std::strcpy((char *)shmptr, "Hello from child");
shmdt(shmptr);
} else {
// 父进程
int status;
waitpid(pid, &status, 0);
std::cout << "Parent received: " << (char *)shmptr << std::endl;
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
}
return 0;
}
在这个示例中,首先使用 ftok()
函数生成键值,然后使用 shmget()
函数创建共享内存段。父子进程通过共享内存段进行数据交换,子进程向共享内存写入数据,父进程从共享内存读取数据。最后,父子进程分别使用 shmdt()
函数分离共享内存段,父进程使用 shmctl()
函数删除共享内存段。
多进程编程中的同步问题
同步问题的产生
在多进程编程中,当多个进程同时访问和修改共享资源时,可能会出现同步问题。例如,两个进程同时读取一个共享变量的值,然后分别对其进行加 1 操作,最后将结果写回。由于进程的执行顺序是不确定的,可能会导致最终的结果与预期不符。
同步机制
信号量(Semaphore)
信号量是一种用于控制对共享资源访问的同步机制。在 Unix - like 系统中,信号量由 semget()
、semop()
和 semctl()
等函数操作。
semget()
函数用于创建或获取一个信号量集,其原型为:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key
是信号量集的键值,nsems
是信号量集中信号量的个数,semflg
用于指定创建或访问信号量集的方式。
semop()
函数用于对信号量集进行操作,其原型为:
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid
是信号量集的标识符,sops
是指向 sembuf
结构数组的指针,nsops
是 sops
数组中元素的个数。
semctl()
函数用于控制信号量集,其原型为:
int semctl(int semid, int semnum, int cmd, ...);
下面是一个使用信号量解决进程同步问题的示例:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <cstring>
#define SHM_SIZE 1024
#define SEM_KEY 1234
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main() {
key_t key = ftok(".", 'c');
if (key == -1) {
std::cerr << "ftok() failed" << std::endl;
return 1;
}
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
std::cerr << "shmget() failed" << std::endl;
return 1;
}
void *shmptr = shmat(shmid, nullptr, 0);
if (shmptr == (void *)-1) {
std::cerr << "shmat() failed" << std::endl;
shmctl(shmid, IPC_RMID, nullptr);
return 1;
}
int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
if (semid == -1) {
std::cerr << "semget() failed" << std::endl;
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
return 1;
}
union semun semval;
semval.val = 1;
if (semctl(semid, 0, SETVAL, semval) == -1) {
std::cerr << "semctl() failed" << std::endl;
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
semctl(semid, 0, IPC_RMID, 0);
return 1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "fork() failed" << std::endl;
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
semctl(semid, 0, IPC_RMID, 0);
return 1;
} else if (pid == 0) {
// 子进程
struct sembuf semopbuf;
semopbuf.sem_num = 0;
semopbuf.sem_op = -1;
semopbuf.sem_flg = 0;
if (semop(semid, &semopbuf, 1) == -1) {
std::cerr << "semop() failed in child" << std::endl;
}
std::strcpy((char *)shmptr, "Hello from child");
semopbuf.sem_op = 1;
if (semop(semid, &semopbuf, 1) == -1) {
std::cerr << "semop() failed in child" << std::endl;
}
shmdt(shmptr);
} else {
// 父进程
struct sembuf semopbuf;
semopbuf.sem_num = 0;
semopbuf.sem_op = -1;
semopbuf.sem_flg = 0;
if (semop(semid, &semopbuf, 1) == -1) {
std::cerr << "semop() failed in parent" << std::endl;
}
int status;
waitpid(pid, &status, 0);
std::cout << "Parent received: " << (char *)shmptr << std::endl;
semopbuf.sem_op = 1;
if (semop(semid, &semopbuf, 1) == -1) {
std::cerr << "semop() failed in parent" << std::endl;
}
shmdt(shmptr);
shmctl(shmid, IPC_RMID, nullptr);
semctl(semid, 0, IPC_RMID, 0);
}
return 0;
}
在这个示例中,首先创建共享内存段和信号量集。信号量的初始值设为 1,表示共享资源可用。子进程在访问共享内存前,先通过 semop()
函数将信号量值减 1(获取锁),访问完后再将信号量值加 1(释放锁)。父进程同样在访问共享内存前获取锁,访问完后释放锁,从而保证了共享资源的正确访问。
Windows 下的多进程编程
CreateProcess() 函数
在 Windows 系统中,使用 CreateProcess()
函数来创建新进程。CreateProcess()
函数的原型较为复杂,其简化形式如下:
BOOL CreateProcess(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName
是要执行的应用程序的名称;lpCommandLine
是传递给应用程序的命令行参数;lpProcessAttributes
和 lpThreadAttributes
分别用于指定进程和线程的安全属性;bInheritHandles
表示新进程是否继承父进程的句柄;dwCreationFlags
用于指定创建进程的标志;lpEnvironment
用于指定新进程的环境变量;lpCurrentDirectory
用于指定新进程的当前工作目录;lpStartupInfo
用于指定新进程的启动信息;lpProcessInformation
用于返回新进程的相关信息。
Windows 下进程间通信与同步
Windows 提供了多种进程间通信和同步机制,如文件映射(类似于共享内存)、命名管道、事件对象等。
文件映射通过 CreateFileMapping()
和 MapViewOfFile()
等函数实现。事件对象通过 CreateEvent()
函数创建,用于进程间的同步。例如:
#include <iostream>
#include <windows.h>
int main() {
HANDLE hEvent = CreateEvent(nullptr, FALSE, FALSE, L"SyncEvent");
if (hEvent == nullptr) {
std::cerr << "CreateEvent() failed" << std::endl;
return 1;
}
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(L"ChildProcess.exe", nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
std::cerr << "CreateProcess() failed" << std::endl;
CloseHandle(hEvent);
return 1;
}
WaitForSingleObject(hEvent, INFINITE);
std::cout << "Parent received signal from child" << std::endl;
CloseHandle(hEvent);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
在这个示例中,父进程创建一个事件对象,然后创建子进程。子进程在适当的时候设置事件,父进程通过 WaitForSingleObject()
函数等待事件被触发,从而实现进程间的同步。
通过以上对 C++ 多进程编程的介绍,包括进程创建、进程间通信以及同步机制,无论是在 Unix - like 系统还是 Windows 系统下,开发者都能够根据具体需求构建高效、稳定的多进程应用程序。在实际应用中,需要根据具体场景选择合适的进程间通信和同步方式,以确保程序的正确性和性能。