# Go 执行命令行
Go 语言中执行外部命令主要的方法是使用包 os/exec
。
此包的详细文档见 exec package - os/exec - pkg.go.dev (opens new window),这里只介绍几种常用操作。
执行命令也分几种情况:
- 仅执行命令;
- 执行命令,获取结果,不区分 stdout 和 stderr;
- 执行命令,获取结果,区分 stdout 和 stderr。
另外,默认的命令执行是在 go 进程当前的目录下执行的,我们可能还需要通过 cmd.Dir
指定命令执行目录。
# 1. 仅执行命令
执行命令,首先要拼接一下命令和参数,然后运行命令。
- 拼接命令与参数使用
exec.Command()
,其会返回一个*Cmd
;
func Command(name string, arg ...string) *Cmd
执行命令使用 *Cmd 中的 Run() 方法,Run() 返回的只有 error。
func (c *Cmd) Run() error
我们直接看代码:
func ExecCommand(name string, args ...string) {
cmd := exec.Command(name, args...) // 拼接参数与命令
if err := cmd.Run(); err != nil { // 执行命令,若命令出错则打印错误到 stderr
log.Println(err)
}
}
func main() {
ExecCommand("ls", "-l")
}
执行代码,没有任何输出。
上面的代码中,我们执行了命令 ls -l,但是没有得到任何东西。
# 2. 不区分 stdout 和 stderr
要组合 stdout 和 stderr 输出,,Cmd
中有方法:
func (c *Cmd) CombinedOutput() ([]byte, error)
用这个方法来执行命令(即这个方法是已有 Run() 方法的作用的,无需再执行 Run())。
我们修改上述代码:
func ExecCommand(name string, args ...string) {
cmd := exec.Command(name, args...) // 拼接参数与命令
var output []byte
var err error
if output, err = cmd.CombinedOutput(); err != nil {
log.Println(err)
}
fmt.Print(string(output)) // output 是 []byte 类型,这里最好转换成 string
}
func main() {
ExecCommand("ls", "-l")
}
我们得到了 ls -l 这条命令的输出.
# 3. 区分 stdout 和 stderr
区分 stdout 和 stderr,可以分别获取 StdoutPipe
和 StderrPipe
,然后分别对其进行读取。可以参考一下实现方式。
func main() {
cmd := exec.Command("ls", "-l")
wg := sync.WaitGroup{}
wg.Add(2)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
panic(err)
}
go func() {
defer wg.Done()
getOutput(stdoutPipe)
}()
go func() {
defer wg.Done()
getOutput(stderrPipe)
}()
cmd.Run()
wg.Wait()
}
func getOutput(reader io.Reader) {
r := bufio.NewReader(reader)
for true {
line, _, err := r.ReadLine()
if err != nil {
if err == io.EOF {
break
}
fmt.Println(err)
return
}
fmt.Println(string(line))
}
}
# 4. 如何避免 Go 命令行执行产生孤儿进程
当我们使用 Go 程序执行其他程序的时候,如果其他程序也开启了其他进程,那么在 kill 的时候可能会把这些进程变成孤儿进程,一直执行并滞留在内存中。当然,如果我们程序非法退出,或者被 kill 调用,也会导致我们执行的进程变成孤儿进程,那么为了解决这个问题,可以参考一下思路:
- 给要执行的程序创建新的进程组,并调用 syscall.Kill,传递负值 pid 来关闭这个进程组中所有的进程。
参考下面代码:
func main() {
cmd := exec.Command("/bin/bash", "-c", "watch top > top.log")
// 创建新的进程组,进程组 ID 就是当前主进程 ID
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid:true}
// 杀掉该进程组的所有进程,注意这里的 pid 要传负值,才会杀掉该进程组的所有进程
defer syscall.Kill{-cmd.Process.Pid, sysycall.SIGKILL}
...
}
僵尸进程 vs 孤儿进程
概念:
- 孤儿进程:父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程。子进程的资源由 init 进程回收(PPID=1)。
- 僵尸进程:子进程退出了,但是父进程没有用 wait 或 waitpid 去获取子进程的状态信息,那么子进程的进程描述符(包括进程号 PID,退出状态 the termination status of the process,运行时间 the amount of CPU time taken by the process 等)仍然保存在系统中,这种进程称为僵尸进程。
危害:
孤儿进程是没有父进程的进程,它由 init 进程循环的 wait() 回收资源,init 进程充当父进程。因此孤儿进程并没有什么危害。
unix 提供了一种机制保证父进程知道子进程结束时的状态信息。
这种机制是:在每个进程退出的时候,内核会释放所有的资源,包括打开的文件,占用的内存等。但是仍保留一部分信息(进程号 PID,退出状态,运行时间等)。直到父进程通过 wait 或 waitpid 来取时才释放。
但是这样就会产生问题:如果父进程不调用 wait 或 waitpid 的话,那么保留的信息就不会被释放,其进程号就会被一直占用,但是系统所能使用的进程号是有限的,如果大量产生僵死进程,将因没有可用的进程号而导致系统无法产生新的进程,这就是僵尸进程的危害。
解决:僵尸进程
kill 杀死元凶父进程(一般不用)。
父进程用 wait 或者 waitpid 去回收资源(方案不好):
父进程通过 wait 或 waitpid 等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。
信号机制,在处理函数中调用 wait 回收资源(推荐):
通过信号机制,子进程退出时向父进程发送 SIGCHLD 信号,父进程调用 signal(SIGCHLD,sig_child) 去处理 SIGCHLD 信号,在信号处理函数 sig_child() 中调用 wait 进行处理僵尸进程。什么时候得到子进程信号,什么时候进行信号处理,父进程可以继续干其他活,不用去阻塞等待。
← Gin