嵌入式多进程开发fork 创建与进程资源管理一、引言上一篇文章我们讲了文件 IO——这是 Linux 编程的单打独斗。但在嵌入式系统中一个采集任务同时还要做数据处理和网络上报怎么办答案是多进程。所谓进程简单说就是运行中的程序。它拥有独立的地址空间、文件描述符表、堆栈。本文从fork的原理讲起拆解子进程的创建与资源继承再手把手处理僵尸进程这个嵌入式开发中高频出现的玄学问题。二、fork 的原理fork()是 Linux 中唯一能创建新进程的系统调用#includeunistd.hpid_tfork(void);神奇之处在于fork调用一次返回两次在父进程中返回值是子进程的 PID0在子进程中返回值是 0出错时在父进程中返回 -1pid_tpidfork();if(pid0){perror(fork failed);exit(1);}elseif(pid0){// 子进程代码区printf(我是子进程, PID%d\n,getpid());}else{// 父进程代码区printf(我是父进程子进程 PID%d\n,pid);}Copy-On-Write写时复制是 fork 高效的核心技术。fork创建子进程时并不会立即复制父进程的全部内存页而是让父子共享同一套物理页标记为只读。只有当某一方尝试写入时内核才真正分配新的物理页并拷贝内容。所以fork的创建成本很低但内存用量会随着实际写入逐渐增长。用流程图来描述整个 fork 过程父进程 (PID1000) | | fork() | \ | \ | 子进程 (PID1001) | | fork 返回 pid1001 fork 返回 pid0 | (执行父进程代码) (执行 if(pid0) 分支) | v v 继续运行 执行子进程逻辑三、子进程继承了什么fork后子进程并不是一张白纸它会继承父进程的很多资源继承的资源说明打开的文件描述符父子指向同一个内核文件表项共享偏移量环境变量子进程拿到完整副本信号处理设置继承父进程的信号处理函数指针当前工作目录和父进程同一个目录用户 ID、组 ID权限一致进程组 ID/session ID继承不继承的资源说明父进程 IDPPID子进程的 PPID 是父进程的 PID挂起的信号子进程的信号等待队列是空的定时器alarm/timer_create 产生的定时器不被继承内存锁mlock/mlockall 锁定的内存重点文件描述符继承带来一个常见问题——父子共享文件偏移量。父进程lseek到 100 后子进程读到的位置也是 100。intfdopen(data.bin,O_RDONLY);pid_tpidfork();if(pid0){// 子进程lseek(fd,0,SEEK_SET);// 回绕到开头read(fd,buf,64);// 读取前64字节}else{// 父进程read(fd,buf2,64);// 从子进程读到的位置继续}要避免这种互相干扰最好的办法就是在fork之后、处理数据之前重新open文件。四、僵尸进程嵌入式开发的幽灵所谓僵尸进程Zombie是指子进程已经结束但父进程还没有调用wait回收其退出状态的一种特殊状态。$psaux|grepdefunct root12340.00.000? Z10:000:00[sensor_daemon]defunct看到状态Z就说明是僵尸进程。它不占用 CPU 和内存除了 PCB 结构体但会占用 PID 资源。在长时间运行的嵌入式设备中PID 号用完了就无法创建新进程导致系统僵死。#includesys/wait.hintmain(void){pid_tpidfork();if(pid0){// 子进程干完活就退出sleep(2);printf(子进程退出\n);exit(0);}// 父进程忘记 wait —— 产生僵尸进程sleep(10);printf(父进程退出\n);return0;}运行这段代码在sleep(10)期间用ps aux | grep defunct就能看到僵尸进程。正确的回收方式pid_tpidfork();if(pid0){// 子进程逻辑...exit(0);}// 父进程等待子进程结束intstatus;pid_tretwaitpid(pid,status,0);if(retpid){if(WIFEXITED(status)){// 正常退出,获取退出码intexit_codeWEXITSTATUS(status);printf(子进程正常退出, 退出码%d\n,exit_code);}elseif(WIFSIGNALED(status)){// 被信号杀死intsigWTERMSIG(status);printf(子进程被信号 %d 杀死\n,sig);}}WIFEXITED、WIFSIGNALED、WEXITSTATUS这些宏是解析退出状态的标配一定要会用。五、避免僵尸进程的工程实践方案1父进程 waitpid适合父进程能控制子进程生命周期的场景voidcreate_worker(void){pid_tpidfork();if(pid0){// 子进程工作do_work();exit(0);}// 父进程等待waitpid(pid,NULL,0);}方案2忽略 SIGCHLD 信号#includesignal.h// 在程序初始化时signal(SIGCHLD,SIG_IGN);子进程退出时会向父进程发 SIGCHLD 信号设置为忽略后内核会自动回收子进程不会产生僵尸。方案3两次 fork孤儿进程pid_tpidfork();if(pid0){// 第一次子进程pid_tpid2fork();if(pid20){// 第二次子进程——真正的打工人do_work();exit(0);}// 第一次子进程立即退出// 第二次子进程变成孤儿进程,被 init 收养exit(0);}// 父进程只回收第一个子进程waitpid(pid,NULL,0);这个技巧常用于创建守护进程让子进程脱离终端、交由 init 进程PID1管理。六、总结fork是 Linux 多进程编程的入口。理解它的 5 个要点fork 调用一次返回两次——通过返回值区分父子Copy-On-Write——fork 的高效秘诀文件描述符继承——多进程要注意共享偏移量僵尸进程——嵌入式长期运行的致命威胁waitpid / 信号 / 两次 fork——三种防僵尸手段掌握这些你就从单线程思维迈入了多任务编程的世界。