开机启动脚本踩坑记录:这些错误千万别再犯
你有没有遇到过这样的情况:辛辛苦苦写好一个服务脚本,加进开机启动,重启后却发现——它根本没跑?日志查不到,进程找不到,系统安静得像什么都没发生过。更糟的是,有时候它甚至导致系统卡在启动界面,连终端都进不去。
这不是玄学,是 Linux 系统启动机制和脚本编写规范之间那些“看不见的约定”在作祟。本文不是教你怎么写第一个脚本,而是聚焦于真实生产环境中反复踩过的坑——全部来自实际部署“测试开机启动脚本”镜像时的血泪经验。每一个错误,我们都复现过、定位过、修复过,并给出可验证的解决方案。
你不需要记住所有技术细节,但请务必避开这六个最致命的错误。它们足以让一个本该自动运行的服务,永远停留在“理论上能跑”的阶段。
1. 脚本权限缺失:不是写了就能执行,而是要“被允许执行”
很多人以为把脚本丢进/etc/init.d/目录就万事大吉,却忽略了 Linux 最基础的安全机制:执行权限。
1.1 错误现场还原
假设你创建了/etc/init.d/myapp.sh,内容完整,注释齐全,update-rc.d也成功执行。但重启后ps aux | grep myapp一片空白。
检查发现:
ls -l /etc/init.d/myapp.sh # 输出:-rw-r--r-- 1 root root 1234 May 20 10:00 /etc/init.d/myapp.sh权限是644,没有x(执行)位。init系统在扫描/etc/init.d/时,会直接跳过所有不可执行的文件。
1.2 正确做法:三步缺一不可
必须显式赋予执行权限,且推荐使用755(所有者可读写执行,组和其他人可读可执行):
sudo chmod 755 /etc/init.d/myapp.sh # 验证 ls -l /etc/init.d/myapp.sh # 应输出:-rwxr-xr-x 1 root root 1234 May 20 10:00 /etc/init.d/myapp.sh关键提醒:
chmod必须在update-rc.d之前执行。如果顺序颠倒,update-rc.d可能静默失败或注册一个无效链接。
2. INIT INFO 注释块不完整:少一行,全盘皆输
Ubuntu 的init.d启动机制严重依赖脚本开头的### BEGIN INIT INFO块。它不是可选注释,而是update-rc.d解析脚本元信息的唯一依据。
2.1 致命缺失项:Default-Start和Default-Stop
参考博文中的示例虽然包含了这两行,但很多开发者会下意识删掉,认为“反正默认就行”。错!update-rc.d在解析时,如果发现Default-Start缺失,会直接拒绝注册,且不报任何错误。
错误写法(缺少 Default-Start):
### BEGIN INIT INFO # Provides: myapp # Required-Start: $local_fs $network # Short-Description: My Application ### END INIT INFO执行sudo update-rc.d myapp defaults后,看似成功,实则/etc/rc?.d/下没有任何Sxxmyapp或Kxxmyapp链接。
2.2 完整且安全的 INIT INFO 模板
请直接复制使用,已通过 Ubuntu 20.04/22.04 实测:
#!/bin/sh ### BEGIN INIT INFO # Provides: myapp # Required-Start: $local_fs $remote_fs $network $syslog # Required-Stop: $local_fs $remote_fs $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start and stop myapp # Description: Myapp is a background service for data processing. ### END INIT INFORequired-Start中$network是关键:确保你的脚本在网络就绪后才启动。Default-Start: 2 3 4 5覆盖了所有标准多用户运行级别。Description字段必须有,且不能为空,否则update-rc.d可能解析失败。
3. 路径硬编码陷阱:cd不等于“我在项目目录”
脚本中常见的写法是:
cd /home/ubuntu/myproject ./start.sh这在手动执行时完全没问题。但当它作为init.d服务启动时,当前工作目录是/(根目录),而非你的用户家目录。
3.1 错误后果
cd /home/ubuntu/myproject失败(因为/home/ubuntu可能尚未挂载,或权限不足)- 后续所有相对路径操作(如
./start.sh,config.ini)全部失效 - 进程可能静默退出,
/var/log/syslog中只有一行myapp: cd: /home/ubuntu/myproject: No such file or directory
3.2 绝对路径 + 存在性校验才是王道
# 定义绝对路径(用变量提高可读性) APP_HOME="/home/ubuntu/myproject" START_SCRIPT="$APP_HOME/start.sh" # 关键:校验路径是否存在且可访问 if [ ! -d "$APP_HOME" ]; then echo "ERROR: APP_HOME directory does not exist: $APP_HOME" >&2 exit 1 fi if [ ! -x "$START_SCRIPT" ]; then echo "ERROR: Start script is not executable: $START_SCRIPT" >&2 exit 1 fi # 确保以正确用户身份运行(避免root权限过大风险) su -c "$START_SCRIPT" -s /bin/sh ubuntu为什么用
su -c而非sudo?sudo在init.d环境中常因缺少 TTY 或配置缺失而失败;su更底层、更可靠,且-s /bin/sh明确指定了 shell,避免环境变量污染。
4. 启动超时与后台化:别让 init 等你等到天荒地老
init.d脚本默认是同步阻塞执行的。如果你的start.sh是一个长期运行的守护进程(比如 Python Flask 服务),它必须主动转入后台,否则init会一直等待,最终触发超时,强制杀死进程并标记服务启动失败。
4.1 典型错误:忘记&和nohup
错误写法:
# 这会让 init 一直卡在这里,直到进程退出或超时 /home/ubuntu/myproject/start.sh4.2 正确的后台化三要素
# 1. 使用 nohup 忽略挂起信号,防止终端关闭影响 # 2. 使用 & 将进程放入后台 # 3. 重定向 stdout/stderr 到日志文件,避免输出丢失 nohup /home/ubuntu/myproject/start.sh > /var/log/myapp.log 2>&1 & # 记录 PID,便于后续 stop 操作 echo $! > /var/run/myapp.pid同时,在脚本的stop函数中,必须读取并 kill 这个 PID:
stop() { if [ -f /var/run/myapp.pid ]; then kill $(cat /var/run/myapp.pid) rm -f /var/run/myapp.pid fi }5. rc.local 的“伪可靠”:它比你想的更脆弱
参考博文提到rc.local方法“尝试了两次没有成功”,这不是偶然。rc.local在现代 Ubuntu(18.04+)中已被 systemd 弱化,其执行时机和可靠性远不如init.d。
5.1 两个隐藏雷区
雷区一:exit 0缺失或位置错误rc.local脚本末尾必须有exit 0,且必须是最后一行。如果exit 0被注释掉,或后面还有空行/命令,整个rc.local会被 systemd 视为失败,从而跳过执行。
雷区二:/etc/rc.local文件本身未启用
Ubuntu 20.04+ 默认不启用rc.local服务。必须手动启用:
sudo systemctl enable rc-local # 并确保 /etc/rc.local 存在且可执行 sudo chmod +x /etc/rc.local5.2 如果坚持用 rc.local,请这样写
#!/bin/sh -e # # rc.local # # 确保在所有网络服务之后执行 sleep 5 # 使用绝对路径,并添加错误处理 if [ -x /home/ubuntu/myproject/start.sh ]; then su -c "/home/ubuntu/myproject/start.sh > /var/log/myapp_rclocal.log 2>&1 &" -s /bin/sh ubuntu fi exit 0强烈建议:除非有特殊兼容性要求,否则优先选择
init.d方案。rc.local应作为临时调试手段,而非生产方案。
6. 日志缺失 = 黑盒调试:没有日志,等于没有真相
所有上述错误,最终都会表现为“服务没起来”。但如果你没有日志,就只能靠猜。/var/log/syslog是第一线索,但它只记录init层面的信息(如Starting myapp...),不记录你的脚本内部发生了什么。
6.1 必须建立的三层日志体系
| 层级 | 位置 | 记录内容 | 查看命令 |
|---|---|---|---|
| 系统层 | /var/log/syslog | init启动状态、update-rc.d操作 | sudo grep myapp /var/log/syslog |
| 服务层 | /var/log/myapp.log | 脚本自身 stdout/stderr | sudo tail -f /var/log/myapp.log |
| 进程层 | /var/run/myapp.pid | 当前运行进程 ID | cat /var/run/myapp.pid |
6.2 一个健壮的日志初始化函数
在你的init.d脚本开头加入:
# 日志配置 LOG_FILE="/var/log/myapp.log" PID_FILE="/var/run/myapp.pid" mkdir -p /var/log /var/run # 写入启动时间戳 echo "=== $(date) ===" >> "$LOG_FILE"并在每个关键步骤后追加日志:
echo "[$(date)] Starting application..." >> "$LOG_FILE" su -c "$START_SCRIPT > $LOG_FILE 2>&1 &" -s /bin/sh ubuntu echo "[$(date)] Application started with PID \$!" >> "$LOG_FILE"总结:一份可立即执行的自查清单
当你完成一个开机启动脚本的编写,不要急着reboot。请按此清单逐项核对,90% 的问题都能在重启前发现:
7. 自查清单:六步确认法
7.1 权限与位置
- [ ] 脚本文件位于
/etc/init.d/目录下 - [ ] 脚本权限为
755(sudo chmod 755 /etc/init.d/myapp.sh)
7.2 INIT INFO 完整性
- [ ] 包含完整的
### BEGIN INIT INFO到### END INIT INFO块 - [ ]
Default-Start和Default-Stop行存在且值正确 - [ ]
Provides:字段名称与文件名一致(myapp.sh→Provides: myapp)
7.3 路径与用户
- [ ] 所有路径均为绝对路径,无
~或$HOME - [ ] 使用
su -c ... -s /bin/sh <username>以普通用户身份运行,而非sudo
7.4 后台化与 PID
- [ ] 启动命令包含
nohup ... &,并重定向日志 - [ ] 成功写入
PID_FILE,且stop函数能正确读取并 kill
7.5 日志与反馈
- [ ]
/var/log/myapp.log文件存在且可写 - [ ]
rc.local方案已确认systemctl enable rc-local并chmod +x
7.6 验证流程
- [ ] 先手动执行
sudo /etc/init.d/myapp start,确认无报错且进程存在 - [ ] 执行
sudo /etc/init.d/myapp status(需自行实现),确认状态正常 - [ ] 最后执行
sudo reboot,观察启动过程
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。