嵌入式后台开发:守护进程制作与程序开机自启
嵌入式后台开发守护进程制作与程序开机自启一、引言前面九篇文章我们的示例代码都是前台运行的程序——启动后在终端里打印日志CtrlC 退出。但在嵌入式设备上传感器采集服务、网络通信服务、MQTT 消息处理服务都不是这么玩的。这些服务需要脱离终端独立运行、开机自动启动、崩溃后自动重启、可以优雅停止。这就是**守护进程Daemon**的用武之地。本文先拆解守护进程的原理和创建步骤再介绍三种主流的开机自启配置方式帮你把前面的劳动成果——不管是文件 IO、网络服务器还是 MQTT 客户端——都做成真正的企业级后台服务。二、守护进程是什么守护进程Daemon是在后台运行、不受终端控制的长期服务进程。它的名字来源于古希腊神话中的守护精灵。和普通进程的核心区别特性普通进程守护进程终端关联关联终端关闭进程退出脱离终端关闭不影响运行位置前台或后台纯后台输出目标标准输出/标准错误日志文件或 syslog父进程shell 或用户手动启动init/systemdPID1工作目录启动时的目录通常是根目录/一句话守护进程就是不需要人来盯着的后台服务——拿嵌入式传感器采集来说你不可能每次断电重启后手动 SSH 上开发板启动程序。三、制作守护进程的五步标准流程Richard Stevens 在《Unix 环境高级编程》中定义了守护进程创建的经典步骤被业界奉为守护进程圣经。步骤一fork 并退出父进程pid_tpidfork();if(pid0){exit(0);// 父进程退出}// 子进程继续目的让 shell 认为程序已经结束释放终端。步骤二创建新会话setsidif(setsid()0){exit(1);}sessid()让调用进程成为新会话和新进程组的首领彻底切断与旧终端的一切关联。步骤三再次 fork可选但推荐pidfork();if(pid0)exit(0);第二次 fork 后的孙子进程不再是会话首领永远不会意外获得控制终端。步骤四改变工作目录到根目录chdir(/);如果不改进程启动时的目录可能是挂载点将无法卸载。步骤五关闭不需要的文件描述符重定向标准 IOclose(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 重定向到 /dev/nullintfdopen(/dev/null,O_RDWR);dup2(fd,STDIN_FILENO);dup2(fd,STDOUT_FILENO);dup2(fd,STDERR_FILENO);if(fd2)close(fd);重要守护进程不应该往 stdout/stderr 输出——因为没有终端能接收了。日志要通过 syslog 或文件记录。完整创建函数#includestdio.h#includestdlib.h#includeunistd.h#includefcntl.h#includesys/stat.hvoiddaemonize(void){pid_tpid;// Step 1: fork, 父进程退出pidfork();if(pid0){perror(fork);exit(1);}if(pid0){exit(0);// 父进程退出,子进程变孤儿}// Step 2: 创建新会话if(setsid()0){perror(setsid);exit(1);}// Step 3: 再次 fork孙进程不再是会话首领pidfork();if(pid0){perror(fork#2);exit(1);}if(pid0){exit(0);// 第二次父进程退出}// Step 4: 切换到根目录chdir(/);// Step 5: 重设文件掩码umask(0);// Step 6: 关闭所有文件描述符intmax_fdsysconf(_SC_OPEN_MAX);for(intfd0;fdmax_fd;fd){close(fd);}// Step 7: 重定向标准 IO 到 /dev/nullintdev_nullopen(/dev/null,O_RDWR);if(dev_null0){dup2(dev_null,STDIN_FILENO);dup2(dev_null,STDOUT_FILENO);dup2(dev_null,STDERR_FILENO);if(dev_null2)close(dev_null);}// Step 8: 记录 PID 文件便于后续管理FILE*fpfopen(/var/run/mydaemon.pid,w);if(fp){fprintf(fp,%d\n,getpid());fclose(fp);}}使用示例把网络服务器做成守护进程#includesignal.hstaticvolatileintg_running1;staticvoidsig_handler(intsigno){if(signoSIGTERM||signoSIGINT){g_running0;// 优雅退出}}intmain(intargc,char*argv[]){// 安装信号处理器signal(SIGTERM,sig_handler);signal(SIGINT,sig_handler);// 变成守护进程if(argc2||strcmp(argv[1],-f)!0){daemonize();}// 打开日志文件守护进程后 stdout 已经没用FILE*log_fpfopen(/var/log/mydaemon.log,a);fprintf(log_fp,[启动] PID%d\n,getpid());fflush(log_fp);// 启动 TCP 服务 (省略了具体的 epoll 实现)intserver_fdtcp_server_init(8888);fprintf(log_fp,[就绪] 端口 8888\n);fflush(log_fp);// 主循环while(g_running){event_loop_iteration(server_fd);}// 清理fprintf(log_fp,[退出] 服务正常关闭\n);fflush(log_fp);close(server_fd);fclose(log_fp);unlink(/var/run/mydaemon.pid);return0;}-f参数foreground是守护进程开发的标配——开发调试时用-f跑在前台看日志发布后去掉-f跑在后台。四、PID 文件机制PID 文件是守护进程的身份证记录了当前运行的进程 ID用于管理脚本的启停控制#!/bin/sh# /etc/init.d/mydaemon 启动/停止脚本PID_FILE/var/run/mydaemon.pidAPP/usr/local/bin/mydaemonstart(){echoStarting mydaemon...$APPsleep1if[-f$PID_FILE];thenechoOK, PID$(cat$PID_FILE)elseechoFailedfi}stop(){if[-f$PID_FILE];thenPID$(cat$PID_FILE)echoStopping mydaemon (PID$PID)...kill$PIDrm-f$PID_FILEelseechoNot runningfi}case$1instart)start;;stop)stop;;restart)stop;start;;*)echoUsage:$0{start|stop|restart};;esac五、开机自启的三种方式方式一/etc/rc.local最简单在/etc/rc.local的exit 0之前添加/usr/local/bin/mydaemon优点零学习成本改完即生效缺点不支持崩溃重启没有进程监控方式二SysV init 脚本兼容性最好将启动脚本放到/etc/init.d/再创建软链接sudocpmydaemon_init.sh /etc/init.d/mydaemonsudochmodx /etc/init.d/mydaemonsudoupdate-rc.d mydaemon defaults9901参数含义defaults使用默认的启动/停止顺序99启动优先级数字越大越晚启动01停止优先级数字越小越早停止优点兼容所有老式嵌入式 Linux 系统缺点配置较繁琐方式三systemd 服务文件现代标配创建/etc/systemd/system/mydaemon.service[Unit] Description嵌入式传感器服务 Afternetwork.target [Service] Typeforking PIDFile/var/run/mydaemon.pid ExecStart/usr/local/bin/mydaemon ExecStop/bin/kill -TERM $MAINPID Restartalways RestartSec5 StandardOutputfile:/var/log/mydaemon.log StandardErrorfile:/var/log/mydaemon_error.log [Install] WantedBymulti-user.target启用并启动sudosystemctl daemon-reloadsudosystemctlenablemydaemon# 开机自启sudosystemctl start mydaemon# 立即启动sudosystemctl status mydaemon# 查看状态sudosystemctl stop mydaemon# 停止systemd 关键字段解析字段含义Typeforking程序自己 fork 成守护进程后才返回PIDFilesystemd 通过这个文件跟踪进程Restartalways不管什么原因退出都自动重启RestartSec5重启前等待 5 秒防止 crash loopWantedBymulti-user.target在多用户模式正常开机时启动三种方式选型指南场景推荐方式开发调试-f前台模式老系统BusyBox init/etc/rc.localDebian/Ubuntu 旧版SysV init 脚本嵌入式 Linuxsystemdsystemd service需要崩溃自动重启systemdRestartalways六、常见问题排查现象原因排查方法启动后立即退出fork失败或setsid失败前台模式-f调试日志文件为空未刷新缓冲区每次写日志后fflush或fsyncsystemd 显示 activating程序没有返回没有 fork 完检查Typeforking是否匹配重启循环crash loop程序启动后就崩溃RestartSec调大查看错误日志两个实例同时运行PID 文件残留启动前检查 PID 文件并确认进程是否存在七、守护进程调试技巧调试守护进程最大的痛点是看不到 stdout。解决方法// 开发阶段不 daemonize直接用 printfif(getenv(FOREGROUND)NULL){daemonize();}// 运行时$ FOREGROUND1./mydaemon// 前台调试$./mydaemon// 后台运行或者用 syslog#includesyslog.hopenlog(mydaemon,LOG_PID,LOG_DAEMON);syslog(LOG_INFO,传感器服务启动);syslog(LOG_ERR,传感器初始化失败: %s,strerror(errno));然后用tail -f /var/log/syslog或journalctl -fu mydaemon查看。八、总结守护进程制作的核心步骤一个都不能少fork exit 父进程— 让 shell 以为程序结束setsid— 成为新会话首领脱离终端二次 fork— 彻底杜绝意外获得控制终端**chdir(“/”) ** — 释放工作目录关闭 fd 重定向到 /dev/null— 防止后台输出干扰PID 文件— 管理脚本的身份证signal 处理器— 优雅停止不丢数据开机自启选择老系统用rc.local新系统用systemd service需要崩溃重启能力果断上 systemd。把前面九篇文章的任何一段代码——不管是文件 IO、串口采集还是 epoll 服务器——套进daemonize()函数它就成了一名合格的嵌入式后台打工人。