Linux下C++服务自更新阻塞死锁问题的解决求助
我有一个多组件构成的复杂应用,组件间互相依赖保障运行,需要以超级用户权限作为daemon在Ubuntu 20.04环境运行。目前正在实现自更新功能:程序从网络下载.deb包后执行更新,更新代码如下:
template <std::size_t N> int execvp(const char* file, const char* const (&argv)[N]) { assert((N > 0) && (argv[N - 1] == nullptr)); return execvp(file, const_cast<char* const*>(argv)); } err_t SoftUpdater::_installServiceUpdate(const std::string &file_path) { const char* const cmd[] = { "dpkg", "-i", file_path.c_str() , NULL}; int pid = fork(); if (pid == 0) { execvp(cmd[0], cmd); DBG(E, ":(, %i, %s", errno, strerror(errno)); exit(1); } else if (pid > 0) { } else { DBG(E, ":((("); } return E_OK; }
在控制台手动执行该.deb包更新正常,但在服务内执行时出现阻塞,执行systemctl status myservice得到如下输出:
myservice.service - MyService daemon Loaded: loaded (/lib/systemd/system/myservice.service; enabled; vendor preset: enabled) Active: deactivating (final-sigterm) since Tue 2022-10-25 12:31:53 MSK; 16s ago Process: 38160 ExecStart=/opt/myservice/sbin/myservicedsp (code=exited, status=0/SUCCESS) Main PID: 38160 (code=exited, status=0/SUCCESS) Tasks: 3 (limit: 9448) Memory: 54.7M CGroup: /system.slice/myservice.service ├─38182 dpkg -i /tmp/07080415-acf7-4404-a086-edd3ca743a22.deb ├─38191 /bin/sh /var/lib/dpkg/info/myservice.prerm upgrade 1.1.5-ubuntu └─38192 systemctl stop myservice oct 25 12:31:51 user-VirtualBox myservicedsp[38182]: dpkg: warning: downgrading myservice from 1.1.7-ubuntu to 1.1.5-ubuntu oct 25 12:31:53 user-VirtualBox myservicedsp[38182]: (Reading database ... 216528 files and directories currently installed.) oct 25 12:31:53 user-VirtualBox myservicedsp[38182]: Preparing to unpack .../07080415-acf7-4404-a086-edd3ca743a22.deb ... oct 25 12:31:53 user-VirtualBox myservicedsp[38164]: terminate called without an active exception oct 25 12:31:53 user-VirtualBox systemd[1]: Stopping MyService daemon...
最终更新因超时失败,显然服务尝试停止自身时陷入了死锁,如何避免该问题?
解决方法
1. 让更新进程脱离服务的cgroup
死锁根源是:dpkg进程是服务进程的子进程,属于服务的cgroup。当.deb的prerm脚本执行systemctl stop myservice时,systemd会等待服务cgroup下的所有进程退出,包括正在运行的dpkg和它的子进程;而dpkg又在等待prerm脚本执行完成,也就是等待systemctl stop结束,形成循环死锁。
解决的核心是让dpkg进程脱离服务的cgroup,不在systemd的服务控制范围内,以下是两种实现方式:
方法A:两次fork创建独立进程
修改更新代码,通过两次fork让执行dpkg的进程成为孤儿进程,被init进程接管,自动脱离原服务的cgroup:
err_t SoftUpdater::_installServiceUpdate(const std::string &file_path) { const char* const cmd[] = { "dpkg", "-i", file_path.c_str() , NULL}; int pid = fork(); if (pid == 0) { // 第一次fork的子进程,再执行一次fork if (fork() == 0) { // 第二次fork的子进程,执行dpkg execvp(cmd[0], cmd); DBG(E, "execvp failed: %i, %s", errno, strerror(errno)); exit(1); } // 第一次fork的子进程直接退出,让孙子进程成为孤儿 exit(0); } else if (pid > 0) { // 父进程等待第一次fork的子进程退出,避免产生僵尸进程 waitpid(pid, nullptr, 0); } else { DBG(E, "fork failed"); } return E_OK; }
方法B:手动修改进程cgroup
在execvp执行前,将进程移动到systemd的根cgroup:
#include <sys/mount.h> #include <unistd.h> #include <fcntl.h> // 在子进程中执行execvp前添加以下代码 if (mount(nullptr, "/sys/fs/cgroup/systemd", nullptr, MS_SLAVE | MS_REC, nullptr) == 0) { chdir("/sys/fs/cgroup/systemd"); int fd = open(".", O_WRONLY); if (fd != -1) { write(fd, "1", 1); close(fd); } else { DBG(W, "Failed to move to root cgroup"); } }
2. 修改.deb包的prerm脚本逻辑
如果有权限修改.deb包的维护脚本,可以在prerm中调整停止逻辑:
- 检查当前进程的父链是否属于myservice的cgroup,如果是,则跳过
systemctl stop,改用通知服务自行优雅退出的方式。 - 服务启动时创建一个标记文件,
prerm脚本检查该文件是否存在,若存在则触发服务内部的退出流程,而非强制执行systemctl stop。
3. 调整服务的systemd配置
修改/lib/systemd/system/myservice.service,添加以下配置,让systemd停止服务时只等待主进程退出:
[Service] # 停止服务时仅等待主进程退出,不等待子进程 KillMode=process
注意:该配置可能导致服务停止时残留子进程,需要确保服务自身能正确管理组件的生命周期。
4. 使用独立更新进程触发更新
不在服务内部直接执行dpkg,而是将下载好的.deb包路径通过socket、文件通知等方式传递给一个独立的辅助进程,由它执行更新操作。这种方式彻底隔离了更新流程与服务运行流程,从根源避免死锁。
内容的提问来源于stack exchange,提问作者WereWind

