子进程的种族条件读取标准输出和标准错误

问题描述 投票:3回答:1

在Go中,我正在尝试:

  1. 启动子过程
  2. 分别从stdout和stderr读取
  3. 实现整体超时

经过大量的搜索之后,大多数时候我们提出了一些似乎可以完成任务的代码。但是似乎存在一种竞态条件,因此无法读取某些输出。

该问题似乎仅在Linux而不是Windows上发生。

按照google找到的最简单的解决方案,我们尝试创建具有超时的上下文:

context.WithTimeout(context.Background(), 10*time.Second)

尽管这种方法在大多数情况下都有效,但我们能够找到永远挂死的案例。子进程的某些方面导致此僵局。 (与未与子进程充分分离的子孙有关,从而导致子永远不会完全退出。)

而且,在某些情况下,当发生超时时返回的error似乎表示超时,但仅在完成交付后[[after时才退出(因此使整个超时概念成为现实)没用)。

func GetOutputsWithTimeout(command string, args []string, timeout int) (io.ReadCloser, io.ReadCloser, int, error) { start := time.Now() procLogger.Tracef("Initializing %s %+v", command, args) cmd := exec.Command(command, args...) // get pipes to standard output/error stdout, err := cmd.StdoutPipe() if err != nil { return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.StdoutPipe() error: %+v", err.Error()) } stderr, err := cmd.StderrPipe() if err != nil { return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.StderrPipe() error: %+v", err.Error()) } // setup buffers to capture standard output and standard error var buf bytes.Buffer var ebuf bytes.Buffer // create a channel to capture any errors from wait done := make(chan error) // create a semaphore to indicate when both pipes are closed var wg sync.WaitGroup wg.Add(2) go func() { if _, err := buf.ReadFrom(stdout); err != nil { procLogger.Debugf("%s: Error Slurping stdout: %+v", command, err) } wg.Done() }() go func() { if _, err := ebuf.ReadFrom(stderr); err != nil { procLogger.Debugf("%s: Error Slurping stderr: %+v", command, err) } wg.Done() }() // start process procLogger.Debugf("Starting %s", command) if err := cmd.Start(); err != nil { procLogger.Errorf("%s: failed to start: %+v", command, err) return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.Start() error: %+v", err.Error()) } go func() { procLogger.Debugf("Waiting for %s (%d) to finish", command, cmd.Process.Pid) err := cmd.Wait() // this can be 'forced' by the killing of the process procLogger.Tracef("%s finished: errStatus=%+v", command, err) // err could be nil here //notify select of completion, and the status done <- err }() // Wait for timeout or completion. select { // Timed out case <-time.After(time.Duration(timeout) * time.Second): elapsed := time.Since(start) procLogger.Errorf("%s: timeout after %.1f\n", command, elapsed.Seconds()) if err := TerminateTree(cmd); err != nil { return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), -1, fmt.Errorf("failed to kill %s, pid=%d: %+v", command, cmd.Process.Pid, err) } wg.Wait() // this *should* take care of waiting for stdout and stderr to be collected after we killed the process return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), -1, fmt.Errorf("%s: timeout %d s reached, pid=%d process killed", command, timeout, cmd.Process.Pid) //Exited normally or with a non-zero exit code case err := <-done: wg.Wait() // this *should* take care of waiting for stdout and stderr to be collected after the process terminated naturally. elapsed := time.Since(start) procLogger.Tracef("%s: Done after %.1f\n", command, elapsed.Seconds()) rc := -1 // Note that we have to use go1.10 compatible mechanism. if err != nil { procLogger.Tracef("%s exited with error: %+v", command, err) exitErr, ok := err.(*exec.ExitError) if ok { ws := exitErr.Sys().(syscall.WaitStatus) rc = ws.ExitStatus() } procLogger.Debugf("%s exited with status %d", command, rc) return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), rc, fmt.Errorf("%s: process done with error: %+v", command, err) } else { ws := cmd.ProcessState.Sys().(syscall.WaitStatus) rc = ws.ExitStatus() } procLogger.Debugf("%s exited with status %d", command, rc) return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), rc, nil } //NOTREACHED: should not reach this line! }
调用GetOutputsWithTimeout("uname",[]string{"-mpi"},10)将返回输出的预期单行

大部分时间。但是有时它会返回no output,好像读取stdout的goroutine不够快就无法“捕获”所有输出(或提前退出?)“大多数时候”强烈暗示了竞争状况。

我们有时还会从goroutine中看到有关“文件已关闭”的错误(这似乎是在超时情况下发生的,但也会在其他“正常”时间发生)。

我以为在cmd.Start()之前启动goroutine将确保不会丢失任何输出,并且使用WaitGroup将确保它们都将在读取缓冲区之前完成。

那么我们怎么会缺少输出?在两个“阅读器” goroutine和cmd.Start()之间是否仍然存在竞争条件?我们是否应该使用另外一个WaitGroup确保这两个正在运行?

或者ReadFrom()的实现是否有问题?

请注意,由于较早版本的操作系统存在向后兼容性问题,因此我们目前正在使用go1.10,但go1.12.4会产生相同的效果。

或者我们是否考虑得太多了,用context.WithTimeout()的简单实现就可以了吗?

go timeout race-condition child-process goroutine
1个回答
0
投票
但是有时它不会返回任何输出,就像读stdout的goroutine不够快就无法“捕获”所有输出
© www.soinside.com 2019 - 2024. All rights reserved.