测试开机启动脚本使用避坑指南,新手必看
你是不是也遇到过这样的情况:写好了启动脚本,加进系统,重启后却发现——什么都没发生?
脚本没执行、日志没输出、服务没起来,甚至系统启动都变慢了……
别急,这不是你代码写错了,大概率是踩进了开机启动脚本的几个经典“坑”里。
这篇指南不讲抽象原理,不堆参数配置,只说你在实际操作中一定会碰到、但文档里很少明说的细节问题。
从文件权限到执行时机,从路径陷阱到日志盲区,我们一条条拆解,手把手带你绕开所有新手高频翻车点。
全文基于常见嵌入式Linux环境(如OpenWrt风格系统)编写,所有操作均经过实测验证,代码可直接复制粘贴,每一步都标注了“为什么必须这么做”。
1. 为什么/etc/rc.local看似简单,却最容易失效?
很多新手第一反应就是改rc.local——毕竟它看起来最像“开机自动运行”的地方。但恰恰是这个“最简单”的方式,隐藏着最多无声失败的可能。
1.1exit 0的位置,比你想象中更重要
你可能已经照着教程,在exit 0前加了命令,比如:
#!/bin/sh # /etc/rc.local echo "Starting my service..." > /tmp/start.log sleep 2 /usr/bin/myapp --daemon exit 0问题来了:如果myapp启动失败、卡住、或需要较长时间初始化,整个rc.local的执行就会被阻塞,后续系统服务(比如网络、SSH)可能无法正常就绪,甚至导致系统假死或反复重启。
正确做法:所有耗时操作必须后台化、非阻塞化:
#!/bin/sh # /etc/rc.local echo "Starting my service..." > /tmp/start.log # 使用 & + nohup + 重定向,确保脱离终端、不占前台 nohup /usr/bin/myapp --daemon > /tmp/myapp.log 2>&1 & exit 0关键点说明:
&让进程在后台运行;nohup防止父进程(shell)退出时将其终止;> /tmp/myapp.log 2>&1把标准输出和错误统一重定向到日志,避免因输出无处可去而卡住;- 缺一不可,少一个都可能让脚本“静默失败”。
1.2 权限不是加了+x就万事大吉
你执行了chmod +x /etc/rc.local,但系统仍提示Permission denied?
这是因为:某些固件(尤其是精简版OpenWrt)默认将/etc/rc.local设为只读挂载,或使用了 overlayfs 机制,直接 chmod 不生效。
验证与修复方法:
# 查看文件实际权限和挂载属性 ls -l /etc/rc.local mount | grep /etc # 如果显示为 ro(read-only),需先 remount 为可写 mount -o remount,rw /overlay # 再次修改并确认 chmod +x /etc/rc.local ls -l /etc/rc.local # 应看到 -rwxr-xr-x注意:
/overlay是 OpenWrt 常见的可写层挂载点,不同系统可能为/mnt或/data,请根据mount输出确认。
1.3 路径陷阱:/usr/bin/myapp可能根本不存在
rc.local在系统早期阶段执行,此时/usr分区可能尚未挂载(尤其在使用外部存储或模块化固件时)。你写的绝对路径,很可能指向一个还“不存在”的目录。
安全写法:用完整路径 + 存在性检查
#!/bin/sh # /etc/rc.local # 检查程序是否存在,再执行 if [ -x "/usr/bin/myapp" ]; then echo "myapp found, starting..." > /tmp/start.log nohup /usr/bin/myapp --daemon > /tmp/myapp.log 2>&1 & else echo "myapp NOT found at /usr/bin/myapp" >> /tmp/start.log fi exit 0提示:也可将脚本和二进制文件一起放在
/tmp(内存盘,始终可用)或/overlay(持久化可写区),规避挂载依赖。
2./etc/init.d/方式更规范,但新手常忽略三个致命细节
把脚本放进/etc/init.d/并enable,听起来很专业。但如果你只是照抄模板、没理解背后机制,90% 的失败都出在这三步。
2.1START=数值不是越大越好,也不是越小越早
很多教程写START=99,意思是“最后启动”。但如果你的服务依赖网络(比如要连WiFi、访问HTTP API),而网络服务的START=50,那99确实合适;
可如果你的服务是udhcpc客户端,需要在网卡初始化前就运行,START=10才对。
查清依赖关系的方法:
# 查看已启用服务的启动顺序 ls -la /etc/rc.d/ # 输出类似: # S50network -> ../init.d/network # S99myapp -> ../init.d/myscript观察
Sxx开头的软链接名,数字即START=值。你的服务应比它所依赖的服务数字更大(启动更晚),比它所被依赖的服务数字更小(启动更早)。
2.2start()函数里不能直接用cd切换工作目录
你以为加一句cd /root/myscript就能进目录执行?错。init.d脚本由procd或sysvinit以最小化环境调用,cd后的路径变更不会延续到后续命令,尤其当命令通过sh -c或子 shell 调用时。
可靠写法:用cd+&&连写,或直接用绝对路径
start() { # 推荐:用 && 保证在指定目录下执行 cd /root/myscript && ./run.sh > /tmp/run.log 2>&1 & # 或更稳妥:全部用绝对路径 /root/myscript/run.sh > /tmp/run.log 2>&1 & }2.3enable不等于“已注册”,还要检查/etc/rc.d/下是否有对应软链接
执行/etc/init.d/myscript enable后,你以为完事了?
其实它只是在/etc/rc.d/下创建一个Sxxmyscript软链接。如果该目录权限不对、磁盘满、或脚本名含非法字符(如-、空格),链接创建会静默失败。
手动验证是否真正启用:
# 查看是否生成了 Sxx 开头的软链接 ls -la /etc/rc.d/S*myscript # 正常应输出类似: # S99myscript -> ../init.d/myscript # 如果没有,手动创建(假设 START=99) ln -sf ../init.d/myscript /etc/rc.d/S99myscript补充:
disable是删除该软链接,不是修改脚本内容。
3. 日志去哪儿了?没有日志,等于没有调试
开机脚本最大的痛苦不是失败,而是失败了却不知道哪里失败。因为rc.local和init.d脚本默认不输出到dmesg或logread,它们的日志往往被丢弃。
3.1 给每个关键步骤加日志,且写入持久化位置
不要只依赖echo "done",要记录时间、状态、返回码:
start() { local ts=$(date '+%Y-%m-%d %H:%M:%S') echo "[$ts] myscript start called" >> /tmp/myscript.log if /root/myscript/run.sh; then echo "[$ts] run.sh executed successfully" >> /tmp/myscript.log else echo "[$ts] run.sh failed with exit code $?" >> /tmp/myscript.log fi }为什么选/tmp/myscript.log?
/tmp是内存文件系统,读写快、无磨损;- 重启后自动清空,避免日志无限增长;
- 所有系统都保证
/tmp可用,不受挂载顺序影响。
3.2 用logread实时跟踪启动过程(OpenWrt 专属技巧)
OpenWrt 的logread默认只显示syslog级别日志,而rc.local输出不会进入 syslog。
但你可以主动推送:
# 在 rc.local 或 init.d 脚本中 logger -t "myscript" "Starting background service..." logger -t "myscript" "PID: $(pgrep myapp)"然后在串口或 SSH 中实时查看:
logread -f -e "myscript"输出示例:
Sat Jan 1 00:02:34 2024 user.info myscript: Starting background service...
这样,你就能像看直播一样,看到脚本每一秒发生了什么。
4. 真实避坑清单:5个被问爆的问题,答案都在这里
以下是你在论坛、群聊里最常看到的提问,我们直接给出根因和解法。
4.1 “脚本在命令行能跑,开机就不执行” —— 根因:环境变量缺失
命令行有PATH=/usr/bin:/bin,但rc.local没有。python、node、jq等命令找不到。
解法:显式声明PATH,或用绝对路径
PATH="/usr/bin:/bin:/usr/sbin:/sbin" export PATH python3 /root/script.py # 或直接写 /usr/bin/python3 /root/script.py4.2 “脚本执行了,但生成的文件不在预期位置” —— 根因:当前工作目录不确定
rc.local启动时工作目录是/,init.d脚本是/或/etc/init.d,不是你期望的/root。
解法:所有文件操作前先cd,或全程用绝对路径
cd /root/myscript && ./do.sh > output.txt # 或 /root/myscript/do.sh > /root/myscript/output.txt4.3 “脚本启了两次” —— 根因:init.d脚本被procd多次触发
OpenWrt 的procd有时会因配置加载顺序问题,重复调用start()。
解法:加进程锁(pidfile)
start() { local pidfile="/var/run/myscript.pid" if [ -f "$pidfile" ] && kill -0 $(cat "$pidfile") > /dev/null 2>&1; then echo "myscript already running" return fi /root/myscript/run.sh & echo $! > "$pidfile" }4.4 “重启后脚本失效” —— 根因:固件升级覆盖了/etc/下的修改
OpenWrt 升级时,默认只保留/etc/config/,其他/etc/文件(如rc.local、init.d脚本)会被还原。
解法:将自定义脚本存于/overlay,并在rc.local中软链接或复制
# 升级后自动恢复 if [ ! -f /etc/rc.local ]; then cp /overlay/etc/rc.local /etc/rc.local chmod +x /etc/rc.local fi4.5 “想让脚本等网络就绪再运行” —— 根因:没做网络就绪检测
直接ping外网可能失败(DNS未通),应检测本地网络接口是否UP。
解法:等待br-lan或eth0获取IP后再执行
# 等待 br-lan 获取IP(OpenWrt 默认LAN桥接) while ! ifconfig br-lan | grep -q "inet "; do sleep 1 done echo "Network ready, starting service..." /root/myscript/run.sh &5. 总结:一份可立即执行的自查清单
别再靠猜和试错。每次部署开机脚本前,花2分钟按这份清单快速核对:
- [ ]
rc.local是否有+x权限?是否在exit 0前添加命令?是否所有长时任务都加了& nohup >log 2>&1? - [ ]
init.d脚本是否以#!/bin/sh /etc/rc.common开头?START=值是否合理?/etc/rc.d/Sxx*软链接是否存在? - [ ] 所有路径是否为绝对路径?是否检查了文件/目录存在性?是否规避了
/usr等可能未挂载的分区? - [ ] 是否为每个关键步骤添加了带时间戳的日志?日志是否写入
/tmp/或通过logger推送至logread? - [ ] 是否测试了重启场景?是否模拟了固件升级后恢复流程?
记住:开机脚本不是写一次就完事,而是要经得起重启、升级、断电的三重考验。
真正的稳定性,藏在那些没人提醒你、但系统每天都在默默执行的细节里。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。