起因

今天用Golang写一个调用外部shell脚本的程序,使用 exec.Command方法,这段脚本代码中包含了一个后台任务语句。 结果发生了一个问题:cmd.Output 始终不会返回,一直挂起。导致我的Go程序也挂起了。

问题还原

//main.go
package main

import (
	"fmt"
	"os/exec"
)

func main() {
	cmd := exec.Command("sh", "test.sh")
	out, err := cmd.Output()
	if err != nil {
		fmt.Errorf(err.Error())
		return
	}
	print(fmt.Sprintf("%s", out))
}

test.sh

#!/bin/bash

ping localhost &

启动golang程序 go run main.go, 此时会发现cmd.Output()始终不会返回。如果在终端直接执行sh test.sh就会直接返回了。 是什么原因导致的这个问题?

脚本执行方式

由于ping localhost会一直运行并且输出,我换了一个立即会结束的脚本:

#!/bin/bash

ls -la

为什么会想到用立即结束的脚本来验证问题,是因为我写这个程序的需求是使用golang写一个web服务来执行脚本。有的脚本是立马n返回,有的是一直输出的。

改成上面后,cmd.Output() 立马就返回了,看来是因为脚本一直有输出,导致 cmd.OutPut()一直不返回。

终端是如何把一个后台任务的输出显示在自己的输出中的? 可以参考菜鸟教程:Shell输入/输出重定向

引用里面的一段话: 大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。

解读源码

Go 等待进程退出的代码

结合这段代码再回到开始的ping localhost &语句(它在被sh执行的脚本中):

其中state, err := c.Process.Wait()等待的是sh进程的结束,没问题,sh很快就退出了, 看到这段代码:

var copyError error
for range c.goroutine {
    if err := <-c.errch; err != nil && copyError == nil {
        copyError = err
    }
}

这段代码会等待所有的 goroutines 退出,这些 goroutines 在干啥呢?可以再看下面这两段代码:

也就是说,开了一个 goroutine去把数据从子进程那里把数据读回来。

结论是什么?

cmd.Output() 会等待直接启动的那个进程的退出,并等待所有的连接标准输入/标准输出/标准错误管道全部关闭之后才会返回。只要这三者之一任意一个没有被关闭(而不是写没写/读没读),等待就会持续。

标准输入:由于本文中的ping是后台任务,sh根本就没把标准输入传递给它,sh退出时就把exec.Command的标准输入管道(的读端)给关闭了。所以这儿的goroutine早就结束了。 标准输出/标准错误:仍然和ping连接在一起

解决办法

那到底该怎么写呢,有两种思路

  1. 改脚本,可以把标准输出和标准错误重定向到文件中。还是参考菜鸟教程Shell输入/输出重定向
#!/bin/bash

#如果要保留输出,可以输出到文件
ping localhost > test.txt 2>&1 &
#如果输出无用,直接丢弃
ping localhost > /dev/null 2>&1 &
  1. 改golang程序, 使用 cmd.Start() 替代 cmd.Output()
package main

import (
	"fmt"
	"os/exec"
)

func main() {
	cmd := exec.Command("sh", "test.sh")
	out:= cmd.Start()
	print(fmt.Sprintf("%s", out))
}

Cmd的几个方法介绍,有阻塞和非阻塞的,按需使用。

func Command(name string, arg ...string) *Cmd
方法返回一个*Cmd 用于执行name指定的程序(携带arg参数)
func (c *Cmd) Run() error
执行Cmd中包含的命令阻塞直到命令执行完成
func (c *Cmd) Start() error
执行Cmd中包含的命令该方法立即返回并不等待命令执行完成
func (c *Cmd) Wait() error
该方法会阻塞直到Cmd中的命令执行完成但该命令必须是被Start方法开始执行的
func (c *Cmd) Output() ([]byte, error)
执行Cmd中包含的命令并返回标准输出的切片
func (c *Cmd) CombinedOutput() ([]byte, error)
执行Cmd中包含的命令并返回标准输出与标准错误合并后的切片
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
返回一个管道该管道会在Cmd中的命令被启动后连接到其标准输入
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
返回一个管道该管道会在Cmd中的命令被启动后连接到其标准输出
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
返回一个管道该管道会在Cmd中的命令被启动后连接到其标准错误

--完--