📌 背景描述
在公司实际业务嵌入式系统中,Go
编写的主程序 probe-agent
负责运行核心业务,同时具备自升级(OTA)功能。主程序需要在运行中,在升级过程中,会进行如下操作:
- 启动
OTA
子程序ota-executor
ota-executor
进行旧程序备份、停止主程序、解压新程序包并完成升级、启动新程序等操作- 启动主程序后,通过curl 检测主程序的健康情况,如果主程序健康,则
OTA
成功,异步通知平台升级成功 - 升级过程中,如果出现异常,就会进行回滚操作
但在我的测试过程中,遇到如下问题:
ota-executor
无法正常启动ota-executor
启动成功后,只要执行systemctl stop probe-agent
停止主程序,ota-executor
就会退出,导致OTA
中断- 即使使用 Go 的
syscall.SysProcAttr{Setsid: true}
尝试分离子进程,仍然无效
下面列出经过排查分析后的具体原因以及相对应的解决方案
⚠️ 问题一:OverlayFS 下无法执行二进制
现象
启动 ota-executor
报错是这样的:
plain
fork/exec /home/probe-agent/ota-executor: operation not permitted
分析
1、首先第一直觉是二进制没有执行权限,于是查看了二进制的权限:
bash
ls -l /home/probe-agent/ota-executor
-rwxr-xr-x 1 root root 10904 5月 7 09:09 /home/probe-agent/ota-executor
显示二进制有执行权限的,不是这个问题
2、再次查看二进制文件和服务器系统是否对应:
bash
# 这里显示系统版本为 Ubuntu 18.04 并且是 x86_64
uname -a
Linux 192.168.1.1 4.15.0-45-generic #48-Ubuntu SMP Tue Jan 29 16:28:13 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
cat /proc/version
# 再查看二进制文件的打包是否是 x86_64
file /home/probe-agent/ota-executor
/home/probe-agent/ota-executor: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c0f0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0, with
确定了运行环境和二进制文件关系是一致的,也不是这个问题
3、经过一番搜索,难道这还跟文件系统有关?
系统根目录为 OverlayFS:
bash
# 查看文件系统
findmnt
# 输出结果
overlayroot on / type overlay (rw,relatime,...)
这里的结果显示:
- 根目录 / 是通过 overlay 文件系统挂载的;
- overlayroot 是挂载源(通常是 overlayfs 的 upperdir/merged);
- overlay 是文件系统类型;
- rw,relatime 表示是可读写的。
OverlayFS
常用于系统只读保护,但它存在以下限制:
- 某些 overlay 的
upperdir
不支持exec
权限 - AppArmor、mount options、SELinux 等也可能限制执行
lsattr
查看权限时可能无效
也就是说,在我所在的这个服务器上不能在 /home
下执行可执行文件。
✅ 解决方案
OverlayFS
允许将文件系统挂载为只读,但执行文件必须位于可执行目录中。所以我把执行文件从 /home/...
移动到 /opt
或其他普通挂载路径下:
bash
mv /home/probe-agent/ota-executor /opt/probe-agent/ota-executor
chmod +x /opt/probe-agent/ota-executor
这样就可以正常执行了。
⚠️ 问题二:systemd cgroup 杀死子进程
现象
ota-executor
在运行中执行 systemctl stop probe-agent
,自己也会立即被杀掉。
分析
对于不熟悉 Linux 的程序员,在这样的情况下根本不知道应该从哪方面开始分析问题,所以只能交给度娘或者Google。
于是经过又经过一番搜索,最终找到如下答案:
主程序是通过 systemd
的 probe-agent.service
启动的:
- 所有由此服务启动的进程(包括子进程)默认属于同一个 cgroup
- systemd 管理下,当停止服务时,会清理其 cgroup 下的所有进程
- 即使在子进程中调用
syscall.Setsid
创建独立会话,也不能逃离 cgroup 限制
查看进程是否独立:
bash
cat /proc/57843/cgroup # 57843 是OTA程序的进程ID
结果显示其仍归属于 system.slice/probe-agent.service
错误尝试
刚开始,我在在主程序中是这样启动OTA
子进程的:
go
cmd := exec.Command("/opt/probe-agent/ota-executor", "-request", "/path/to/request")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
cmd.Process.Release()
结果表明,这样启动确实可以启动子进程,但是子进程并未完全脱离主进程的 cgroup,当主程序被 systemctl stop
停止时,子进程也会被一起停止。
✅ 解决方案:使用 systemd-run
创建独立 cgroup
正确的方式是使用 systemd-run
启动 OTA 子程序,使其运行在独立 scope 中,不被主服务控制:
go
cmd := exec.Command("systemd-run", "--scope", "/opt/probe-agent/ota-executor", "-request", requestFile)
也可选用:
bash
systemd-run --unit=ota-executor /opt/probe-agent/ota-executor -request /path/to/request
这样做的好处:
- 该进程运行在
ota-executor.scope
,不再隶属于probe-agent.service
- 即使主程序被
systemctl stop
,子进程也能存活
✅ 总结与建议
问题 | 原因 | 解决方案 |
---|---|---|
无法执行 OTA 程序 | OverlayFS 限制了执行权限 | 将文件移动到 /opt 等常规挂载目录 |
OTA 程序被误杀 | systemctl stop 杀死所有在同一 cgroup 中的子进程 |
使用 systemd-run --scope 启动子进程,分离 cgroup |
最后
- 如果程序需要启动另外一个子程序并让其能够独立运行,不受主程序影响,应使用
systemd-run
启动 - 在部署过程中,不要将可执行文件部署在
Overlay
或只读文件系统下 - 通过
/proc/<pid>/cgroup
检查进程的 cgroup,看看是否是独立的进程 - 使用
journalctl -u xxx.scope
可以单独查看 OTA 子进程日志
以上问题的发现和解决,很大程度上考验程序员是否熟悉 Linux
操作系统,足以体现了熟悉 Linux
系统的重要性,所以作为后端开发,熟悉 Linux
操作系统是非常有必要的,在关键时刻才能及早发现并解决问题。