进程 线程 goroutine


Go 语言 进程 线程和 goroutine  涉及操作系统
Go 语言提供相关标准库 API
goroutine 为 Go 核心特性
标准库中能操作进程 线程和 goroutine 的API 是深入学习 理解 Go 语言必须掌握的知识

创建进程

os 包及其子包 os/exec 提供创建进程的方法
优先使用 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) 实现

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
其中的参数 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 的内部结构
type ProcessState struct {
    pid    int                // The process's id.
   status syscall.WaitStatus // System-dependent status info.
   rusage *syscall.Rusage
}
ProcessState 保存了 Wait 函数报告的某个进程的信息 status 记录了状态原因 通过 syscal.WaitStatus 类型定义的方法可以判断
Exited() 是否正常退出 如调用 os.Exit
Signaled() 是否收到未处理信号而终止
CoreDump() 是否收到未处理信号而终止 同时生成 coredump 文件 如 SIGABRT
Stopped() 是否因信号而停止(SIGSTOP)
Continued() 是否因收到信号 SIGCONT 而恢复
syscal.WaitStatus 还提供了其他一些方法 比如获取退出状态 信号 停止信号和中断(Trap)原因
因为 Linux 下 Wait 的内部实现使用的是 wait4 系统调用 因此 ProcessState 中包含了 rusage 用于统计进程的各类资源信息
一般情况下 syscall.Rusage 中定义的信息都用不到 如果实际中需要使用 可以查阅 Linux 系统调用 getrusage 获得相关说明(getrusage(2))
ProcessState 结构内部字段是私有的
可以通过它提供的方法来获得一些基本信息
如 进程是否退出 Pid 进程是否是正常退出 进程CPU时间 用户时间等
实现类似 Linux 中 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 以及作其它的一些调整

查找可执行程序

exec.LookPath 函数在 PATH 指定目录中搜索可执行程序
如 file 中有 / 则只在当前目录搜索 该函数返回完整路径或相对于当前路径的一个相对路径
func LookPath(file string) (string, error)
如果在 PATH 中没有找到可执行文件 则返回 exec.ErrNotFound

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 用于使用给出的参数执行 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 问题
如果 c.Stdin 不是 *os.File 类型 Wait 会等待 直到数据从 c.Stdin 拷贝到进程的标准输入
Wait 方法会在命令返回后释放相关的资源
Output
除了 Run() 是 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
func UseOutput(name string, arg ...string) ([]byte, error) {
    return exec.Command(name, arg...).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.Exit() 函数会终止当前进程 对应的系统调用不是 _exit 而是 exit_group
func Exit(code int)
Exit 让当前进程以给出的状态码 code 退出
一般来说 状态码 0 表示成功 非 0 表示出错 进程会立刻终止 defer 的函数不会被执行
进程 线程 goroutine

Go 语言 进程线程和threads相关