C++ 多线程与 Linux 进程创建详解
1. 理解线程和进程的基本概念
什么是进程?
想象一个工厂:
每个进程就像一个独立的工厂
有自己独立的土地(内存空间)
有自己的工人(线程)
工厂之间相互隔离,不能直接访问对方的设备
什么是线程?
想象工厂里的工人:
线程是进程内部的执行单元
同一个工厂里的工人共享所有资源
工人们可以同时做不同的工作
工人们可以方便地互相沟通
2. 创建新线程的详细讲解
2.1 使用 C++ 标准库创建线程(推荐)
基本步骤:
包含线程头文件
定义线程要执行的任务
创建线程对象
管理线程的生命周期
通俗解释:
想象你要请一个帮手(线程)来帮你做家务:
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. 面试重点总结
创建线程的关键点:
包含头文件:
#include <thread>选择任务类型:普通函数、lambda、成员函数
创建线程对象:
std::thread t(任务函数)管理线程:用
join()等待或用detach()分离
创建进程的关键点:
使用 fork():创建进程副本
判断返回值:0表示子进程,>0表示父进程
使用 exec:在子进程中执行新程序
等待子进程:父进程用
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() 的核心要点:
一次调用,两次返回 - 在父子进程中各返回一次
写时复制 - 高效的内存管理机制
完全继承 - 子进程继承父进程的大部分资源
独立运行 - 父子进程并发执行
使用模式:
fork() - 创建进程副本
fork() + exec() - 运行新程序
wait() - 协调父子进程
exit() - 正常结束进程
