为什么从 StdoutPipe 读取会改变输出顺序?

Why does reading from StdoutPipe changes order of output?

我有以下调用命令的函数,但我无法在此处发布。以前我使用 cmd.Stdout = os.Stdout 在我的命令行上获取命令输出,效果很好:

func run(cmd *exec.Cmd) {
    cmd.Stdout = os.Stdout
    if err := cmd.Start(); err != nil {
        panic(err)
    }
    if err := cmd.Wait(); err != nil {
        panic(err)
    }
    return
}

这给了我以下命令行输出,这很棒:

[loader] loading programm...
[programm] foo bar

现在我需要先与输出交互,所以我尝试使用 StdoutPipe:

func run(cmd *exec.Cmd) {
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        panic(err)
    }
    if err := cmd.Start(); err != nil {
        panic(err)
    }
    reader := bufio.NewReader(stdout)
    for {
        s, err := reader.ReadString('\n')
        if err != nil {
            break
        }
        //  do some evaluation of s here
        fmt.Print(s)
    }
    if err := cmd.Wait(); err != nil {
        panic(err)
    }
    return
}

但这打乱了消息的顺序,我没有得到以下输出:

[programm] foo bar
[loader] loading programm...

为什么输出不再按顺序排列?如何在使用 stdoutPipe 时保持输出按顺序排列?

编辑: 如果我尝试像这样使用 io.MultiWriter,也会发生同样的情况:

func run(cmd *exec.Cmd) {
    var reader bytes.Buffer
    cmd.Stdout = io.MultiWriter(os.Stdout, &reader)
    if err := cmd.Start(); err != nil {
        panic(err)
    }
    for {
        s, err := reader.ReadString('\n')
        if err != nil {
            break
        }
        fmt.Print(s)
    }
    if err := cmd.Wait(); err != nil {
        panic(err)
    }
    return
}

很大程度上取决于您 运行 的程序。

那个:

I used stdbuf -o0 to deactivate the buffering

这只会影响程序的 stdout(POSIX 系统上的文件描述符 1),而不影响其 stderr(文件描述符 2)。但是 POSIX 系统通常使用以下默认值,至少对于 C 语言程序或那些使用 C 运行 时间系统的程序:

  • stdin:缓冲不是特别相关,但输入通常会完全缓冲。
  • stdout:如果文件描述符 1 是“tty”,则缓冲行;否则完全缓冲。
  • stderr:无缓冲。

通过调用 C 库 isatty 函数来确定什么是“tty”,该函数通常使用 termios operation to do this. For instance, the glibc isatty calls tcgetattr and verifies that it returns without error. The glibc tcgetattr uses the TCGETATTR ioctl。所以“tty 设备”是实现 TCGETATTR.

的设备

过去使用的实际串行线(例如 COM1 和 COM2),或终端集中器设备,例如 中的 DH/DM 或 DZ 旧日,当然实行TCGETATTR。伪终端也是如此,例如交互式 ssh 会话、script 和其他程序所使用的伪终端。这使得 1970 年代和 1980 年代的所有旧软件能够继续使用虚拟化 ASR33, ADM3A, or DECwriter 继续工作。1

无论如何,这里的关键是管道——或插座或类似物——实现TCGETATTR,所以如果 stdout 是一个管道,那么 isatty(1) 就是 0(假)。这使得 stdout 完全缓冲.

请记住,这一切都发生在 您 运行 的程序中。您自己的程序,读取其他程序的输出,(不一定)不是问题。当其他程序实际 发送 时,您的程序仅 获取 输出。在全缓冲模式下,其他程序在填满缓冲区块或调用某些输出刷新操作时仅 发送 输出。

一些程序将输出混合到 stdout——他们通常假设 是行缓冲的,因为编写(有缺陷的)程序的人只用 tty 输出测试它——并输出到标准错误。当 stdout 被重定向到一个文件或管道时,这些程序会表现得很奇怪。像 stdbuf 这样的程序——可以作为库预加载实现,hence might not work on setuid programs——可以用来解决这些问题。

在 Go 中,缓冲更多地取决于您自己的程序。 Go 不使用 POSIX 设置。例如,参见 Idiomatically buffer os.Stdout


1我喜欢我的虚拟Tek4025,Heathkit H19等,更好,虽然旧的打印终端有话要说!或旧的 AAA 终端,在斜视模式下最多可显示 60 条线路。 OK,Terminal、iterm2等都打不过了