当前位置: 首页 > news >正文

C++ 多进程编程深度解析【C++进阶每日一学】

在这里插入图片描述

文章目录

    • 一、引言
    • 二、核心概念:进程 (Process)
      • 功能与作用
    • 三、C++ 多进程的实现方式
    • 四、核心函数详解
      • 1. `fork()` - 创建子进程
        • 函数原型
        • 功能说明
        • 返回值
        • 完整使用格式
      • 2. `wait()` 和 `waitpid()` - 等待子进程结束
        • 函数原型
        • 参数与返回值详解
      • 3. `exec` 系列函数 - 执行新程序
        • 函数族
        • 返回值
    • 五、完整示例
      • 示例一:基本的 `fork` 使用
      • 示例二:`fork` 与 `exec` 结合 (fork-exec 模型)
    • 六、关键注意事项
    • 七、总结


如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力


一、引言

在现代计算中,为了充分利用多核处理器的计算能力并提高应用的稳定性,多进程编程是一种至关重要且应用广泛的技术。

二、核心概念:进程 (Process)

在操作系统中,一个 进程 (Process) 是一个正在执行的程序的实例。每个进程都拥有独立的内存空间,这包括代码段、数据段、堆和栈。这种内存隔离是进程最重要的特性之一。

功能与作用

多进程编程的 核心优势 在于:

  1. 稳定性与健壮性:由于进程间内存相互独立,一个进程的崩溃(如内存访问错误)通常不会影响到其他进程的正常运行。这使得多进程架构在需要高可靠性的服务中备受青睐。
  2. 资源隔离:操作系统为每个进程分配独立的资源(内存、文件描述符等),简化了资源管理,避免了复杂的同步问题。
  3. 利用多核CPU:操作系统可以轻易地将不同的进程调度到不同的CPU核心上并行执行,从而最大限度地利用硬件性能。

与多线程相比,多进程的主要区别在于内存模型。线程共享同一进程的内存空间,通信效率高但需要复杂的同步机制(如互斥锁、信号量)来避免数据竞争;而进程通信(IPC)需要借助操作系统提供的机制,相对开销更大,但模型更简单、更安全。

三、C++ 多进程的实现方式

C++ 标准库本身并未提供直接创建进程的API(不同于 thread,可直接调用创建线程)。因此,C++ 的多进程编程严重依赖于底层操作系统提供的接口。最主流的实现方式是使用 POSIX 标准定义的 fork() 系统调用,这在所有类Unix系统(Linux, macOS等)上都是通用的。

Windows系统使用另一套API(CreateProcess),其模型与fork有本质区别。本文将重点阐述POSIX标准的fork模型。

四、核心函数详解

1. fork() - 创建子进程

fork() 是在类Unix系统中创建新进程的唯一方式。它通过复制调用它的进程(父进程)来创建一个新的、几乎完全相同的子进程。

函数原型
#include <unistd.h>pid_t fork(void);
功能说明

调用 fork() 后,操作系统会创建一个新的子进程。子进程是父进程的一个副本,它拥有父进程内存空间的副本(采用写时复制 Copy-on-Write 技术以优化性能)、相同的文件描述符、相同的程序计数器(即子进程从fork()返回处开始执行)等。

返回值

fork() 的返回值是区分父子进程的关键,它有三种可能性:

  • 在父进程中:返回新创建的子进程的ID(一个正整数)。
  • 在子进程中:返回 0
  • 创建失败:返回 -1,并设置全局变量 errno
完整使用格式
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>// ...pid_t pid = fork();if (pid < 0) {// fork 失败,处理错误cerr << "fork failed!" << endl;exit(1);
} else if (pid == 0) {// 此代码块由子进程执行cout << "This is the child process, PID = " << getpid() << endl;// ... 执行子进程的任务 ...exit(0); // 子进程任务完成后必须退出
} else {// 此代码块由父进程执行cout << "This is the parent process, PID = " << getpid() << ", child PID = " << pid << endl;// ... 父进程可以继续执行自己的任务,或者等待子进程结束 ...
}

2. wait()waitpid() - 等待子进程结束

父进程通常需要等待子进程执行完毕,以回收其资源并获取其退出状态。否则,已终止但未被父进程回收的子进程将成为“僵尸进程”(Zombie Process),浪费系统资源。

函数原型
#include <sys/wait.h>pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
参数与返回值详解
  • wait(int *status):

    • 功能:阻塞当前进程(父进程),直到它的 任意一个 子进程结束。
    • status:一个整型指针,用于存储子进程的退出状态信息。如果不需要,可以传入nullptr
    • 返回值:成功时返回结束的子进程的ID;如果没有子进程或出错,则返回-1
  • waitpid(pid_t pid, int *status, int options):

    • 功能:提供了更灵活的等待方式。
    • pid:指定要等待的子进程ID。
      • > 0:等待指定ID的子进程。
      • -1:等待任意子进程(与wait相同)。
      • 0:等待与当前进程组ID相同的任何子进程。
    • status:同wait
    • options:控制waitpid的行为,最常用的是WNOHANG,它使waitpid变为非阻塞调用。如果没有子进程退出,它会立即返回0
    • 返回值:成功时返回结束的子进程ID;如果使用了WNOHANG且没有子进程退出,则返回0;出错时返回-1

3. exec 系列函数 - 执行新程序

fork() 创建的子进程执行的是与父进程相同的代码。如果我们希望子进程执行一个全新的程序,就需要使用 exec 系列函数。

exec 系列函数会用一个全新的程序替换当前进程的内存空间(包括代码、数据、堆栈),进程ID保持不变。一旦调用成功,原程序中 exec 调用之后的代码将永远不会被执行。

函数族

exec 不是一个函数,而是一族函数,它们的命名规则反映了其参数传递方式:

  • l (list): 参数以可变参数列表的形式给出,以NULL结尾。
  • v (vector): 参数以一个字符串数组(char*[])的形式给出。
  • p (path): 会在系统的PATH环境变量中搜索要执行的程序。
  • e (environment): 允许额外传递一个环境变量数组。

常用组合:

  • execl(const char *path, const char *arg, ...)
  • execlp(const char *file, const char *arg, ...)
  • execv(const char *path, char *const argv[])
  • execvp(const char *file, char *const argv[])
返回值

如果 exec 调用成功,它将不会返回。如果调用失败(例如程序不存在、没有权限),它会返回-1,并设置 errno

五、完整示例

示例一:基本的 fork 使用

这个例子展示了如何创建一个子进程,父子进程如何执行不同的代码路径,以及父进程如何等待子进程结束。

#include <iostream>
#include <string>
#include <unistd.h>    // for fork, getpid, getppid
#include <sys/wait.h>  // for waitusing namespace std;int main() {cout << "Main process started, PID: " << getpid() << endl;pid_t pid = fork();if (pid < 0) {// Errorcerr << "Fork failed. Exiting." << endl;return 1;} else if (pid == 0) {// Child Processcout << "--> Child process started." << endl;cout << "--> My PID is " << getpid() << ", my parent's PID is " << getppid() << "." << endl;// 模拟子进程执行任务sleep(2);cout << "--> Child process finished." << endl;exit(0); // 子进程正常退出} else {// Parent Processcout << "Parent process continues." << endl;cout << "Created a child with PID: " << pid << endl;cout << "Parent is waiting for the child to finish..." << endl;int status;wait(&status); // 阻塞等待子进程结束if (WIFEXITED(status)) {cout << "Child process exited with status: " << WEXITSTATUS(status) << endl;} else {cout << "Child process terminated abnormally." << endl;}cout << "Parent process finished." << endl;}return 0;
}

示例二:forkexec 结合 (fork-exec 模型)

这个例子展示了多进程编程最经典的用法:父进程创建一个子进程,然后子进程通过exec执行一个全新的程序(例如系统的 ls 命令)。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>using namespace std;int main() {cout << "Parent process (PID: " << getpid() << ") is starting..." << endl;pid_t pid = fork();if (pid < 0) {cerr << "Fork failed." << endl;return 1;} else if (pid == 0) {// Child Processcout << "--> Child (PID: " << getpid() << ") is about to run 'ls -l /'" << endl;// 第一个参数是要执行的程序名// 后续参数是程序的命令行参数,最后一个必须是 nullptrexeclp("ls", "ls", "-l", "/", nullptr);// 如果 execlp 成功,下面的代码将不会被执行// 如果执行到这里,说明 execlp 失败了cerr << "--> execlp failed!" << endl;exit(1); // 必须退出,否则子进程会继续执行父进程的代码} else {// Parent Processcout << "Parent is waiting for the command to complete..." << endl;wait(nullptr); // 等待子进程结束,这里不关心退出状态cout << "Child has finished. Parent is exiting." << endl;}return 0;
}

六、关键注意事项

  1. 绝不忘记 wait:父进程必须调用 waitwaitpid 来回收子进程资源,否则会产生僵尸进程。
  2. fork 后的资源处理fork 会复制文件描述符。这意味着父子进程可能同时操作同一个文件句柄,可能导致输出混乱或数据损坏,需要小心处理或关闭不需要的描述符。
  3. 写时复制 (Copy-on-Write):理解 fork 的 COW 机制。父子进程共享物理内存页,直到其中一方尝试写入,这时内核才会为写入方复制一份私有页面。这使得 fork 的开销远比想象中要小。
  4. 进程间通信 (IPC):由于内存隔离,进程间通信必须通过显式机制,如管道 (Pipe)、共享内存 (Shared Memory)、消息队列 (Message Queue) 或套接字 (Socket)。选择合适的IPC机制是多进程设计的关键。
  5. 信号处理:在多进程环境中,信号处理变得更加复杂。需要明确哪个进程应该处理哪个信号,并妥善设计信号处理函数。

七、总结

C++ 多进程编程是一种强大而基础的技术,它通过利用操作系统提供的 forkwaitexec 等原生接口,实现了程序的并行化和模块化。其核心优势在于无与伦比的稳定性和资源隔离性。虽然带来了进程间通信的开销,但在许多高可靠、高并发的系统设计中,这种代价是完全值得的。熟练掌握 fork-exec 模型,并正确处理进程的生命周期管理,是每一位资深C++系统程序员必备的技能。

如果觉得本文对您有所帮助,点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力

http://www.lryc.cn/news/625157.html

相关文章:

  • 一个基于纯前端技术实现的五子棋游戏,无需后端服务,直接在浏览器中运行。
  • 深度学习篇---softmax层
  • Maven 生命周期和插件
  • 大数据分析-读取文本文件内容进行词云图展示
  • 大厂求职 | 2026海尔校园招聘,启动!
  • Vuex 状态持久化企业级解决方案
  • ​Kali Linux 环境中的系统配置文件与用户配置文件大全
  • MongoDB 从入门到精通:安装配置与基础操作指令详解
  • 计算机组成原理(9) - 整数的乘除法运算
  • 抽象类和接口的区别
  • VLN视觉语言导航(3)——神经网络的构建和优化 2.3
  • qsort函数使用及其模拟实现
  • Android Cutout(屏幕挖孔)详解
  • SpringBoot--Spring MVC 拦截器注入与 new 的区别
  • gdb的load命令和传给opeocd的monitor flash write_image erase命令的区别
  • 优秀开发者的重要认知能力无法被AI替代
  • 在win10/11下Node.js安装配置教程
  • Ai Agent 项目
  • 项目延期的主要原因分析,以及应对策略
  • 摔倒检测数据集:1w+图像,yolo标注
  • 深度学习-计算机视觉-微调 Fine-tune
  • 【完整源码+数据集+部署教程】织物缺陷检测系统源码和数据集:改进yolo11-RevCol
  • STL库——string(类函数学习)
  • steal tsoding‘s pastebeam code as go server
  • CMake指令:查找文件(find_file)、查找目录(find_path)、查找库文件(find_library)
  • npm设置了镜像 pnpm还需要设置镜像吗
  • Esp32基础(③旋转编码器)
  • wait / notify、单例模式
  • 在openEuler系统中如何查看文件夹下每个文件的大小
  • AVB(Android Verified Boot)中vbmeta结构浅析