You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 08:50:36