字节技术总监推荐硬核笔记-多线程编程(1):线程 创建-execFunc-守护-GDB调试-进程创建-回收-取消 后续锁 互斥 管道 共享内存待更新
本文系统讲解了嵌入式开发中进程与线程编程的核心技术。在进程管理方面,详细介绍了fork()创建进程、wait/waitpid回收进程、exec函数族程序替换以及守护进程的实现方法,并重点分析了僵尸进程的危害及解决方法。在线程编程方面,讲解了pthread_create创建线程、参数传递技巧、pthread_join线程回收以及内存管理等关键内容。特别针对嵌入式开发场景,强调了资源限制、硬件交互等注
今天继续更新:多线程编程(1)
![]()
废话少说 开干!!!!!!!
模块一:进程创建与回收(fork/wait/waitpid)
核心价值:实现嵌入式系统的 “多任务协作”(如传感器采集、网络通信并行执行)。
一、进程创建:fork()——“克隆自己干多活”
原理通俗解释
fork()是 “克隆进程” 的操作:调用后操作系统会复制当前进程的代码、数据、栈、打开的文件等资源,生成一个 “子进程”。父子进程几乎一样,但有两个关键区别:
- 父进程的
fork()返回子进程的 PID(进程 ID)。 - 子进程的
fork()返回0。
代码实战:最基础的父子进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid; // 存储进程ID的变量
// 克隆进程(父进程执行后,子进程也会从这里开始执行)
pid = fork();
if (pid < 0) {
// fork失败(如内存不足)
printf("进程创建失败\n");
return -1;
} else if (pid == 0) {
// 子进程分支:pid=0
printf("我是子进程,我的PID是 %d,父进程PID是 %d\n",
getpid(), getppid());
} else {
// 父进程分支:pid=子进程的PID
printf("我是父进程,我的PID是 %d,子进程PID是 %d\n",
getpid(), pid);
sleep(2); // 父进程休眠2秒,确保子进程先输出
}
return 0;
}
逐行通俗解释
pid_t pid;:pid_t是进程 ID 的类型(本质是整数),用来区分父子进程。pid = fork();:执行后,操作系统会 “复制” 当前进程,父子进程从此处并行执行。if (pid == 0):子进程的逻辑分支,getpid()获取自己的 PID,getppid()获取父进程的 PID。else:父进程的逻辑分支,pid变量存储的是子进程的 PID。sleep(2);:父进程休眠 2 秒,确保子进程先打印,方便观察运行顺序。
嵌入式考点
- 子进程会复制父进程的文件描述符(比如父进程打开的传感器设备,子进程可直接读写)。
- 父子进程共享文件偏移量(父进程读了传感器前 10 字节,子进程再读会从第 11 字节开始)。
二、进程回收:wait()与waitpid()——“等子进程干完活”
如果父进程不回收子进程,子进程会变成僵尸进程(占用 PID 资源,导致系统无法创建新进程)。wait()和waitpid()是父进程 “等子进程退出并回收资源” 的工具。
代码实战:用wait()回收子进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid, wait_pid;
int status; // 存储子进程退出状态
pid = fork();
if (pid < 0) {
printf("fork失败\n");
return -1;
} else if (pid == 0) {
// 子进程:模拟“干活3秒”后退出
printf("子进程开始干活,3秒后退出\n");
sleep(3);
printf("子进程干完活,退出\n");
return 123; // 子进程退出码(自定义,0表示正常)
} else {
// 父进程:等子进程退出
printf("父进程开始等子进程退出...\n");
wait_pid = wait(&status); // 阻塞等待,直到子进程退出
if (WIFEXITED(status)) { // 判断子进程是否正常退出
printf("子进程 %d 正常退出,退出码是 %d\n",
wait_pid, WEXITSTATUS(status));
} else {
printf("子进程异常退出\n");
}
}
return 0;
}
逐行通俗解释
#include <sys/wait.h>:wait和waitpid的头文件。int status;:存储子进程的退出状态(需用宏解析)。return 123;:子进程的 “退出码”,父进程可通过它判断子进程的退出原因。wait_pid = wait(&status);:父进程阻塞在这里,直到子进程退出。wait_pid是退出的子进程 PID。WIFEXITED(status):宏,判断子进程是否 “正常退出”(非被信号杀死)。WEXITSTATUS(status):宏,提取子进程的退出码(如示例中的 123)。
代码实战:用waitpid()灵活回收(指定子进程、非阻塞)
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid1, pid2, wait_pid;
int status;
// 创建两个子进程
pid1 = fork();
if (pid1 == 0) {
printf("子进程1(PID=%d)开始干活,5秒后退出\n", getpid());
sleep(5);
return 1;
}
pid2 = fork();
if (pid2 == 0) {
printf("子进程2(PID=%d)开始干活,2秒后退出\n", getpid());
sleep(2);
return 2;
}
// 父进程:先等子进程2,再等子进程1
wait_pid = waitpid(pid2, &status, 0); // 阻塞等pid2
if (WIFEXITED(status)) {
printf("回收子进程2(PID=%d),退出码=%d\n", wait_pid, WEXITSTATUS(status));
}
wait_pid = waitpid(pid1, &status, 0); // 阻塞等pid1
if (WIFEXITED(status)) {
printf("回收子进程1(PID=%d),退出码=%d\n", wait_pid, WEXITSTATUS(status));
}
return 0;
}
嵌入式考点
- 僵尸进程的危害:嵌入式系统资源有限,大量僵尸进程会导致 PID 耗尽,新进程无法创建。
waitpid的WNOHANG参数:表示 “非阻塞等待”(父进程可边等边处理硬件中断),嵌入式中常用于 “多任务并发监控”。
面试高频问题
- 问:
fork后父子进程的执行顺序?答:由操作系统调度决定,若要控制顺序,需用wait或信号量同步。 - 问:嵌入式中用
fork要注意什么?答:嵌入式内存小,fork会复制父进程所有资源,易导致内存不足。建议仅在必要时创建子进程,且子进程尽量少分配内存。
模块二:exec 函数族 ——“换个程序接着跑”
核心价值:在当前进程中运行另一个程序(如嵌入式中 “进程 A 启动固件升级程序 B”)。
原理通俗解释
exec函数族的作用是 **“程序替换”:当前进程的代码、数据、栈等会被新程序完全替换,仅保留进程 ID(PID)和打开的文件描述符 **。
常用exec函数:
execl:“l” 表示参数列表(可变参数传参)。execv:“v” 表示参数数组(用数组传参)。execvp:“p” 表示从PATH 环境变量找程序。
代码实战:execl执行系统命令(如ls)
c
运行
#include <stdio.h>
#include <unistd.h>
int main() {
printf("执行前:当前进程PID=%d\n", getpid());
// 执行ls -l命令(execl参数格式:程序路径, 程序名, 参数1, 参数2, ..., NULL)
execl("/bin/ls", "ls", "-l", NULL);
// 若execl成功,下面代码不会执行(程序已被替换)
printf("若看到这句话,说明execl执行失败\n");
return 0;
}
逐行通俗解释
execl("/bin/ls", "ls", "-l", NULL);:- 第一个参数
/bin/ls是程序的绝对路径。 - 第二个参数
"ls"是程序名(传入新程序的argv[0])。 - 第三个参数
"-l"是命令行参数。 - 必须以
NULL结尾,标记参数列表结束。
- 第一个参数
printf("若看到这句话..."):若execl成功,当前进程代码被ls替换,这句话不会执行;若失败(如路径错误)才会执行。
代码实战:execv执行自定义程序
假设我们有一个hello.c:
c
运行
// hello.c
#include <stdio.h>
int main(int argc, char* argv[]) {
printf("我是新程序,PID=%d\n", getpid());
if (argc > 1) {
printf("收到参数:%s\n", argv[1]);
}
return 0;
}
编译为hello后,用execv执行:
c
运行
#include <stdio.h>
#include <unistd.h>
int main() {
char* argv[] = {"hello", "嵌入式14koffer", NULL}; // 参数数组,以NULL结尾
printf("执行前:PID=%d\n", getpid());
execv("./hello", argv); // 执行当前目录下的hello程序
printf("execv执行失败\n");
return 0;
}
嵌入式考点
exec会保留打开的文件描述符:父进程打开的传感器设备,替换程序后仍可访问。- 嵌入式中常用于启动 “工具程序”(如固件升级脚本、硬件诊断程序)。
面试高频问题
- 问:
exec成功后,原进程的代码还在吗?答:不在了,原进程的代码、数据、栈会被新程序完全替换,仅保留 PID 和打开的文件描述符。
模块三:守护进程 ——“后台默默干活的服务”
核心价值:实现嵌入式系统的 “后台服务”(如传感器采集服务、网络心跳守护)。
原理通俗解释
守护进程是在后台运行、脱离终端、不受用户登录退出影响的进程。创建步骤:
fork子进程,父进程退出(子进程成为孤儿进程,被init进程收养)。- 子进程调用
setsid,创建新会话,脱离原终端。 - 改变工作目录(防止原目录被卸载)。
- 重定向标准输入 / 输出 / 错误到
/dev/null(避免终端干扰)。
代码实战:创建简单守护进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void create_daemon() {
pid_t pid;
// 步骤1:fork子进程,父进程退出
pid = fork();
if (pid > 0) {
printf("父进程退出,PID=%d\n", getpid());
return;
} else if (pid < 0) {
perror("fork失败");
return;
}
// 子进程继续执行(此时是孤儿进程,被init收养)
printf("子进程启动,PID=%d,父进程PID=%d(init进程)\n",
getpid(), getppid());
// 步骤2:创建新会话,脱离原终端
setsid();
// 步骤3:改变工作目录到根目录(/)
chdir("/");
// 步骤4:重定向标准输入/输出/错误到/dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO); // 标准输入重定向到/dev/null
dup2(fd, STDOUT_FILENO); // 标准输出重定向到/dev/null
dup2(fd, STDERR_FILENO); // 标准错误重定向到/dev/null
close(fd);
// 守护进程核心逻辑:模拟“每10秒写一次日志”
while (1) {
sleep(10);
printf("守护进程在后台干活...\n"); // 输出被重定向到/dev/null,实际看不到
}
}
int main() {
create_daemon();
return 0;
}
逐行通俗解释
pid = fork(); if (pid > 0) return;:父进程退出,子进程成为孤儿进程,由init进程(PID=1)收养。setsid();:子进程创建新会话,成为会话组长,彻底脱离原终端(否则终端关闭会导致进程退出)。chdir("/");:防止原工作目录被卸载(如 U 盘挂载的目录)。open("/dev/null", O_RDWR); dup2(...):/dev/null是 “黑洞设备”,所有输入输出会被丢弃,确保守护进程不被终端干扰。
嵌入式考点
- 守护进程的日志输出:不能用
printf(会被重定向),需写入专门的日志文件(如/var/log/sensor.log)。 - 嵌入式常用场景:网络心跳检测、硬件看门狗、自动升级服务。
面试高频问题
- 问:守护进程为什么要
fork两次?答:第一次fork让子进程成为孤儿进程(被init收养);第二次fork(部分实现)让进程不再是会话组长,彻底避免终端影响。
模块四:GDB 调试多进程程序 ——“同时盯多个任务”
核心价值:嵌入式开发中需同时调试父子进程或多个独立进程,GDB 多进程调试是必备技能。
调试步骤:以父子进程为例
先写一个多进程程序multi_process.c:
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
// 子进程:循环打印
for (int i = 0; i < 5; i++) {
printf("子进程:i=%d\n", i);
sleep(1);
}
} else {
// 父进程:循环打印
for (int i = 0; i < 5; i++) {
printf("父进程:i=%d\n", i);
sleep(1);
}
wait(NULL);
}
return 0;
}
编译时加调试信息:gcc -g -o multi_process multi_process.c
步骤 1:启动 GDB 并设置跟踪模式
bash
gdb ./multi_process
(gdb) set follow-fork-mode child # 跟踪子进程(默认跟踪父进程)
(gdb) run
步骤 2:查看当前调试的进程
gdb
(gdb) info inferiors
Num PID Status Command
* 1 1235 running ./multi_process
*表示当前正在调试的进程(子进程)。
步骤 3:切换调试进程
gdb
(gdb) inferior 2 # 假设父进程的inferior编号是2(需先找到父进程PID)
步骤 4:设置断点、单步调试
在子进程的printf处设断点:
gdb
(gdb) b multi_process.c:8
(gdb) c # 继续运行,直到断点
嵌入式调试技巧
- 若要调试多个独立进程,可先启动 GDB,再用
attach 进程PID附加到目标进程。 - 嵌入式板卡上调试多进程时,结合
gdbserver的--multi模式,支持同时调试多个进程。
模块五:线程创建与参数传递(pthread_create)
核心价值:实现嵌入式系统的 “轻量级多任务”(如实时数据处理、硬件中断响应)。
原理通俗解释
线程是进程内的 “子任务”,共享进程的内存空间(代码、数据、堆、打开的文件),但有独立的栈。pthread_create用于创建线程,参数包括:线程 ID、线程属性、线程函数、线程函数的参数。
代码实战:创建线程并传递参数
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程函数:接收void*参数,返回void*
void* thread_func(void* arg) {
int* num = (int*)arg; // 强转为int指针,获取参数
printf("线程启动,收到参数:%d\n", *num);
// 线程核心逻辑:模拟处理传感器数据
for (int i = 0; i < 3; i++) {
printf("线程干活中:%d\n", i);
sleep(1);
}
return (void*)0; // 线程退出码(0表示正常)
}
int main() {
pthread_t tid; // 线程ID
int arg = 123; // 要传递给线程的参数
// 创建线程:参数为线程ID、默认属性、线程函数、参数
int ret = pthread_create(&tid, NULL, thread_func, &arg);
if (ret != 0) {
printf("线程创建失败\n");
return -1;
}
printf("主线程继续干活,等待线程退出\n");
sleep(5); // 主线程休眠,确保线程执行完
return 0;
}
逐行通俗解释
void* thread_func(void* arg):线程函数格式固定,参数和返回值都是void*(可传递任意类型数据)。int* num = (int*)arg;:将void*强转为int*,获取传递的参数。pthread_t tid;:线程 ID 的类型(与进程 PID 不同,是线程的唯一标识)。pthread_create(&tid, NULL, thread_func, &arg);:- 第一个参数
&tid:存储线程 ID。 - 第二个参数
NULL:使用默认线程属性。 - 第三个参数
thread_func:线程要执行的函数。 - 第四个参数
&arg:传递给线程函数的参数地址。
- 第一个参数
sleep(5);:主线程休眠 5 秒,确保子线程有时间执行(否则主线程提前退出,子线程也会被终止)。
代码实战:传递复杂参数(结构体)
嵌入式中常需传递 “多个关联参数”(如传感器 ID、采样率),用结构体封装更方便。
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 定义参数结构体
typedef struct {
int sensor_id; // 传感器ID
int sample_rate; // 采样率(Hz)
char buffer[100];// 数据缓冲区
} SensorParam;
void* thread_func(void* arg) {
SensorParam* param = (SensorParam*)arg; // 强转为结构体指针
printf("线程启动:传感器ID=%d,采样率=%d\n",
param->sensor_id, param->sample_rate);
// 模拟采集数据
sprintf(param->buffer, "传感器%d的采集数据", param->sensor_id);
printf("采集数据:%s\n", param->buffer);
return (void*)0;
}
int main() {
pthread_t tid;
SensorParam param = {1, 10, {0}}; // 初始化参数
pthread_create(&tid, NULL, thread_func, ¶m);
printf("主线程等待...\n");
sleep(2);
return 0;
}
嵌入式考点
- 线程共享进程的文件描述符:主线程打开的传感器设备,子线程可直接读写。
- 线程共享全局变量:需加锁(如
pthread_mutex_t)防止竞争。
面试高频问题
- 问:线程和进程的区别?答:进程是资源分配单位,线程是调度执行单位;进程间内存独立,线程共享进程内存;进程创建开销大,线程创建开销小。
- 问:嵌入式中什么时候用线程,什么时候用进程?答:
- 线程:需要高并发、低延迟(如实时数据处理),且任务间需共享内存 / 资源。
- 进程:任务间需要隔离(一个任务崩溃不影响其他任务),或需要运行独立程序(如通过
exec启动)。
模块六:线程回收与内存管理(pthread_join)
核心价值:避免线程内存泄漏,确保嵌入式系统长期稳定运行。
原理通俗解释
pthread_join是线程版的 “wait”:阻塞等待指定线程退出,回收其栈资源,并获取线程的退出状态。
代码实战:用pthread_join回收线程
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("线程启动,开始干活\n");
sleep(3); // 模拟干活3秒
printf("线程干完活,退出\n");
return (void*)123; // 线程退出码
}
int main() {
pthread_t tid;
void* ret_val; // 存储线程退出状态
pthread_create(&tid, NULL, thread_func, NULL);
// 回收线程:阻塞等待,直到线程退出
pthread_join(tid, &ret_val);
printf("线程退出,退出码=%d\n", (int)ret_val);
return 0;
}
代码实战:线程内存管理(避免泄漏)
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* thread_func(void* arg) {
// 线程内分配堆内存
int* data = (int*)malloc(sizeof(int));
*data = 123;
printf("线程分配内存:%p,值=%d\n", data, *data);
// 线程退出前必须释放,否则内存泄漏
free(data);
printf("线程释放内存\n");
return (void*)0;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
printf("主线程结束,内存无泄漏\n");
return 0;
}
嵌入式考点
- 线程退出后,栈资源会自动回收,但堆内存(如
malloc分配的)需手动free,否则会内存泄漏。 - 嵌入式中可用
valgrind --leak-check=full ./prog检测内存泄漏。
面试高频问题
- 问:
pthread_join和wait的区别?答:pthread_join回收线程资源,wait回收进程资源;线程是进程内的执行单元,进程是独立的资源单位。 - 问:嵌入式中如何避免线程内存泄漏?答:线程内分配的堆内存退出前必须
free;用内存池管理频繁分配的内存;集成valgrind或自定义工具定期扫描泄漏。
总结
掌握进程创建与回收、exec 函数族、守护进程、GDB 多进程调试、线程创建与回收后,你已具备嵌入式 “多任务编程” 的核心能力。结合嵌入式场景的资源限制、硬件交互、实时性要求,完全可以胜任我们现在学的知识点!
更多推荐



所有评论(0)