golang的CommandContext取消不退出问题
我用golang的os/exec库的command类来调用shell脚本,发现强制cancel后cmd调用没有立即返回,代码流程如下。
首先创建了用于中断cmd的context:
1 |
jobExecuteInfo.CancelCtx, jobExecuteInfo.CancelFunc = context.WithCancel(context.TODO()) |
然后调用cmd执行shell,传入cancelCtx用于中断脚本执行:
1 2 3 4 5 |
// 创建Shell任务 cmd = exec.CommandContext(info.CancelCtx, "/bin/bash", "-c", "sleep 60;") // 执行命令, 捕获输出 output, err = cmd.CombinedOutput() |
当我想终止脚本时,我会调用cancelFunc,结果发现脚本依旧60秒后退出,退出后错误码是Killed。
一开始怀疑cancelFunc未能触发Command杀死子进程,所以当我调用cancelFunc后去ps aux看了一下/bin/bash进程的确被杀死了,那为什么CombinedOutput方法仍旧60秒后才返回呢?
于是找到了这个issue:https://github.com/golang/go/issues/23019。
我们知道,go与/bin/bash之间架设了2个pipe,分别用于捕获/bin/bash的stderr和stdout输出。本来杀死/bin/bash进程后,pipe的写入端将被自动关闭,从而go可以感知到子进程退出,从而立刻返回,但现在没有返回必有其他原因。
仔细思考发现,/bin/bash其实又启动了一个sleep子进程,导致pipe继续被fork带到了sleep进程,而go只是杀死了/bin/bash而没有杀死sleep进程,导致了pipe需要等到sleep退出后才被关闭,而go的标准库实现是一定要等到pipe关闭才返回的。
在调试过程中发现更有意思的一件事是,sleep 60和sleep 60;对于/bin/bash -c是完全不同的,前者被bash认为是单条命令所以直接在/bin/bash进程空间内exec直接执行了sleep二进制,而后者bash则认为脚本存在多条命令,所以会fork子进程exec执行sleep,从而导致了不同的结果。
目前官方已经注意到这个问题,可能在后续版本中增加机制来解决。
我能想到的2个解决思路,大家可以自己尝试一下:
- 直接用Process库来实现子进程交互,并以子进程退出为依据,而不是以pipe关闭为依据。
- 等待官方修复,官方可能会给Command库增加一些wait超时机制,或者为何不直接kill进程组呢?目前官方把这个问题归属于1.11版本解决。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

这个问题,我也遇到过,楼主可以看下我的处理
https://www.jianshu.com/p/e147d856074c
精彩!
我看了一下,你的做法可以解决linux上的问题,但是不具备跨平台兼容性,sysProcAttr的行为是平台不同的。
Go 1.11好像并没有修复这个问题,依然会出现博主的情况
好的,稍等我看一下API是否有变动。
go1.12.7 还是有这个问题-_-
可惜了。
1.20推出了WaitDelay来做处理了