适用场景

本文适用于 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 执行启动命令失败。它不同于程序启动后返回非零退出码,也不同于端口冲突、配置解析失败这类业务进程内部错误。

常见原因

排查时优先检查下面几类问题:

  1. ExecStart 指向的文件不存在,或路径写错。
  2. 脚本没有可执行权限。
  3. 脚本第一行解释器路径错误,例如 #!/bin/bash 不存在。
  4. 脚本是 Windows 换行,导致解释器路径变成带 \r 的异常路径。
  5. ExecStart 使用了相对路径,systemd 找不到命令。
  6. 运行用户没有访问目录或文件的权限。
  7. 二进制文件架构不匹配,例如把 ARM 程序放到 x86 机器上运行。
  8. unit 文件写法错误,把 shell 语法直接写进 ExecStart

排查思路

1. 确认 systemd 实际加载的 unit 内容

不要只看自己编辑的文件,先确认 systemd 当前加载的最终配置:

systemctl cat demo.service

重点看:

  • ExecStart 是否是绝对路径;
  • UserGroup 是否符合预期;
  • 是否有多个 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 明确指定工作目录,避免脚本里相对路径漂移;
  • UserGroup 使用普通业务用户,避免服务长期以 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 问题都能快速定位。