Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?

Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断? 为什么当 exec.Cmd.Stdoutcmd.Stderr 指向同一个 bytes.Buffer 时,超过 65536 的 exec 命令输出会被截断,如下所示?

package main

import (
	"bytes"
	"encoding/json"
	"os"
	"os/exec"

	"k8s.io/klog/v2"
)

func main() {
	klog.Infof("==============1==================")
	args := []string{"bucket", "stats"}

	execCmd := exec.Command("radosgw-admin", args...)
	f, err := os.Create("xxxx")
	if err != nil {
		panic(err)
	}
	execCmd.Stdout = f

	err = execCmd.Run()
	if err != nil {
		panic(err)
	}
	f.Close()

	bs, err := os.ReadFile("xxxx")
	if err != nil {
		panic(err)
	}
	klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", len(bs))

	klog.Infof("==============2==================")
	execCmd = exec.Command("radosgw-admin", args...)
	var b bytes.Buffer
	execCmd.Stdout = &b

	err = execCmd.Run()
	if err != nil {
		panic(err)
	}
	klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", b.Len())

	klog.Infof("==============3==================")
	execCmd = exec.Command("radosgw-admin", args...)
	var b2 bytes.Buffer
	execCmd.Stdout = &b2
	execCmd.Stderr = &b2   // <== here

	err = execCmd.Run()
	if err != nil {
		panic(err)
	}
	klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", b2.Len())

	klog.Infof("==============4==================")
	execCmd = exec.Command("radosgw-admin", args...)
	f, err = os.Create("yyyy")
	if err != nil {
		panic(err)
	}
	execCmd.Stdout = f
	execCmd.Stderr = f

	err = execCmd.Run()
	if err != nil {
		panic(err)
	}
	f.Close()

	bs2, err := os.ReadFile("yyyy")
	if err != nil {
		panic(err)
	}
	klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", len(bs2))
	
	klog.Infof("==============5==================")
	execCmd = exec.Command("radosgw-admin", args...)
	var b3 bytes.Buffer
	var b4 bytes.Buffer
	execCmd.Stdout = &b3
	execCmd.Stderr = &b4

	err = execCmd.Run()
	if err != nil {
		panic(err)
	}
	klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d, err: %d", b3.Len(), b4.Len())
}

输出:

root@node01:/# ./test 
I1207 10:53:47.150299   57435 test.go:36] ==============1==================
I1207 10:53:48.800277   57435 test.go:56] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:48.803742   57435 test.go:64] ==============2==================
I1207 10:53:50.421419   57435 test.go:73] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:50.421446   57435 test.go:76] ==============3==================
I1207 10:53:52.537304   57435 test.go:86] exec.Command(radosgw-admin bucket stats), length: 65536
I1207 10:53:52.537343   57435 test.go:89] ==============4==================
I1207 10:53:55.030006   57435 test.go:108] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:55.035473   57435 test.go:115] ==============5==================
I1207 10:53:58.429107   57435 test.go:126] exec.Command(radosgw-admin bucket stats), length: 404237, err: 0
root@node01:/# radosgw-admin bucket stats | wc -c
404237

更多关于Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个问题涉及 Go 标准库 os/exec 包中管道缓冲区的内部实现。当 StdoutStderr 指向同一个 bytes.Buffer 时,超过 65536 字节的输出被截断,是因为 Go 的 exec.Cmd 内部使用了固定大小的管道缓冲区,并且在并发写入同一个缓冲区时可能发生阻塞。

具体原因是:exec.Cmd 启动子进程后,会为 StdoutStderr 分别创建管道(如果它们不是文件描述符)。每个管道的缓冲区大小在 Linux 系统上默认为 65536 字节(64KB)。当两个管道同时向同一个 bytes.Buffer 写入时,如果其中一个管道填满了缓冲区,而另一个管道还在等待缓冲区读取,就可能发生死锁,导致输出被截断。

以下是重现问题的简化示例:

package main

import (
    "bytes"
    "fmt"
    "os/exec"
)

func main() {
    // 生成超过 64KB 输出的命令
    cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
    
    var buf bytes.Buffer
    cmd.Stdout = &buf
    cmd.Stderr = &buf  // 指向同一个缓冲区
    
    err := cmd.Run()
    if err != nil {
        fmt.Printf("命令执行错误: %v\n", err)
    }
    
    fmt.Printf("输出长度: %d (预期: 71KB+)\n", buf.Len())
    // 实际输出可能只有 65536 字节
}

要解决这个问题,可以使用独立的缓冲区分别捕获标准输出和标准错误:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
    
    var stdoutBuf, stderrBuf bytes.Buffer
    cmd.Stdout = &stdoutBuf
    cmd.Stderr = &stderrBuf
    
    err := cmd.Run()
    if err != nil {
        fmt.Printf("命令执行错误: %v\n", err)
    }
    
    // 合并输出(如果需要)
    var combinedBuf bytes.Buffer
    combinedBuf.Write(stdoutBuf.Bytes())
    combinedBuf.Write(stderrBuf.Bytes())
    
    fmt.Printf("输出长度: %d\n", combinedBuf.Len())
}

或者使用 io.MultiWriter 将输出复制到多个写入器:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
    
    var buf bytes.Buffer
    // 创建多写入器,确保并发安全
    stdoutWriter := io.MultiWriter(&buf)
    stderrWriter := io.MultiWriter(&buf)
    
    cmd.Stdout = stdoutWriter
    cmd.Stderr = stderrWriter
    
    err := cmd.Run()
    if err != nil {
        fmt.Printf("命令执行错误: %v\n", err)
    }
    
    fmt.Printf("输出长度: %d\n", buf.Len())
}

在 Linux 系统上,管道缓冲区大小可以通过 fcntl 系统调用设置,但 Go 的 os/exec 包没有暴露这个接口。因此,最可靠的解决方案是避免将 StdoutStderr 重定向到同一个内存缓冲区,特别是当预期输出较大时。

回到顶部