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

C++ 多线程与 Linux 进程创建详解

1. 理解线程和进程的基本概念

什么是进程?

想象一个工厂

  • 每个进程就像一个独立的工厂

  • 有自己独立的土地(内存空间)

  • 有自己的工人(线程)

  • 工厂之间相互隔离,不能直接访问对方的设备

什么是线程?

想象工厂里的工人

  • 线程是进程内部的执行单元

  • 同一个工厂里的工人共享所有资源

  • 工人们可以同时做不同的工作

  • 工人们可以方便地互相沟通

2. 创建新线程的详细讲解

2.1 使用 C++ 标准库创建线程(推荐)

基本步骤:

  1. 包含线程头文件

  2. 定义线程要执行的任务

  3. 创建线程对象

  4. 管理线程的生命周期

通俗解释:
想象你要请一个帮手(线程)来帮你做家务:

cpp

// 1. 准备请帮手的工具
#include <thread>// 2. 定义帮手要做的工作
void 帮手的工作() {std::cout << "帮手正在打扫卫生..." << std::endl;
}int main() {// 3. 请一个帮手(创建线程)std::thread 帮手(帮手的工作);// 4. 等待帮手完成工作帮手.join();std::cout << "所有工作都完成了!" << std::endl;return 0;
}

2.2 线程的几种创建方式

方式1:普通函数

cpp

void 普通任务() {// 线程执行的工作
}std::thread 线程1(普通任务);

方式2:Lambda表达式(最常用)

cpp

std::thread 线程2([]() {std::cout << "Lambda线程在工作" << std::endl;
});

方式3:带参数的线程

cpp

void 带参数任务(int 次数, std::string 任务名) {for (int i = 0; i < 次数; i++) {std::cout << 任务名 << " 第" << i << "次" << std::endl;}
}std::thread 线程3(带参数任务, 5, "洗碗");

2.3 线程管理的重要概念

join() - 等待线程结束

  • 就像等帮手干完活再一起休息

  • 主线程会暂停,等待子线程完成

detach() - 分离线程

  • 就像让帮手独立工作,不用等他

  • 子线程在后台运行,与主线程无关

joinable() - 检查线程是否可以等待

  • 检查帮手是否还在工作

3. 在 Linux 中创建新进程的详细讲解

3.1 使用 fork() 系统调用

fork() 的工作原理:
想象细胞分裂:

  • 一个进程调用 fork()

  • 系统创建该进程的完整副本

  • 两个进程从 fork() 调用后继续执行

  • 父进程得到子进程的ID,子进程得到0

通俗示例:

cpp

#include <unistd.h>
#include <iostream>int main() {std::cout << "准备分裂进程..." << std::endl;// 细胞分裂开始pid_t 子进程ID = fork();if (子进程ID == -1) {std::cout << "分裂失败!" << std::endl;} else if (子进程ID == 0) {// 这是子进程(新细胞)std::cout << "我是子进程,我的ID是:" << getpid() << std::endl;std::cout << "我的父进程ID是:" << getppid() << std::endl;} else {// 这是父进程(原细胞)std::cout << "我是父进程,我的ID是:" << getpid() << std::endl;std::cout << "我创建的子进程ID是:" << 子进程ID << std::endl;}return 0;
}

3.2 fork() 的重要特性:写时复制

什么是写时复制?
想象双胞胎兄弟:

  • 刚开始时,他们共享所有玩具(内存数据)

  • 只有当某个兄弟要修改玩具时,才给他买一个新的

  • 这样既节省资源,又保证独立性

在代码中的体现:

cpp

int 共享变量 = 100;pid_t pid = fork();if (pid == 0) {// 子进程修改变量共享变量 = 200;  // 这里才会真正复制内存std::cout << "子进程看到的变量:" << 共享变量 << std::endl;  // 输出200
} else {// 父进程看到的还是原值std::cout << "父进程看到的变量:" << 共享变量 << std::endl;  // 输出100
}

3.3 使用 exec 系列函数替换进程

exec 的作用:
就像灵魂附体:

  • 保持原来的身体(进程ID)

  • 但换了一个全新的灵魂(程序代码)

常见用法:

cpp

#include <unistd.h>// 在子进程中执行新程序
if (fork() == 0) {// 子进程:执行 ls -l 命令execl("/bin/ls", "ls", "-l", nullptr);// 如果执行成功,下面的代码不会运行std::cout << "如果看到这句话,说明exec失败了" << std::endl;
}

4. 线程 vs 进程的对比

4.1 资源开销对比

进程(工厂模式):

  • ✅ 安全性高:一个工厂倒闭不影响其他工厂

  • ✅ 稳定性好:问题隔离

  • ❌ 开销大:每个工厂都要独立建设

  • ❌ 通信复杂:工厂之间需要专门的通信渠道

线程(工人模式):

  • ✅ 创建快速:招聘工人比建工厂快

  • ✅ 通信简单:工人之间可以直接说话

  • ✅ 资源共享:所有工人共用工厂设备

  • ❌ 风险高:一个工人出错可能影响整个工厂

4.2 使用场景对比

适合用进程的情况:

  • 需要高度稳定性和安全性的任务

  • 任务之间相互独立,不需要频繁通信

  • 比如:Web服务器、数据库服务

适合用线程的情况:

  • 任务需要频繁通信和共享数据

  • 对性能要求高,需要快速创建

  • 比如:图形界面程序、游戏引擎

5. 实际应用示例

5.1 多线程下载器(模拟)

cpp

#include <thread>
#include <vector>void 下载任务(int 文件编号) {std::cout << "线程" << std::this_thread::get_id() << "开始下载文件" << 文件编号 << std::endl;// 模拟下载时间std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "文件" << 文件编号 << "下载完成" << std::endl;
}int main() {std::vector<std::thread> 下载线程组;// 创建5个下载线程for (int i = 1; i <= 5; i++) {下载线程组.push_back(std::thread(下载任务, i));}// 等待所有下载完成for (auto& 线程 : 下载线程组) {线程.join();}std::cout << "所有文件下载完成!" << std::endl;return 0;
}

5.2 多进程任务处理(模拟)

cpp

#include <unistd.h>
#include <sys/wait.h>
#include <iostream>void 处理任务(int 任务编号) {std::cout << "进程" << getpid() << "处理任务" << 任务编号 << std::endl;sleep(2);  // 模拟处理时间std::cout << "任务" << 任务编号 << "处理完成" << std::endl;
}int main() {for (int i = 1; i <= 3; i++) {pid_t 子进程 = fork();if (子进程 == 0) {// 子进程处理任务处理任务(i);exit(0);  // 任务完成,子进程退出}}// 父进程等待所有子进程for (int i = 0; i < 3; i++) {wait(nullptr);}std::cout << "所有任务处理完成!" << std::endl;return 0;
}

6. 面试重点总结

创建线程的关键点:

  1. 包含头文件#include <thread>

  2. 选择任务类型:普通函数、lambda、成员函数

  3. 创建线程对象std::thread t(任务函数)

  4. 管理线程:用 join() 等待或用 detach() 分离

创建进程的关键点:

  1. 使用 fork():创建进程副本

  2. 判断返回值:0表示子进程,>0表示父进程

  3. 使用 exec:在子进程中执行新程序

  4. 等待子进程:父进程用 wait() 等待子进程结束

选择依据:

  • 需要共享数据、频繁通信 → 用线程

  • 需要稳定性、安全性 → 用进程

  • 任务简单、创建频繁 → 用线程

  • 任务复杂、相互独立 → 用进程

Linux 创建新进程详解(超详细版)

1. 进程的基本概念

什么是进程?

想象一个独立的王国

  • 每个进程就是一个独立的王国

  • 有自己的领土(内存空间)

  • 有自己的法律(执行代码)

  • 有自己的资源(文件、设备)

  • 王国之间相互隔离,不能随意访问

进程的组成部分:

  • 代码段:要执行的指令

  • 数据段:全局变量、静态变量

  • :动态分配的内存

  • :函数调用、局部变量

  • 文件描述符表:打开的文件

  • 进程控制块:进程的身份证

2. fork() 系统调用详解

2.1 fork() 的基本工作原理

fork() 就像细胞分裂:

text

父进程(细胞A)↓ fork()
父进程(细胞A) + 子进程(细胞B)

代码示例:

c

#include <stdio.h>
#include <unistd.h>int main() {printf("准备分裂进程...\n");// 关键的一步:分裂!pid_t pid = fork();if (pid == -1) {printf("分裂失败!\n");} else if (pid == 0) {// 子进程区域printf("👶 我是子进程,我的PID是:%d\n", getpid());printf("👶 我的父进程PID是:%d\n", getppid());} else {// 父进程区域printf("👨 我是父进程,我的PID是:%d\n", getpid());printf("👨 我创建的子进程PID是:%d\n", pid);}return 0;
}

运行结果可能是:

text

准备分裂进程...
👨 我是父进程,我的PID是:1234
👨 我创建的子进程PID是:1235
👶 我是子进程,我的PID是:1235  
👶 我的父进程PID是:1234

2.2 fork() 的返回值详解

三种返回值情况:

返回值含义执行代码
-1创建失败错误处理代码
0当前是子进程子进程的代码
>0当前是父进程,返回值是子进程PID父进程的代码

重要理解:

  • fork() 调用一次,返回两次

  • 在父进程和子进程中各返回一次

  • 通过返回值区分当前是父进程还是子进程

3. 写时复制(Copy-on-Write)机制

3.1 什么是写时复制?

传统复制(浪费资源):

text

父进程内存: [数据A][数据B][数据C]↓ 完全复制
子进程内存: [数据A][数据B][数据C]  ← 立即复制所有内容

写时复制(聪明高效):

text

父进程内存: [数据A][数据B][数据C]↓ 共享
子进程内存: [数据A][数据B][数据C]  ← 刚开始共享同一块内存↓ 子进程修改数据B
子进程内存: [数据A][新数据B][数据C]  ← 只有修改时才复制

3.2 写时复制的实际例子

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int global_var = 100;  // 全局变量int main() {int local_var = 200;  // 局部变量int *heap_var = malloc(sizeof(int));  // 堆变量*heap_var = 300;printf("创建进程前:global=%d, local=%d, heap=%d\n", global_var, local_var, *heap_var);pid_t pid = fork();if (pid == 0) {// 子进程修改所有变量global_var = 101;local_var = 201;*heap_var = 301;printf("子进程修改后:global=%d, local=%d, heap=%d\n", global_var, local_var, *heap_var);} else {// 父进程等待子进程完成wait(NULL);printf("父进程看到的:global=%d, local=%d, heap=%d\n", global_var, local_var, *heap_var);}free(heap_var);return 0;
}

运行结果:

text

创建进程前:global=100, local=200, heap=300
子进程修改后:global=101, local=201, heap=301
父进程看到的:global=100, local=200, heap=300

关键理解:

  • 子进程修改变量时,系统才真正复制内存

  • 父进程看到的还是原来的值

  • 这大大提高了 fork() 的效率

4. 进程间的继承关系

4.1 子进程继承父进程的哪些东西?

完全继承的资源:

  • 代码段(只读)

  • 数据段、堆、栈(写时复制)

  • 打开的文件描述符

  • 当前工作目录

  • 环境变量

  • 信号处理设置

不继承的资源:

  • 进程ID(PID)

  • 父进程ID(PPID)

  • 挂起的信号

  • 文件锁

  • 定时器

  • 内存锁

4.2 文件描述符的继承

c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>int main() {// 父进程打开一个文件int fd = open("test.txt", O_CREAT | O_WRONLY, 0644);write(fd, "父进程写入\n", 12);pid_t pid = fork();if (pid == 0) {// 子进程可以写入同一个文件write(fd, "子进程写入\n", 12);close(fd);  // 子进程关闭文件} else {wait(NULL);write(fd, "父进程再次写入\n", 15);close(fd);  // 父进程关闭文件}return 0;
}

文件内容:

text

父进程写入
子进程写入
父进程再次写入

5. 进程生命周期管理

5.1 等待子进程结束

为什么要等待?

  • 避免僵尸进程(子进程结束但父进程没回收)

  • 获取子进程的退出状态

  • 协调父子进程的执行顺序

wait() 函数:

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程工作3秒printf("子进程开始工作...\n");sleep(3);printf("子进程工作完成!\n");return 42;  // 子进程退出码} else {// 父进程等待子进程printf("父进程等待子进程...\n");int status;pid_t finished_pid = wait(&status);if (WIFEXITED(status)) {printf("子进程 %d 正常结束,退出码:%d\n", finished_pid, WEXITSTATUS(status));}}return 0;
}

5.2 更精细的等待控制

waitpid() 函数:

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid1 = fork();pid_t pid2 = fork();  // 创建多个子进程if (pid1 == 0) {sleep(1);printf("第一个子进程结束\n");return 1;} else if (pid2 == 0) {sleep(2);printf("第二个子进程结束\n");return 2;} else {// 父进程等待特定子进程int status;// 等待第一个子进程waitpid(pid1, &status, 0);printf("收到第一个子进程退出码:%d\n", WEXITSTATUS(status));// 等待第二个子进程waitpid(pid2, &status, 0);printf("收到第二个子进程退出码:%d\n", WEXITSTATUS(status));}return 0;
}

6. exec 系列函数:替换进程映像

6.1 exec 的基本概念

exec 的作用:

  • 不创建新进程

  • 用新程序替换当前进程的代码、数据、堆栈

  • 保持相同的进程ID

  • 就像给进程"换脑子"

6.2 exec 函数家族

函数名参数传递方式是否搜索PATH环境变量
execl参数列表继承
execlp参数列表继承
execle参数列表自定义
execv参数数组继承
execvp参数数组继承
execvpe参数数组自定义

6.3 exec 使用示例

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程替换为 ls 命令// 方法1:execl(参数列表)execl("/bin/ls", "ls", "-l", "-a", NULL);// 方法2:execv(参数数组)// char *args[] = {"ls", "-l", "-a", NULL};// execv("/bin/ls", args);// 方法3:execlp(自动搜索PATH)// execlp("ls", "ls", "-l", "-a", NULL);// 如果exec成功,下面的代码不会执行printf("如果看到这句话,说明exec失败了!\n");return 1;} else {// 父进程等待子进程wait(NULL);printf("子进程执行完成\n");}return 0;
}

7. fork() + exec() 的经典组合

7.1 为什么需要这个组合?

单独使用 fork():

  • 只能创建当前程序的副本

  • 不能运行其他程序

单独使用 exec():

  • 会替换当前进程

  • 当前程序就消失了

fork() + exec():

  • 创建新进程(fork)

  • 在新进程中运行其他程序(exec)

  • 父进程继续运行

7.2 完整示例

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {printf("父进程启动,PID=%d\n", getpid());pid_t pid = fork();if (pid == 0) {// 子进程:运行 date 命令printf("子进程启动,准备运行 date 命令\n");execl("/bin/date", "date", NULL);// 如果exec失败perror("exec失败");return 1;} else {// 父进程:等待子进程printf("父进程等待子进程 %d...\n", pid);int status;wait(&status);if (WIFEXITED(status)) {printf("子进程正常结束\n");}}printf("父进程继续工作...\n");return 0;
}

8. 实际应用场景

8.1 Web 服务器创建子进程

c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>void handle_client(int client_socket) {// 模拟处理客户端请求printf("处理客户端请求...\n");sleep(2);printf("请求处理完成\n");
}int main() {// 模拟服务器主循环for (int i = 1; i <= 3; i++) {printf("收到新客户端连接 #%d\n", i);pid_t pid = fork();if (pid == 0) {// 子进程处理客户端printf("子进程 %d 处理客户端 %d\n", getpid(), i);handle_client(i);  // 模拟处理printf("子进程 %d 完成\n", getpid());return 0;  // 子进程退出}}// 父进程等待所有子进程for (int i = 0; i < 3; i++) {wait(NULL);}printf("所有客户端请求处理完成\n");return 0;
}

9. 常见问题和注意事项

9.1 避免僵尸进程

错误做法:

c

// 创建子进程后不等待
fork();
// 子进程变成僵尸

正确做法:

c

pid_t pid = fork();
if (pid > 0) {wait(NULL);  // 等待子进程
}

9.2 文件描述符管理

问题: 子进程继承所有打开的文件描述符

解决方案:

c

// 在fork之前关闭不需要的文件
close(unneeded_fd);
pid_t pid = fork();

9.3 信号处理

问题: 子进程继承信号处理程序

解决方案:

c

// 在子进程中重新设置信号处理
if (pid == 0) {signal(SIGINT, SIG_DFL);  // 恢复默认处理
}

10. 总结

fork() 的核心要点:

  1. 一次调用,两次返回 - 在父子进程中各返回一次

  2. 写时复制 - 高效的内存管理机制

  3. 完全继承 - 子进程继承父进程的大部分资源

  4. 独立运行 - 父子进程并发执行

使用模式:

  • fork() - 创建进程副本

  • fork() + exec() - 运行新程序

  • wait() - 协调父子进程

  • exit() - 正常结束进程

http://www.dtcms.com/a/561746.html

相关文章:

  • 【C语言基础案例】经典C语言程序设计100例附源码解析(91-100例)
  • 子目录创建网站wordpress html5视频播放插件
  • 武夷山网站设计沈阳网站seo排名优化
  • 湘潭市建设路学校网站拍卖网站功能需求文档
  • 优秀北京网站建设深圳龙华区龙华街道高坳新村
  • 计算机网络自顶向下方法26——运输层 SYN洪泛攻击 SYNCookie
  • 【RL】以信息熵的角度理解RL
  • linux下讲解基础IO
  • 乌兰察布网站建设桂林漓江图片高清
  • Docker革命:软件开发的集装箱时代
  • 北京移动官网网站建设商务网站建设注意事项
  • 某网站的安全建设方案纪念平台网站建设
  • 定州网站制作潍坊网站制作人才招聘
  • 【C语言基础案例】经典C语言程序设计100例附源码解析(21-30例)
  • 网站建设需要缴纳印花税么邢台瑞光网络科技有限公司
  • 2025 年山西省职业院校技能大赛(高职教师组)移动应用设计与开发赛项样题
  • 证券投资网站做哪些内容做网站简单的软件
  • 网站建设费的分录怎么写济南知名网站建设平台
  • 『 数据库 』MySQL复习 - 查询进阶指南:基于经典测试表的复合查询实践
  • openpi π 0.5复现 实战
  • git命令和markdown语法参考
  • 域名如何跟网站绑定网站托管怎做
  • 怎样可以快速增加网站的反链寮步网站建设哪家好
  • 四.docker容器数据卷
  • Sora 2 引爆后,AI 视频赛道正进入「超级加速」
  • 二叉树最小深度解题思路
  • 网站建设与开发 期末作品公司网站更换域名流程
  • 佛山网站建设在哪班级优化大师手机版下载
  • 如何在VScode环境下使用git进行版本控制,并上传到gitee远程仓库
  • 个人网站开发项目报告数据库营销