Golang | 调用带有后台进程shell脚本可能会被挂起
起因
今天用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 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。
解读源码
结合这段代码再回到开始的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
连接在一起
解决办法
那到底该怎么写呢,有两种思路
- 改脚本,可以把标准输出和标准错误重定向到文件中。还是参考菜鸟教程Shell输入/输出重定向
#!/bin/bash
#如果要保留输出,可以输出到文件
ping localhost > test.txt 2>&1 &
#如果输出无用,直接丢弃
ping localhost > /dev/null 2>&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中的命令被启动后连接到其标准错误
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2021/10/shell/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接