10.1 创建进程
一般的,应该优先使用 os/exec
包。因为 os/exec
包依赖 os
包中关键创建进程的 API,为了便于理解,我们先探讨 os
包中和进程相关的部分。
在 Unix 中,创建一个进程,通过系统调用 fork
实现(及其一些变种,如 vfork、clone)。在 Go 语言中,Linux 下创建进程使用的系统调用是 clone
。
很多时候,系统调用 fork
、execve
、wait
和 exit
会在一起出现。此处先简要介绍这 4 个系统调用及其典型用法。
- fork:允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。可将此视为把父进程一分为二。
- exit(status):终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核,交其进行再次分配。参数
status
为一整型变量,表示进程的退出状态。父进程可使用系统调用wait()
来获取该状态。 - wait(&status) 目的有二:其一,如果子进程尚未调用
exit()
终止,那么wait
会挂起父进程直至子进程终止;其二,子进程的终止状态通过wait
的status
参数返回。 - execve(pathname, argv, envp) 加载一个新程序(路径名为 pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。
在 Go 语言中,没有直接提供 fork
系统调用的封装,而是将 fork
和 execve
合二为一,提供了 syscall.ForkExec
。如果想只调用 fork
,得自己通过 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
实现。
os.Process
存储了通过 StartProcess
创建的进程的相关信息。
一般通过 StartProcess
创建 Process
的实例,函数声明如下:
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)
它使用提供的程序名、命令行参数、属性开始一个新进程。StartProcess
是一个低级别的接口。os/exec
包提供了高级别的接口,一般应该尽量使用 os/exec
包。如果出错,错误的类型会是 *PathError
。
其中的参数 attr
,类型是 ProcAttr
的指针,用于为 StartProcess
创建新进程提供一些属性。定义如下:
type ProcAttr struct {
// 如果 Dir 非空,子进程会在创建 Process 实例前先进入该目录。(即设为子进程的当前工作目录)
Dir string
// 如果 Env 非空,它会作为新进程的环境变量。必须采用 Environ 返回值的格式。
// 如果 Env 为 nil,将使用 Environ 函数的返回值。
Env []string
// Files 指定被新进程继承的打开文件对象。
// 前三个绑定为标准输入、标准输出、标准错误输出。
// 依赖底层操作系统的实现可能会支持额外的文件对象。
// nil 相当于在进程开始时关闭的文件对象。
Files []*File
// 操作系统特定的创建属性。
// 注意设置本字段意味着你的程序可能会执行异常甚至在某些操作系统中无法通过编译。这时候可以通过为特定系统设置。
// 看 syscall.SysProcAttr 的定义,可以知道用于控制进程的相关属性。
Sys *syscall.SysProcAttr
}
FindProcess
可以通过 pid
查找一个运行中的进程。该函数返回的 Process
对象可以用于获取关于底层操作系统进程的信息。在 Unix 系统中,此函数总是成功,即使 pid
对应的进程不存在。
func FindProcess(pid int) (*Process, error)
Process
提供了四个方法:Kill
、Signal
、Wait
和 Release
。其中 Kill
和 Signal
跟信号相关,而 Kill
实际上就是调用 Signal
,发送了 SIGKILL
信号,强制进程退出,关于信号,后续章节会专门讲解。
Release
方法用于释放 Process
对象相关的资源,以便将来可以被再使用。该方法只有在确定没有调用 Wait
时才需要调用。Unix 中,该方法的内部实现只是将 Process
的 pid
置为 -1。
我们重点看看 Wait
方法。
func (p *Process) Wait() (*ProcessState, error)
在多进程应用程序的设计中,父进程需要知道某个子进程何时改变了状态 —— 子进程终止或因收到信号而停止。Wait
方法就是一种用于监控子进程的技术。
Wait
方法阻塞直到进程退出,然后返回一个 ProcessState
描述进程的状态和可能的错误。Wait
方法会释放绑定到 Process
的所有资源。在大多数操作系统中,Process
必须是当前进程的子进程,否则会返回错误。
看看 ProcessState
的内部结构:
ProcessState
保存了 Wait
函数报告的某个进程的信息。status
记录了状态原因,通过 syscal.WaitStatus
类型定义的方法可以判断:
- Exited():是否正常退出,如调用
os.Exit
; - Signaled():是否收到未处理信号而终止;
- CoreDump():是否收到未处理信号而终止,同时生成 coredump 文件,如 SIGABRT;
- Stopped():是否因信号而停止(SIGSTOP);
- Continued():是否因收到信号 SIGCONT 而恢复;
syscal.WaitStatus
还提供了其他一些方法,比如获取退出状态、信号、停止信号和中断(Trap)原因。
ProcessState
结构内部字段是私有的,我们可以通过它提供的方法来获得一些基本信息,比如:进程是否退出、Pid、进程是否是正常退出、进程 CPU 时间、用户时间等等。
实现类似 Linux 中 time
命令的功能:
package main
import (
"fmt"
"os/exec"
"path/filepath"
"time"
)
func main() {
fmt.Printf("Usage: %s [command]\n", os.Args[0])
os.Exit(1)
}
cmdName := os.Args[1]
if filepath.Base(os.Args[1]) == os.Args[1] {
if lp, err := exec.LookPath(os.Args[1]); err != nil {
fmt.Println("look path error:", err)
os.Exit(1)
} else {
cmdName = lp
}
}
procAttr := &os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
}
cwd, err := os.Getwd()
if err != nil {
fmt.Println("look path error:", err)
os.Exit(1)
}
start := time.Now()
process, err := os.StartProcess(cmdName, []string{cwd}, procAttr)
if err != nil {
fmt.Println("start process error:", err)
os.Exit(2)
}
processState, err := process.Wait()
if err != nil {
fmt.Println("wait error:", err)
os.Exit(3)
}
fmt.Println()
fmt.Println("real", time.Now().Sub(start))
fmt.Println("user", processState.UserTime())
fmt.Println("system", processState.SystemTime())
}
// go build main.go && ./main ls
// Output:
//
// real 4.994739ms
// system 2.279ms
通过 os
包可以做到运行外部命令,如前面的例子。不过,Go 标准库为我们封装了更好用的包: os/exec
,运行外部命令,应该优先使用它,它包装了 os.StartProcess
函数以便更容易的重定向标准输入和输出,使用管道连接 I/O,以及作其它的一些调整。
exec.LookPath
函数在 PATH
指定目录中搜索可执行程序,如 file
中有 /
,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。
func LookPath(file string) (string, error)
如果在 PATH
中没有找到可执行文件,则返回 exec.ErrNotFound
。
Cmd
结构代表一个正在准备或者在执行中的外部命令,调用了 Run
、Output
或 CombinedOutput
后,Cmd
实例不能被重用。
Command
一般的,应该通过 exec.Command
函数产生 Cmd
实例:
func Command(name string, arg ...string) *Cmd
该函数返回一个 *Cmd
,用于使用给出的参数执行 name
指定的程序。返回的 *Cmd
只设定了 Path
和 Args
两个字段。
如果 name
不含路径分隔符,将使用 LookPath
获取完整路径;否则直接使用 name
。参数 arg
不应包含命令名。
得到 *Cmd
实例后,接下来一般有两种写法:
- 调用
Start()
,接着调用Wait()
,然后会阻塞直到命令执行完成; - 调用
Run()
,它内部会先调用Start()
,接着调用Wait()
;
Start
func (c *Cmd) Start() error
开始执行 c
包含的命令,但并不会等待该命令完成即返回。Wait
方法会返回命令的退出状态码并在命令执行完后释放相关的资源。内部调用 os.StartProcess
,执行 forkExec
。
Wait
func (c *Cmd) Wait() error
Wait
会阻塞直到该命令执行完成,该命令必须是先通过 Start
执行。
如果命令成功执行,stdin、stdout、stderr 数据传递没有问题,并且返回状态码为 0,方法的返回值为 nil;如果命令没有执行或者执行失败,会返回 *ExitError
类型的错误;否则返回的 error 可能是表示 I/O 问题。
Wait
方法会在命令返回后释放相关的资源。
Output
除了 是 Start
+Wait
的简便写法,Output()
更是 Run()
的简便写法,外加获取外部命令的输出。
func (c *Cmd) Output() ([]byte, error)
它要求 c.Stdout
必须是 nil
,内部会将 bytes.Buffer
赋值给 c.Stdout
,在 Run()
成功返回后,会将 Buffer
的结果返回(stdout.Bytes()
)。
CombinedOutput
Output()
只返回 Stdout
的结果,而 CombinedOutput
组合 Stdout
和 Stderr
的输出,即 Stdout
和 Stderr
都赋值为同一个 bytes.Buffer
。
StdoutPipe、StderrPipe 和 StdinPipe
除了上面介绍的 Output
和 CombinedOutput
直接获取命令输出结果外,还可以通过 StdoutPipe
返回 io.ReadCloser
来获取输出;相应的 StderrPipe
得到错误信息;而 StdinPipe
则可以往命令写入数据。
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
StdoutPipe
方法返回一个在命令 Start
执行后与命令标准输出关联的管道。Wait
方法会在命令结束后会关闭这个管道,所以一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait
出错了,则必须手动关闭。
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
StderrPipe
方法返回一个在命令 Start
执行后与命令标准错误输出关联的管道。Wait
方法会在命令结束后会关闭这个管道,一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 Wait
出错了,则必须手动关闭。
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
StdinPipe
方法返回一个在命令 Start
执行后与命令标准输入关联的管道。Wait
方法会在命令结束后会关闭这个管道。必要时调用者可以调用 Close
方法来强行关闭管道。例如,标准输入已经关闭了,命令执行才完成,这时调用者需要显示关闭管道。
因为 Wait
之后,会将管道关闭,所以,要使用这些方法,只能使用 Start
+Wait
组合,不能使用 Run
。
前面讲到,通过 Cmd
实例后,有两种方式运行命令。有时候,我们不只是简单的运行命令,还希望能控制命令的输入和输出。通过上面的 API 介绍,控制输入输出有几种方法:
- 得到
Cmd
实例后,直接给它的字段Stdin
、Stdout
和Stderr
赋值; - 通过
Output
或CombinedOutput
获得输出; - 通过带
Pipe
后缀的方法获得管道,用于输入或输出;
直接赋值 Stdin
、Stdout
和 Stderr
func FillStd(name string, arg ...string) ([]byte, error) {
cmd := exec.Command(name, arg...)
var out = new(bytes.Buffer)
cmd.Stdout = out
cmd.Stderr = out
err := cmd.Run()
if err != nil {
return nil, err
}
return out.Bytes(), nil
}
使用 Output
使用 Pipe
func UsePipe(name string, arg ...string) ([]byte, error) {
cmd := exec.Command(name, arg...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}
var out = make([]byte, 0, 1024)
for {
tmp := make([]byte, 128)
n, err := stdout.Read(tmp)
out = append(out, tmp[:n]...)
if err != nil {
break
}
}
if err = cmd.Wait(); err != nil {
return nil, err
}
return out, nil
}
完整代码见 os_exec。
os.Exit()
函数会终止当前进程,对应的系统调用不是 _exit
,而是 exit_group
。
func Exit(code int)
导航
- 下一节:进程属性和控制