Corral 能在 30 秒内确保子进程全部被清理
在使用 AI 编码代理或 CI 任务时,常会遇到子进程残留、端口占用或 IO 卡死的情况。Corral 通过独立会话和可选 cgroup,保证在命令结束后所有衍生进程都被杀死;若检测不到完整清理则返回 120。下面把原理、使用方法和几处坑点拆开说清楚,照着走基本就能在自己的 runner 里复现。
为什么普通 runner 常会留下进程
- 双重 fork 并 setsid
- 进程会在子进程内部再次 fork 并调用 setsid(),形成新的进程组和会话。父进程结束后,信号只会发送到原来的进程组,新的组因为父已经是 init,根本收不到。
- 后台进程保持 stdout/stderr 打开
- runner 等待子进程的输出管道关闭。如果后台进程仍持有写端,管道永远不 EOF,导致 runner 卡住。
- 进程自行忽略 SIGTERM
- 即使收到终止信号,进程仍继续运行,端口、文件锁等资源被占用,后续任务会因冲突报错。
这三类情况在多数开源 runner(如 GitHub Actions、GitLab CI)里都会出现,因为它们只向直接启动的进程或其进程组发送信号。
Corral 是怎么解决的
- 独立会话:Corral 为目标命令创建全新的 session(
setsid),这样即使命令内部再 fork,也不会把信号回传到父进程。 - 可选 cgroup:若系统支持 cgroup,Corral 把整个进程树放进一个临时 cgroup,结束时直接删除 cgroup,保证树上所有进程一起被杀。
- /proc 追踪:在没有 cgroup 的环境下,Corral 通过遍历
/proc树来追踪子进程。该方式只能在父进程死亡后捕获已改会话的子进程,且无法处理切换到其他用户或交给 systemd 的服务。 - 退出码 120:在所有清理手段都尝试过后仍发现有进程存活,Corral 会以 120 退出,提示调用者需要人工干预。
基本使用方式
# 让命令在 30 秒内执行完毕,结束后确保没有残留进程
corral --wall 30s -- yourcommand arg1 arg2
--wall 30s:设定最大运行时间,超时后 Corral 会强制终止整个进程树。--:后面的所有内容都视为要被管理的命令行,避免参数冲突。- 返回码:
- 0:命令正常结束且所有子进程已被清理。
- 124(或类似超时码):命令因超时被杀。
- 120:Corral 未能确认所有进程已死亡。
手动检查残留进程(可选)
如果返回码是 120,可以在同一台机器上运行:
ps -ef | grep yourcommand
定位仍在运行的进程,手动 kill -9 <pid> 或检查 cgroup 状态。
可能的限制与注意点
- cgroup 权限:创建临时 cgroup 需要 root 或相应的 CAP_SYS_ADMIN 权限。普通 CI 运行器如果没有这些权限,只会回退到
/proc追踪。 - 跨用户进程:如果子进程切换到别的用户(例如使用
sudo -u),Corral 的/proc追踪将失效,进程会残留。 - 系统服务交接:某些守护进程会将自身交给
systemd(如systemd-run),此时即使在同一 session 内,也会在父进程退出后被 systemd 接管,Corral 也无法彻底清除。 - 资源占用:在高并发 CI 场景下频繁创建 cgroup 可能导致内核的 cgroup 控制块耗尽,需要提前做好配额规划。
小结
Corral 把 “运行命令 → 必须全部清理” 这件事做成了一个可复用的工具:通过独立 session 防止信号泄漏,优先使用 cgroup 实现“一键干掉树”,没有 cgroup 时回退到 /proc 追踪,并在清理失败时用 120 提示。只要你的 CI 环境能提供必要的权限,直接换成 corral --wall … -- <cmd>,大多数后台残留、IO 卡死的问题都能被消灭。
timeout 连 setsid 孤儿都杀不死,Corral 报 120 才是硬伤,你这提议根本没用。