前言

一般来说,进程的操作使用的是一些系统的命令,所以go内部使用os包,进行一些运行系统命令的操作

os 包及其子包 os/exec 提供了创建进程的方法。

一般的,应该优先使用 os/exec 包。因为 os/exec 包依赖 os 包中关键创建进程的 API,为了便于理解,我们先探讨 os 包中和进程相关的部分。

1.1. 在系统下进程的创建及基本方法介绍

Unix :fork创建一个进程,(及其一些变种,如 vfork、clone)。
Go:Linux 下创建进程使用的系统调用是 clone。

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) 实现。

1.1.1. os包的进程Process 对象及其相关方法

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 的实例,函数声明如下:

func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)

它使用提供的程序名、命令行参数、属性开始一个新进程。StartProcess 是一个低级别的接口。os/exec 包提供了高级别的接口,一般应该尽量使用 os/exec 包。如果出错,错误的类型会是 *PathError。

属性定义如下:

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。

1.2. 运行外部命令

通过 os 包可以做到运行外部命令,如前面的例子。不过,Go 标准库为我们封装了更好用的包: os/exec,运行外部命令,应该优先使用它,它包装了 os.StartProcess 函数以便更容易的重定向标准输入和输出,使用管道连接 I/O,以及作其它的一些调整。

1.2.1. 查找可执行程序

exec.LookPath 函数在 PATH 指定目录中搜索可执行程序,如 file 中有 /,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。

func LookPath(file string) (string, error)

如果在 PATH 中没有找到可执行文件,则返回 exec.ErrNotFound。

1.2.2. Cmd 及其相关方法(执行命令的对象)

Cmd 结构代表一个正在准备或者在执行中的外部命令,调用了 Run、Output 或 CombinedOutput 后,Cmd 实例不能被重用。

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 实例:

func Command(name string, arg ...string) *Cmd

用法
得到 *Cmd 实例后,接下来一般有两种写法:

  1. 调用 Start(),接着调用 Wait(),然后会阻塞直到命令执行完成;
  2. 调用 Run(),它内部会先调用 Start(),接着调用 Wait()
    细节化使用过程请自行探究

1.2.3. 执行外部命令示例

前面讲到,通过 Cmd 实例后,有两种方式运行命令。有时候,我们不只是简单的运行命令,还希望能控制命令的输入和输出。通过上面的 API 介绍,控制输入输出有几种方法:

  • 得到 Cmd 实例后,直接给它的字段 StdinStdoutStderr 赋值;
  • 通过 OutputCombinedOutput 获得输出;
  • 通过带 Pipe 后缀的方法获得管道,用于输入或输出;

直接赋值 StdinStdoutStderr(基本使用)

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
}
有了上述一些基础后,来了解一下服务的无缝升级,其实就是父进程fork一个子进程后,再关闭父进程的一个过程。