今天继续更新:多线程编程(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>waitwaitpid的头文件。
  • 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 耗尽,新进程无法创建。
  • waitpidWNOHANG参数:表示 “非阻塞等待”(父进程可边等边处理硬件中断),嵌入式中常用于 “多任务并发监控”。
面试高频问题
  • 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 和打开的文件描述符。

模块三:守护进程 ——“后台默默干活的服务”

核心价值:实现嵌入式系统的 “后台服务”(如传感器采集服务、网络心跳守护)。

原理通俗解释

守护进程是在后台运行、脱离终端、不受用户登录退出影响的进程。创建步骤:

  1. fork子进程,父进程退出(子进程成为孤儿进程,被init进程收养)。
  2. 子进程调用setsid,创建新会话,脱离原终端。
  3. 改变工作目录(防止原目录被卸载)。
  4. 重定向标准输入 / 输出 / 错误到/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, &param);
    
    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_joinwait的区别?pthread_join回收线程资源,wait回收进程资源;线程是进程内的执行单元,进程是独立的资源单位。
  • :嵌入式中如何避免线程内存泄漏?:线程内分配的堆内存退出前必须free;用内存池管理频繁分配的内存;集成valgrind或自定义工具定期扫描泄漏。

总结

掌握进程创建与回收、exec 函数族、守护进程、GDB 多进程调试、线程创建与回收后,你已具备嵌入式 “多任务编程” 的核心能力。结合嵌入式场景的资源限制、硬件交互、实时性要求,完全可以胜任我们现在学的知识点!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐