为什么 exec.Command.Start() 会挂在达尔文身上?

Why might exec.Command.Start() hang on darwin?

我偶尔会挂起 运行使用官方版本似乎不会挂起的程序的特定开发版本。 dev 版本的不同之处主要在于它引入了更多的 Go std 库,这些库(在大多数情况下)它不使用;所以可执行文件更大,加上 static-var 和 init() 初始化已经完成,这可能会增加达到某些竞争条件的可能性。

git bisect run 将(golang)罪魁祸首确定为 6becb033341602f2df9d7c55cc23e64b925bbee2:

Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu Apr 11 16:53:11 2019 -0700

[...]

    runtime: switch to using new timer code

diff --git a/src/runtime/time.go b/src/runtime/time.go
index fea5d6871c..db48a932d4 100644
--- a/src/runtime/time.go
+++ b/src/runtime/time.go
@@ -14,7 +14,7 @@ import (
 )

 // Temporary scaffolding while the new timer code is added.
-const oldTimers = true
+const oldTimers = false

 // Package time knows the layout of this structure.
 // If this struct changes, adjust ../time/sleep.go:/runtimeTimer.

看了一眼这个小改动带来的差异,我强烈倾向于在它启用的 "new timer code" and/or 代码中存在一些竞争条件。

无论是通过 Ctrl-\ (SIGQUIT) 还是 delve attach,罪魁祸首似乎总是这里的 cmd.Start() 调用:

func sh(dir string, stdin io.Reader, stdout io.Writer, stderr io.Writer, name string, args []string) Object {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdin = stdin

var stdoutBuffer, stderrBuffer bytes.Buffer
if stdout != nil {
    cmd.Stdout = stdout
} else {
    cmd.Stdout = &stdoutBuffer
}
if stderr != nil {
    cmd.Stderr = stderr
} else {
    cmd.Stderr = &stderrBuffer
}

err := cmd.Start()
PanicOnErr(err)

那里的堆栈跟踪看起来非常相似,直到到达 syscall/exec_unix.go(在 Go 源代码树中)。然后,在 Delve 中,似乎挂起的是 forkAndExecInChild() 调用,而 Ctrl-\ 显示 readlen() 调用挂起:

// Kick off child.
pid, err1 = forkAndExecInChild(argv0p, argvp, envvp, chroot, dir, attr, sys, p[1])
if err1 != 0 {
    err = Errno(err1)
    goto error
}
ForkLock.Unlock()

// Read child error status from pipe.
Close(p[1])
n, err = readlen(p[0], (*byte)(unsafe.Pointer(&err1)), int(unsafe.Sizeof(err1)))
Close(p[0])
if err != nil || n != 0 {

forkAndExecInChild() 代码似乎挂在 exec_darwin.go:206,这是一个循环内对 libc_dup2_trampoline 的系统调用。假设这只是对 dup2() 的调用,我想不出它会挂起的任何原因;但我已经 "caught" 挂起测试 运行 那里(并且没有其他地方)至少两次,通过 delve,尽管这可能只是使用 delve attach <pid> ... 与 Ctrl- 的神器\ (SIGQUIT)?

多年来(好吧,几十年)我已经调试并修复了围绕此类活动的各种问题,但我对 Go 生态系统相对较新,并且在我对以下内容有所了解之前不想提交错误报告怎么回事。

特别是,Cmd.Start() 记录如下:

Start starts the specified command but does not wait for it to complete.

所以,从表面上看,似乎 st运行ge,如果不是彻头彻尾的错误,这些挂起似乎指向那个调用是罪魁祸首。 IE。如果它不等待,它为什么会挂起?也许看起来像直接 OS 调用实际上在底层 OS 调用之前或之后检查 Go 线程机制,并挂在那里。

问题出现在 运行 测试套件时,通常需要大约 12 秒才能 运行。我 运行 这个循环持续了大约 5 个小时来完成 git bisect run;虽然它通常在 15 分钟内触发,但我看到它需要 3 个多小时才能完成。

如果有人想更深入地研究(哈哈!)并尝试重现它,我正在研究的程序是 "Joker",这是开发版本(我的叉子):

https://github.com/jcburley/joker/(参见 b运行ch gostd;通过 ./run.sh 构建。)

运行ning ./all-tests.sh 时,OS X 上(偶尔)出现问题。到目前为止,挂起仅在该脚本 运行s ./flag-tests.sh./linter-tests.sh 时发生,尚未发生 ./eval-tests.sh(这似乎也是 st运行ge,因为由于按字母顺序排列,总是首先获得 运行)。

相同的测试套件 运行 在我的 Ubuntu Linux (Ryzen 3) 开发箱上循环运行超过 24 小时,没有挂起。 Windows 7 循环也持续了几个小时,到目前为止没有挂起。

重现更新:

与官方 (master/released) 版本相比,开发版本的 Joker 可执行文件要大得多;尽管大部分额外的代码都没有被这个小测试套件执行,但可能有一些 init() 或 static-var-init 代码,由于引入了额外的 Go std 库(包)而成为 运行,可能会做出更多贡献(如果不是完全)通过启动额外的 go and/or OS 线程、增加竞争等来解决纯粹的大小和与大小相关的问题

这是 MacOS 版 Go 的一个错误,已在 https://go-review.googlesource.com/c/go/+/372798/

中修复

对于修复之前受影响的 Go 版本,解决方法是将 -Wl,-bind_at_load 传递给链接器,这可以通过使用 -ldflags="-extldflags=-Wl,-bind_at_load"

调用 go 来完成