golang的CommandContext取消不退出问题

我用golang的os/exec库的command类来调用shell脚本,发现强制cancel后cmd调用没有立即返回,代码流程如下。

首先创建了用于中断cmd的context:

然后调用cmd执行shell,传入cancelCtx用于中断脚本执行:

当我想终止脚本时,我会调用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版本解决。

 

 

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~