适用场景
本文适用于 Linux 服务器上使用 systemd 管理自研服务、脚本任务、Java 服务、Python 服务或二进制程序时,执行 systemctl start 后服务立即失败,并在日志中看到 status=203/EXEC 的场景。
这类问题通常不是业务代码运行时报错,而是 systemd 在执行 ExecStart 阶段就没有成功拉起进程。换句话说,程序还没真正进入业务逻辑,启动命令本身就无法被内核执行。
现象描述
服务启动失败后,先看状态:
systemctl status demo.service
常见输出类似:
demo.service: Main process exited, code=exited, status=203/EXEC
demo.service: Failed with result 'exit-code'.
Failed to start demo service.
再查看最近日志:
journalctl -u demo.service -n 80 --no-pager
可能会看到:
Failed at step EXEC spawning /data/app/start.sh: Permission denied
或者:
Failed at step EXEC spawning /data/app/start.sh: No such file or directory
203/EXEC 的重点在 EXEC,表示 systemd 执行启动命令失败。它不同于程序启动后返回非零退出码,也不同于端口冲突、配置解析失败这类业务进程内部错误。
常见原因
排查时优先检查下面几类问题:
ExecStart指向的文件不存在,或路径写错。- 脚本没有可执行权限。
- 脚本第一行解释器路径错误,例如
#!/bin/bash不存在。 - 脚本是 Windows 换行,导致解释器路径变成带
\r的异常路径。 ExecStart使用了相对路径,systemd 找不到命令。- 运行用户没有访问目录或文件的权限。
- 二进制文件架构不匹配,例如把 ARM 程序放到 x86 机器上运行。
- unit 文件写法错误,把 shell 语法直接写进
ExecStart。
排查思路
1. 确认 systemd 实际加载的 unit 内容
不要只看自己编辑的文件,先确认 systemd 当前加载的最终配置:
systemctl cat demo.service
重点看:
ExecStart是否是绝对路径;User和Group是否符合预期;- 是否有多个 drop-in 覆盖配置;
WorkingDirectory是否存在;EnvironmentFile是否存在且可读。
如果刚修改过 unit 文件,一定要执行:
systemctl daemon-reload
否则 systemd 可能仍然使用旧配置。
2. 检查启动文件是否存在
ls -l /data/app/start.sh
如果提示文件不存在,先检查路径是否写错,尤其是部署目录中常见的版本软链接:
ls -l /data/app
readlink -f /data/app/current
生产环境里经常出现发布脚本更新了软链接,但 unit 文件仍指向旧目录的情况。
3. 检查执行权限
ls -l /data/app/start.sh
如果权限类似:
-rw-r--r-- 1 app app 820 Jul 8 10:00 /data/app/start.sh
说明没有执行位。修复:
chmod 755 /data/app/start.sh
更稳妥的做法是在构建或发布流程中固定权限,避免每次手工处理:
install -m 755 start.sh /data/app/start.sh
4. 检查脚本解释器
查看脚本第一行:
head -n 1 /data/app/start.sh
常见正确写法:
#!/usr/bin/env bash
或者:
#!/bin/bash
然后确认解释器存在:
command -v bash
ls -l /bin/bash /usr/bin/env
如果脚本第一行写的是不存在的路径,例如 #!/usr/local/bin/bash,systemd 就会在 EXEC 阶段失败。
5. 检查 Windows 换行
如果日志里出现类似 bad interpreter,或者明明 /bin/bash 存在却仍然提示找不到文件,要检查换行符:
file /data/app/start.sh
如果输出中包含 CRLF,说明文件使用了 Windows 换行。修复:
sed -i 's/\r$//' /data/app/start.sh
也可以用:
dos2unix /data/app/start.sh
修复后重新启动:
systemctl restart demo.service
6. 用目标用户手工执行
如果 unit 中指定了 User=app,不要只用 root 测试。应该切换到同一个用户执行:
sudo -u app /data/app/start.sh
如果 unit 中设置了工作目录,也要模拟:
cd /data/app
sudo -u app ./start.sh
这一步可以快速发现目录权限、环境变量、相对路径等问题。
unit 文件写法建议
一个较稳妥的示例:
[Unit]
Description=Demo application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/data/app
ExecStart=/data/app/start.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
关键点说明:
ExecStart使用绝对路径,减少环境差异;WorkingDirectory明确指定工作目录,避免脚本里相对路径漂移;User和Group使用普通业务用户,避免服务长期以 root 运行;Restart=on-failure只处理运行期异常,不会掩盖203/EXEC的根因;- 日志输出到 journal,便于用
journalctl排查。
不建议的写法
不要直接在 ExecStart 中写复杂 shell 语法:
ExecStart=cd /data/app && ./start.sh
systemd 默认不会像交互式 shell 那样解释这类命令。需要 shell 能力时,应显式调用 shell:
ExecStart=/bin/bash -lc 'cd /data/app && exec ./start.sh'
不过更推荐把复杂逻辑放进启动脚本,让 unit 文件保持简单。
也不要依赖 PATH 查找命令:
ExecStart=python app.py
改成明确路径:
ExecStart=/data/app/.venv/bin/python /data/app/app.py
这样线上机器的环境变量变化不会影响服务启动。
修复后的验证
修改 unit 文件或脚本后,按下面顺序验证:
systemctl daemon-reload
systemctl restart demo.service
systemctl status demo.service --no-pager
journalctl -u demo.service -n 80 --no-pager
如果服务正常启动,状态中应看到:
Active: active (running)
再确认进程和端口:
ps -ef | grep '[d]emo'
ss -lntp | grep 8080
如果是 HTTP 服务,可以在本机做健康检查:
curl -fsS http://127.0.0.1:8080/health
注意:线上排查时不要只看 systemctl restart 是否返回成功,还要看服务是否持续运行,避免进程启动后马上退出。
预防措施
为了减少 203/EXEC 在线上出现,建议把下面几项固化到发布流程:
- unit 文件统一模板化,
ExecStart必须使用绝对路径; - 发布脚本使用
install -m 755部署启动脚本; - CI 中检查脚本换行符,禁止 CRLF 进入 Linux 发布包;
- 发布后自动执行
systemctl daemon-reload; - 部署前用
systemd-analyze verify demo.service做静态校验; - 服务启动后检查
systemctl is-active demo.service和健康检查接口; - 业务用户、部署目录、日志目录的权限由自动化脚本创建和校验。
可以在发布脚本中增加基础检查:
test -x /data/app/start.sh
test -d /data/app
systemd-analyze verify /etc/systemd/system/demo.service
这些检查不能覆盖所有业务错误,但能提前发现大部分 EXEC 阶段问题。
总结
status=203/EXEC 的核心含义是 systemd 没有成功执行 ExecStart 指定的命令。排查时不要先看业务配置,而要先确认路径、权限、解释器、换行符、运行用户和 unit 文件写法。
一个实用顺序是:先 systemctl cat 看真实配置,再 journalctl 看 EXEC 阶段错误,然后用目标用户手工执行启动命令。只要把“systemd 实际执行了什么”和“这个命令在目标用户下能不能执行”对齐,绝大多数 203/EXEC 问题都能快速定位。
Discussion
评论