如何确保子进程与父进程处于同一个cgroup中?

后端开发 2026-07-12

我正在基于bubblewrap和 systemd的 cgroup管理开发一款自己的沙箱工具。我通过DBus API动态创建一个systemd scope,然后启动一个运行bubblewrap沙箱的子进程。bubblewrap进程及其子进程没有被加入到cgroup。

目前我让systemd知道我的包装进程的pidfd(这和systemd-run做的一样),它只把该进程放入cgroup,而不会把它的子进程放进去。我也尝试把子进程放入该作用域,但仍然无果。

这就是cgroup的样子:

app.slice
│   │ │ ├─my-unit.scope
│   │ │ │ └─155454 /usr/bin/bwrap --bind /usr /usr --symlink /usr/lib64 /lib64 --new-session --unshare-all --clearenv --die-with-parent bash

这应该像systemd-run那样的样子:

app.slice
│   │ │ ├─my-unit.scope
│   │ │ │ ├─156845 /usr/bin/bwrap --new-session --die-with-parent --unshare-all --bind /usr /usr --symlink /usr/lib64 /lib64 --clearenv bash
│   │ │ │ ├─156846 /usr/bin/bwrap --new-session --die-with-parent --unshare-all --bind /usr /usr --symlink /usr/lib64 /lib64 --clearenv bash
│   │ │ │ └─156847 bash

这是我当前用Rust编写的代码:

async fn setup_process(profile: Profile, conn: &zbus::Connection) -> Result<Child, Box<dyn Error>> {
    let mut command = Command::new("/usr/bin/bwrap");
    command.env_clear();
    command.kill_on_drop(true);
    command.reap_on_drop(true);

    for bind in profile.binds.iter() {
        let source = format!("{}", bind.source.display());
        let target = format!("{}", bind.target.display());
        if bind.read_only {
            command.arg("--ro-bind").arg(source).arg(target);
        } else {
            command.arg("--bind").arg(source).arg(target);
        }
    }

    for symlink in profile.symlinks.iter() {
        let source = format!("{}", symlink.source.display());
        let target = format!("{}", symlink.target.display());
        command.arg("--symlink").arg(source).arg(target);
    }

    command.arg("--new-session");
    command.arg("--unshare-all");
    command.arg("--clearenv");
    command.arg("--die-with-parent");

    command.arg(profile.command);

    for arg in profile.args.iter() {
        command.arg(arg);
    }

    let pid = nix::unistd::getpid();
    let pidfd_raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) as RawFd };
    if pidfd_raw < 0 {
        return Err(io::Error::new(io::ErrorKind::Unsupported, "unable to open pidfd").into());
    }
    let pidfd: zvariant::Fd = unsafe { OwnedFd::from_raw_fd(pidfd_raw) }.into();

    let proxy = systemd::manager::ManagerProxy::new(&conn).await?;

    let unit = systemd::properties::Unit::builder()
        .description("My Transient Unit")
        .collect_mode("inactive-or-failed")
        .build();

    let resouces = systemd::properties::Resources::builder()
        .slice("app.slice")
        .memory_max(20971520)
        .memory_swap_max(0)
        .build();

    let pidfds = &[pidfd];

    let scope = systemd::properties::Scope::builder()
        .unit(unit)
        .resources(resouces)
        .pidfds(pidfds)
        .build();

    let _ = proxy
        .start_transient_unit("my-unit.scope", "fail", &Vec::from(scope), &[])
        .await?;

    let err = command.exec();
    Err(err.into())
}

编辑:

看来在Rust实现的Command中似乎并不存在问题,因为调用 let err = nix::unistd::execvpe(cmd.as_ref(), args.as_slice(), env)?;(systemd-run的做法)也会以相同的方式失败。

编辑2:

我发现 这个 看起来是一个类似的问题,但其实并没有解决。

编辑3:

找到了缺失的子进程,出于某种原因它们被放在了我的终端所在的作用域下,仍然不知道为什么

kitty-122960-0.scope
│   │ │   ├─122971 /usr/bin/bash
│   │ │   ├─231054 /usr/bin/bwrap --bind /usr /usr --symlink /usr/lib64 /lib64 --new-session --unshare-all --clearenv --die-with-parent bash
│   │ │   └─231055 bash

解决方案

在阅读了 systemd-runrunapp 的源代码(它们做的是同一件事)后,我找到了正确的解决办法。似乎我的exec在作用域实际初始化之前就已经运行,因此子进程在systemd把它们放入正确的作用域之前就已经创建。解决方案是等待systemd确认作用域已经实际创建,然后再执行exec。

在执行exec之前我添加了以下这段:

let mut job_result: String = "".into();
let mut job_removed_stream = proxy.receive_job_removed().await?;
while let Some(msg) = job_removed_stream.next().await {
    let args: JobRemovedArgs = msg.args()?;
    if args.job.as_str() == job_path.as_str() {
        job_result = args.result.into();
        break;
    }
}

if job_result == "failed" {
    return Err(io::Error::new(io::ErrorKind::Other, "failed to start scope").into());
}

if job_result != "done" {
    return Err(io::Error::new(io::ErrorKind::Other, job_result.as_str()).into());
}

总的来说,systemd的 DBus API文档非常不完善,希望这个解决方案能帮助后来的人。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章