golang 执行命令 设置超时

目录

0x00 前言

0x01 首先跑一个 os/exec

0x02 os/exec 黑魔法

0x03 Channel & Goroutine

Channel

Goroutine

一些问题

0x04 带 timeout 执行命令


最近有一个需求,用 Golang 执行一条命令,并且需要为该命令设置一定时间的 timeout。由此展开,我们从执行一条基本的 shell 命令开始,逐渐研究黑魔法。

0x00 前言

本文我们假设没有任何前置知识,可放心食用。

正好昨天面试问到了 Go-routine 的相关原理问题,以及 CSP 相关概念问题。开篇先 diss 一波这位面试同学,感觉非常学院派,不是很懂问一个安全算法岗 Engineer 而不是 Researcher 这些问题的意义,一个初阶 user 是肯定不知道 CSP 并发模型 (Concurrent mode) 的定义的,并且还问了”诸如你既然喜欢 Golang 为什么不深入研究“之类引起不适的问题(我当时就说了刚接触 ML 没研究语言特性)这里就简单学习一下。

然而,文章还是以此作为标题,末尾会附上开头的需求之外,关于这部分知识的一些面试小技巧,工作面前都是舔狗(x

0x01 首先跑一个 os/exec

我们用官方文档的样例代码运行一条命令,获取输出。

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	outBytes, err := cmd.Output()
	if err != nil {
		log.Fatal(err)
	}
	stdout = string(outBytes)
	return
}

func main() {
	log.Print(RunCommand("echo", "fuck"))
}

输出了2018/11/21 22:27:00 fuck

0x02 os/exec 黑魔法

os/exev 有一些黑魔法,

我们新建了一个名为 echo_loop 的 bash 脚本:

while true; do date; sleep 1; done

当我们执行RunCommand("bash", "echo_loop.sh"),程序卡住了。显然这个脚本在不断地输出,我们希望不断取得的中间输出。但是我们不能让他卡住,不然怎么检测 timeout 呢。我们有一些针对的 command 对象的操作,cmd.Run()是要等待命令运行完成的,而cmd.Start()就可以让命令不要 wait:

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
//	stdout = string(outBytes)
	return
}

然而,此时要想获得输出,可以通过cmd.StdoutPipe()(需要注意的是,这个需要在Start()之前完成,不然会输出的内容会是exec: StdoutPipe after process started)。加上这个之后我们又卡住了:

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	out, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	defer out.Close()
	outBytes, err := ioutil.ReadAll(out)
	if err != nil {
		log.Fatal(err)
	}
	stdout = string(outBytes)
	return
}

当然,还有一个cmd.Wait(),返回一个 error 类型,可以这样获得输出。不幸的是也会卡住:

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	outBytes := cmd.Wait()
	stdout = outBytes.Error()
	return
}

我们很自然的想到一个操作,我们需要在一个命令不在前台执行(为了控制 timeout),并获得它的输出。很幸运,Golang 实现了这一套机制。

0x03 Channel & Goroutine

我们首先看一段代码:

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	time.Sleep(2 * time.Second)
	c <- sum // 发送数据到 Channel
}

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	c := make(chan int)
	log.Print("Waiting")
	go sum(s[len(s)/2:], c) // 前半段
	go sum(s[:len(s)/2], c) // 后半段
	x, y := <-c, <-c // 分别阻塞地接收数据
	log.Print("Done")
	log.Print(x, y)
}

// Output:
//	2018/11/21 23:51:54 Waiting
//	2018/11/21 23:51:56 Done
//	2018/11/21 23:51:56 15 6

Channel

Channel 是 Go 中的一个核心类型。你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。基本操作方式如下:

ch := make(chan int)	// 新建一个 Channel
ch <- v					// 发送值 v 到 Channel ch中
v := <-ch				// 从 Channel ch 中接收数据,并将数据赋值给 v

一个 Channel 是可以设置单双向,收发类型,容量的。关于 Channel 的一些知识,可以参考官方文档或这个文章。值得注意的一个问题是,在进行一次 receive 操作时,如果 Channel 中还没有数据,会使得程序在此阻塞,此时应从一个 goroutine 向其中写数据。存取道理一样,都是阻塞的,需要一个 goroutine 来读取才行。

Goroutine

关于 Routine,其实没有什么复杂的调用方式,调用函数前加一个标识符go就可以了,所以我们常叫做 goroutine。可以看做是我们熟知的线程,但是在实现上更轻。Golang 实现的是一个线程池。具体实现会在后面【0x05 面试小技巧】部分说到。

Goroutine 和 Channel 也是相辅相成的。由于没有类似 Python 中thread.join()的概念。正如之前所说,Channel 的存取是阻塞的。默认的 Channel 大小是 0,我们把这叫做无缓冲。无缓冲的 Channel 在取消息和存消息的时候都会挂起当前的 goroutine。因此,我们可以完成 goroutine 向一个 Channel 写数据,写完之后该 Channel 可以读到数据,继续向下执行剩余部分。

默认的 Channel 大小是 0,叫做无缓冲。我们不用默认值,给一个大小,就可缓冲了。试图让无缓冲 Channel 承载数据是死锁的重要原因之一,给 Channel 一个大小可以避免。

一些问题

Q1:写入数据一直没读会怎样?没有数据一直在读会怎样?

都是死锁,错误信息为all goroutines are asleep - deadlock

我们打印 Done 之前增加一句c <- 0,就会出现一个死锁。这个数据没有人读。

我们打印 Done 之前增加一句_ = <- c,也会出现一个死锁。没有数据一直在读。

Q2:假设同时加上这两句话呢?

也是死锁。

划重点:单一的 goroutine 中操作数据一定死锁。在传入 0 到 c 的时候,此时需要一个线程池中,已经 join 的一个线程来读,此时除了 main 这个线程,没有别的 goroutine,正如错误提示所说,all goroutines are asleep。

Q3:还能怎么死锁?

好问题。我们要知道死锁的原理,是出现了循环读取、循环等待,使得一个无缓冲的 Channel 承载数据。这里给一个典型的循环等待的例子:

c, quit := make(chan int), make(chan int)

go func() {
   c <- 1 // c 正在等待数据被取走,才能执行 quit
   quit <- 0 // 楼上等待期间这里也在等
}()

x := <- quit // quit 在等待 goroutine 有人写
y := <- c // 由于一直没有 quit,c 中数据不可能被取走

通俗点说,依次存入 Channel 的数据,需要依次取出。如果真的需要不能依次取出,正如之前索索,可以给 Channel 一个大小。这里写成c, quit := make(chan int, 1), make(chan int, 1),上面的程序就不会死锁了。

当然,bug 千千万,我们在这里也给一个不死锁的不是 bug 的反例。其原因是,这个 goroutine 在运行前,主程序就退出了:

c := make(chan int)

go func() {
   c <- 1
}()

0x04 带 timeout 执行命令

我们好像已经解决了最初的问题:需要在一个命令不在前台执行(将cmd.Wait()启动成一个 goroutine),并获得它的输出(通过 Channel)在原来的代码上修改有这些:

  1. 为了避免在发送 SigInt 和 Kill 线程时管道之间关闭顺序的其妙操作(StdoutPipe 在结束后无法获取内容,未结束时会阻塞等待),参考了这篇文章中这段的处理方式,通过 bytes buffer 来接受。
  2. 通过 select 来处理正常运行完成和 Kill 结束的命令的操作。
  3. 在发送 SigInt 信号后,有些命令会输出一些信息(比如 ping),在 Kill 前添加了 10ms 延时。

代码如下,可直接复制测试运行:

package main

import (
	"syscall"
	"time"
	"bytes"
	"log"
	"os/exec"
)

func RunCommandWithTimeout(timeout int, command string, args ...string) (stdout, stderr string, isKilled bool) {
	var stdoutBuf, stderrBuf bytes.Buffer
	cmd := exec.Command(command, args...)
	cmd.Stdout = &stdoutBuf
	cmd.Stderr = &stderrBuf
	cmd.Start()
	done := make(chan error)
	go func() {
		done <- cmd.Wait()
	}()
	after := time.After(time.Duration(timeout) * time.Millisecond)
	select {
		case <-after:
			cmd.Process.Signal(syscall.SIGINT)
			time.Sleep(10*time.Millisecond)
			cmd.Process.Kill()
			isKilled = true
		case <-done:
			isKilled = false
	}
	stdout = string(bytes.TrimSpace(stdoutBuf.Bytes())) // Remove \n
	stderr = string(bytes.TrimSpace(stderrBuf.Bytes())) // Remove \n
	return
}

func main() {
	resultOut, resultErr, resultStat := RunCommandWithTimeout(3000, "ping", "baidu.com")
	
	log.Print("Is Killed: ", resultStat)
	log.Print("Res: \n===\n", resultOut, "\n===")
	log.Print("Err: \n===\n", resultErr, "\n===")
}

// Output:
//	2018/11/22 01:47:44 Is Killed: true
//	2018/11/22 01:47:44 Res: 
//	===
//	PING baidu.com (220.181.57.216): 56 data bytes
//	64 bytes from 220.181.57.216: icmp_seq=0 ttl=53 time=44.607 ms
//	64 bytes from 220.181.57.216: icmp_seq=1 ttl=53 time=44.476 ms
//	64 bytes from 220.181.57.216: icmp_seq=2 ttl=53 time=45.519 ms
//
//	--- baidu.com ping statistics ---
//	3 packets transmitted, 3 packets received, 0.0% packet loss
//	round-trip min/avg/max/stddev = 44.476/44.867/45.519/0.464 ms
//	===
//	2018/11/22 01:47:44 Err: 
//	===
//
//	===

猜你喜欢

转载自blog.csdn.net/whatday/article/details/113837586