os 包及其子包 os/exec 提供了创建进程的方法。
os
os/exec
一般的,应该优先使用 os/exec 包。因为 os/exec 包依赖 os 包中关键创建进程的 API,为了便于理解,我们先探讨 os 包中和进程相关的部分。
在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。在 Go 语言中,Linux 下创建进程使用的系统调用是 clone。
fork
clone
很多时候,系统调用 fork、execve、wait 和 exit 会在一起出现。此处先简要介绍这 4 个系统调用及其典型用法。
execve
wait
exit
status
wait()
exit()
在 Go 语言中,没有直接提供 fork 系统调用的封装,而是将 fork 和 execve 合二为一,提供了 syscall.ForkExec。如果想只调用 fork,得自己通过 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 实现。
syscall.ForkExec
syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
os.Process 存储了通过 StartProcess 创建的进程的相关信息。
os.Process
StartProcess
type Process struct { Pid int handle uintptr // handle is accessed atomically on Windows isdone uint32 // process has been successfully waited on, non zero if true }
一般通过 StartProcess 创建 Process 的实例,函数声明如下:
Process
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)
它使用提供的程序名、命令行参数、属性开始一个新进程。StartProcess 是一个低级别的接口。os/exec 包提供了高级别的接口,一般应该尽量使用 os/exec 包。如果出错,错误的类型会是 *PathError。
*PathError
其中的参数 attr,类型是 ProcAttr 的指针,用于为 StartProcess 创建新进程提供一些属性。定义如下:
attr
ProcAttr
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 对应的进程不存在。
FindProcess
pid
func FindProcess(pid int) (*Process, error)
Process 提供了四个方法:Kill、Signal、Wait 和 Release。其中 Kill 和 Signal 跟信号相关,而 Kill 实际上就是调用 Signal,发送了 SIGKILL 信号,强制进程退出,关于信号,后续章节会专门讲解。
Kill
Signal
Wait
Release
SIGKILL
Release 方法用于释放 Process 对象相关的资源,以便将来可以被再使用。该方法只有在确定没有调用 Wait 时才需要调用。Unix 中,该方法的内部实现只是将 Process 的 pid 置为 -1。
我们重点看看 Wait 方法。
func (p *Process) Wait() (*ProcessState, error)
在多进程应用程序的设计中,父进程需要知道某个子进程何时改变了状态 —— 子进程终止或因收到信号而停止。Wait 方法就是一种用于监控子进程的技术。
Wait 方法阻塞直到进程退出,然后返回一个 ProcessState 描述进程的状态和可能的错误。Wait 方法会释放绑定到 Process 的所有资源。在大多数操作系统中,Process 必须是当前进程的子进程,否则会返回错误。
ProcessState
看看 ProcessState 的内部结构:
type ProcessState struct { pid int // The process's id. status syscall.WaitStatus // System-dependent status info. rusage *syscall.Rusage }
ProcessState 保存了 Wait 函数报告的某个进程的信息。status 记录了状态原因,通过 syscal.WaitStatus 类型定义的方法可以判断:
syscal.WaitStatus
os.Exit
syscal.WaitStatus 还提供了其他一些方法,比如获取退出状态、信号、停止信号和中断(Trap)原因。
因为 Linux 下 Wait 的内部实现使用的是 wait4 系统调用,因此,ProcessState 中包含了 rusage,用于统计进程的各类资源信息。一般情况下,syscall.Rusage 中定义的信息都用不到,如果实际中需要使用,可以查阅 Linux 系统调用 getrusage 获得相关说明 (getrusage(2))。
wait4
rusage
syscall.Rusage
getrusage
getrusage(2)
ProcessState 结构内部字段是私有的,我们可以通过它提供的方法来获得一些基本信息,比如:进程是否退出、Pid、进程是否是正常退出、进程 CPU 时间、用户时间等等。
实现类似 Linux 中 time 命令的功能:
time
package main import ( "fmt" "os" "os/exec" "path/filepath" "time" ) func main() { if len(os.Args) < 2 { 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 // user 1.177ms // system 2.279ms
通过 os 包可以做到运行外部命令,如前面的例子。不过,Go 标准库为我们封装了更好用的包: os/exec,运行外部命令,应该优先使用它,它包装了 os.StartProcess 函数以便更容易的重定向标准输入和输出,使用管道连接 I/O,以及作其它的一些调整。
os.StartProcess
exec.LookPath 函数在 PATH 指定目录中搜索可执行程序,如 file 中有 /,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。
exec.LookPath
PATH
file
/
func LookPath(file string) (string, error)
如果在 PATH 中没有找到可执行文件,则返回 exec.ErrNotFound。
exec.ErrNotFound
Cmd 结构代表一个正在准备或者在执行中的外部命令,调用了 Run、Output 或 CombinedOutput 后,Cmd 实例不能被重用。
Cmd
Run
Output
CombinedOutput
type Cmd struct { // Path 是将要执行的命令路径。 // 该字段不能为空(也是唯一一个不能为空的字段),如为相对路径会相对于 Dir 字段。 // 通过 Command 初始化时,会在需要时调用 LookPath 获得完整的路径。 Path string // Args 存放着命令的参数,第一个值是要执行的命令(Args[0]);如果为空切片或者 nil,使用 {Path} 运行。 // 一般情况下,Path 和 Args 都应被 Command 函数设定。 Args []string // Env 指定进程的环境变量,如为 nil,则使用当前进程的环境变量,即 os.Environ(),一般就是当前系统的环境变量。 Env []string // Dir 指定命令的工作目录。如为空字符串,会在调用者的进程当前工作目录下执行。 Dir string // Stdin 指定进程的标准输入,如为 nil,进程会从空设备读取(os.DevNull) // 如果 Stdin 是 *os.File 的实例,进程的标准输入会直接指向这个文件 // 否则,会在一个单独的 goroutine 中从 Stdin 中读数据,然后将数据通过管道传递到该命令中(也就是从 Stdin 读到数据后,写入管道,该命令可以从管道读到这个数据)。在 goroutine 停止数据拷贝之前(停止的原因如遇到 EOF 或其他错误,或管道的 write 端错误),Wait 方法会一直堵塞。 Stdin io.Reader // Stdout 和 Stderr 指定进程的标准输出和标准错误输出。 // 如果任一个为 nil,Run 方法会将对应的文件描述符关联到空设备(os.DevNull) // 如果两个字段相同,同一时间最多有一个线程可以写入。 Stdout io.Writer Stderr io.Writer // ExtraFiles 指定额外被新进程继承的已打开文件,不包括标准输入、标准输出、标准错误输出。 // 如果本字段非 nil,其中的元素 i 会变成文件描述符 3+i。 // // BUG: 在 OS X 10.6 系统中,子进程可能会继承不期望的文件描述符。 // http://golang.org/issue/2603 ExtraFiles []*os.File // SysProcAttr 提供可选的、各操作系统特定的 sys 属性。 // Run 方法会将它作为 os.ProcAttr 的 Sys 字段传递给 os.StartProcess 函数。 SysProcAttr *syscall.SysProcAttr // Process 是底层的,只执行一次的进程。 Process *os.Process // ProcessState 包含一个已经存在的进程的信息,只有在调用 Wait 或 Run 后才可用。 ProcessState *os.ProcessState }
Command
一般的,应该通过 exec.Command 函数产生 Cmd 实例:
exec.Command
func Command(name string, arg ...string) *Cmd
该函数返回一个 *Cmd,用于使用给出的参数执行 name 指定的程序。返回的 *Cmd 只设定了 Path 和 Args 两个字段。
*Cmd
name
Path
Args
如果 name 不含路径分隔符,将使用 LookPath 获取完整路径;否则直接使用 name。参数 arg 不应包含命令名。
LookPath
arg
得到 *Cmd 实例后,接下来一般有两种写法:
Start()
Wait()
Run()
Start
func (c *Cmd) Start() error
开始执行 c 包含的命令,但并不会等待该命令完成即返回。Wait 方法会返回命令的退出状态码并在命令执行完后释放相关的资源。内部调用 os.StartProcess,执行 forkExec。
c
forkExec
func (c *Cmd) Wait() error
Wait 会阻塞直到该命令执行完成,该命令必须是先通过 Start 执行。
如果命令成功执行,stdin、stdout、stderr 数据传递没有问题,并且返回状态码为 0,方法的返回值为 nil;如果命令没有执行或者执行失败,会返回 *ExitError 类型的错误;否则返回的 error 可能是表示 I/O 问题。
*ExitError
如果 c.Stdin 不是 *os.File 类型,Wait 会等待,直到数据从 c.Stdin 拷贝到进程的标准输入。
c.Stdin
*os.File
Wait 方法会在命令返回后释放相关的资源。
除了 Run() 是 Start+Wait 的简便写法,Output() 更是 Run() 的简便写法,外加获取外部命令的输出。
Output()
func (c *Cmd) Output() ([]byte, error)
它要求 c.Stdout 必须是 nil,内部会将 bytes.Buffer 赋值给 c.Stdout,在 Run() 成功返回后,会将 Buffer 的结果返回(stdout.Bytes())。
c.Stdout
nil
bytes.Buffer
Buffer
stdout.Bytes()
Output() 只返回 Stdout 的结果,而 CombinedOutput 组合 Stdout 和 Stderr 的输出,即 Stdout 和 Stderr 都赋值为同一个 bytes.Buffer。
Stdout
Stderr
StdoutPipe、StderrPipe 和 StdinPipe
除了上面介绍的 Output 和 CombinedOutput 直接获取命令输出结果外,还可以通过 StdoutPipe 返回 io.ReadCloser 来获取输出;相应的 StderrPipe 得到错误信息;而 StdinPipe 则可以往命令写入数据。
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 方法来强行关闭管道。例如,标准输入已经关闭了,命令执行才完成,这时调用者需要显示关闭管道。
Close
因为 Wait 之后,会将管道关闭,所以,要使用这些方法,只能使用 Start+Wait 组合,不能使用 Run。
前面讲到,通过 Cmd 实例后,有两种方式运行命令。有时候,我们不只是简单的运行命令,还希望能控制命令的输入和输出。通过上面的 API 介绍,控制输入输出有几种方法:
Stdin
Pipe
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 }
func UseOutput(name string, arg ...string) ([]byte, error) { return exec.Command(name, arg...).Output() }
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。
os.Exit()
_exit
exit_group
func Exit(code int)
Exit 让当前进程以给出的状态码 code 退出。一般来说,状态码 0 表示成功,非 0 表示出错。进程会立刻终止,defer 的函数不会被执行。
Exit
code
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8