第12章 进程
# 第12章 进程
本章的方法将展示如何运行外部程序、如何与其交互以及如何优雅地终止进程。在处理外部进程时,有几个关键点需要牢记:
- 启动外部进程时,它会与你的程序并发运行。
- 如果你需要与子进程通信,就必须使用进程间通信机制,比如管道。
- 运行子进程时,它的标准输入和标准输出流对于父进程而言,就像是独立的并发流。你不能依赖从这些流中接收数据的顺序。
本节涵盖以下主要方法:
- 运行外部程序
- 向进程传递参数
- 使用管道处理子进程的输出
- 向子进程提供输入
- 修改子进程的环境变量
- 使用信号优雅地终止进程
# 运行外部程序
在许多场景下,你可能希望执行外部程序来完成某项任务。通常,这是因为在自己的程序中执行相同任务要么不可行,要么难度较大。例如,你可能会选择执行多个外部图像处理程序实例来修改一组图像。另一种场景是,你希望使用设备制造商提供的程序来配置某些设备。本方法将介绍几种执行外部程序的方式。
# 如何实现……
使用exec.Command
或exec.CommandContext
从你的程序中运行另一个程序。如果你不需要取消(终止)子进程或设置超时,exec.Command
就很适用。否则,使用exec.CommandContext
,并通过取消或让上下文超时来终止子进程:
- 使用程序名称及其参数创建
exec.Command
(或exec.CommandContext
)对象:- 如果你需要在平台的可执行命令路径中搜索程序,不要包含任何路径分隔符。
- 如果你在程序名称中使用路径分隔符,它必须是相对于
exec.Command.Dir
的路径;如果exec.Command.Dir
为空,它必须是相对于当前工作目录的路径。 - 如果你知道可执行文件的位置,就使用绝对路径。
- 准备输入和输出流,以便捕获程序输出,或者通过标准输入流发送输入。
- 启动程序。
- 等待程序结束。
以下示例使用sub/
目录下的go
命令构建一个Go程序:
// 运行 "go build" 构建 "sub" 目录下的子进程
func buildProgram() {
// 使用可执行文件及其参数创建一个Command
cmd := exec.Command(
"go", "build", "-o", "subprocess", ".")
// 设置工作目录
cmd.Dir = "sub"
// 收集进程的标准输出和标准错误作为合并输出
// 这将运行进程,并等待它结束
output, err := cmd.CombinedOutput()
if err != nil {
panic(err)
}
// 如果构建成功,构建命令不会输出任何内容。所以如果有任何输出,就表示失败。
if len(output) > 0 {
panic(string(output))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上述示例会将进程输出收集为一个合并字符串。程序的标准输出和标准错误将作为单个字符串返回,所以你无法确定输出字符串的哪些部分来自标准输出,哪些部分来自标准错误。确保你能够正确解析输出。
警告 进程的标准输出和标准错误流是独立的并发流。一般来说,没有可移植的方法来确定哪个流先产生输出。这可能会产生严重影响。例如,假设你执行了一个程序,它在标准输出上生成一系列行,但每当检测到错误时,它会在标准错误中打印一条类似 “最后打印的行有问题” 的消息。但是当你在程序中读取错误时,最后打印的行可能还没有到达你的程序。 |
---|
以下程序展示了exec.CommandContext
和管道的使用:
// 运行由buildProgram函数构建的程序10毫秒,并发地从输出和错误管道读取数据
func runSubProcessStreamingOutputs() {
// 创建一个带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
// 创建一个将在10毫秒后超时的命令
cmd := exec.CommandContext(ctx, "sub/subprocess")
// 为输出和错误流设置管道
stdout, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
panic(err)
}
// 从单独的goroutine中读取标准错误
go func() {
io.Copy(os.Stderr, stderr)
}()
// 开始运行程序
err = cmd.Start()
if err != nil {
panic(err)
}
// 将子进程的标准输出复制到我们的标准输出
io.Copy(os.Stdout, stdout)
// 等待程序结束
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
上述示例接入了子进程的标准输出和标准错误输出。注意,程序在启动前就开始从标准错误流读取数据。该goroutine将阻塞,直到子进程输出错误或子进程终止,此时标准错误管道将关闭,goroutine也将终止。从标准输出读取数据的部分在主goroutine中运行,在cmd.Wait
之前。这个顺序很重要。如果子进程开始在标准输出上产生输出,但父程序没有监听,子进程将阻塞。此时调用cmd.Wait
会造成死锁,但运行时无法检测到这种情况,因为父程序依赖于子进程的行为。你可以将子进程的标准输出和标准错误分配到同一个流,如下所示:
// 运行构建的子进程10毫秒,合并输出
func runSubProcessCombinedOutput() {
// 创建一个带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
// 使用上下文定义命令
cmd := exec.CommandContext(ctx, "sub/subprocess")
// 将标准输出和标准错误都分配到同一个流。这相当于调用CombinedOutput
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
// 启动进程
err := cmd.Start()
if err != nil {
panic(err)
}
// 等待进程结束。输出将打印到我们的标准输出
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上述方法类似于使用CombinedOutput
运行子进程。将cmd.Stdout
和cmd.Stderr
分配到同一个流,与合并子进程的两个输出具有相同的效果。
# 向进程传递参数
向子进程传递参数的机制可能会让人感到困惑。Shell环境会解析和展开进程参数。例如,*.txt
参数会被替换为与该模式匹配的文件名列表,并且这些文件名中的每一个都会成为一个单独的参数。本方法将介绍如何正确地向子进程传递此类参数。
向子进程传递参数有两种选择。
# 展开参数
第一种选择是手动执行Shell参数处理。
# 如何实现……
要手动执行Shell处理,请遵循以下步骤:
- 从参数中删除特定于Shell的引号,例如Shell命令:
./prog "test directory"
这个Shell命令变为cmd:=exec.Command("./prog","test directory")
。./prog dir1 "long dir name" '"quoted name"'
这个Bash命令变为cmd:=exec.Command("./prog", "long dir name", "'\"quoted name\"'")
。注意Bash对引号的特殊处理。
- 展开模式。
./prog *.txt
变为cmd:=exec.Command("./prog",listFiles("*.txt")...)
,其中listFiles
是一个返回文件名切片的函数。 | 提示
传递用空格分隔的文件列表会将它们作为单个参数传递。也就是说,cmd:=exec.Command("./prog","file1.txt file2.txt")
会将file1.txt file2.txt
作为单个参数传递给进程。 | | ------------------------------------------------------------ | - 替换环境变量。
./prog $HOME
变为cmd:=exec.Command("./prog", os.Getenv("HOME"))
。运行cmd:=exec.Command("./prog", "$HOME")
会将字符串$HOME
传递给程序,而不是传递环境中的实际值。 - 最后,你必须手动处理管道。也就是说,对于
./prog >output.txt
这个Shell命令,你必须运行cmd:=exec.Command("./prog")
,创建一个output.txt
文件,并设置cmd.Stdout=outputFile
。
# 通过Shell运行命令
第二种选择是通过Shell运行程序。
# 如何实现……
使用特定于平台的Shell及其语法来运行命令:
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/C", "echo test>test.txt")
case "darwin": // Mac OS
cmd = exec.Command("/bin/sh", "-c", "echo test>test.txt")
case "linux": // Linux系统,假设存在bash
cmd = exec.Command("/bin/bash", "-c", "echo test>test.txt")
default: // 其他操作系统。假设它有`sh`
cmd = exec.Command("/bin/sh", "-c", "echo test>test.txt")
}
out, err := cmd.Output()
2
3
4
5
6
7
8
9
10
11
12
13
上述示例为Windows平台选择cmd
,为Darwin(Mac)选择/bin/sh
,为Linux选择/bin/bash
,为其他系统选择/bin/sh
。传递给Shell的命令包含重定向,由Shell进行处理。命令的输出将写入test.txt
。
# 使用管道处理子进程的输出
要记住,进程的标准输出和标准错误流是并发的流。如果子进程生成的输出可能是无边界的,你可以在一个单独的goroutine中处理它。本方法展示具体做法。
# 如何操作...
先介绍一下管道(pipe)。管道是Go语言通道的基于流的类似物。它是一种先进先出(FIFO)的通信机制,有两端:一个写入端和一个读取端。读取端会阻塞,直到写入端写入内容;写入端会阻塞,直到读取端读取数据。当你使用完管道后,关闭写入端,这会同时关闭管道的读取端。当子进程终止时就会发生这种情况。如果你关闭了管道的读取端,然后再向其写入数据,程序将收到一个信号,甚至可能终止。如果父程序在子程序之前终止,就会出现这种情况。
- 创建命令,并获取其标准输出管道:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "sub/subprocess")
pipe, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
2
3
4
5
6
7
8
- 创建一个新的goroutine,并从子进程的标准输出读取数据。在这个goroutine中处理子进程的输出:
// 在单独的goroutine中从管道读取数据
go func() {
// 过滤包含"0"的行
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "0") {
fmt.Printf("Filtered line: %s\n", line)
}
}
if err := scanner.Err(); err != nil {
fmt.Println("Scanner error: %v", err)
}
}()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 启动进程:
err = cmd.Start()
if err != nil {
panic(err)
}
2
3
4
- 等待进程结束:
err = cmd.Wait()
if err != nil {
fmt.Println(err)
}
2
3
4
为子进程提供输入
有两种方法可以为子进程提供输入:将cmd.Stdin
设置为一个流,或者使用cmd.StdinPipe
获取一个写入器,将输入发送给子进程。
如何操作...
- 创建命令:
// 运行grep命令并搜索一个单词
cmd := exec.Command("grep", word)
2
- 通过设置标准输入流为进程提供输入:
// 打开一个文件
input, err := os.Open("input.txt")
if err != nil {
panic(err)
}
cmd.Stdin = input
2
3
4
5
6
7
- 运行程序并等待其结束:
if err = cmd.Start(); err != nil {
panic(err)
}
if err = cmd.Wait(); err != nil {
panic(err)
}
2
3
4
5
6
7
或者,你可以使用管道提供流式输入。 4. 创建命令:
// 运行grep命令并搜索一个单词
cmd := exec.Command("grep", word)
2
- 获取输入管道:
input, err:=cmd.StdinPipe()
if err != nil {
panic(err)
}
2
3
4
- 通过管道将输入发送给程序。完成后,关闭管道:
go func() {
// 延迟关闭管道
defer input.Close()
// 打开一个文件
file, err := os.Open("input.txt")
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(input,file)
}()
2
3
4
5
6
7
8
9
10
11
12
13
- 运行程序并等待其结束:
if err = cmd.Start(); err != nil {
panic(err)
}
if err = cmd.Wait(); err != nil {
panic(err)
}
2
3
4
5
6
7
# 修改子进程的环境变量
环境变量(Environment variables)是与进程相关联的键值对。它们对于传递特定于环境的信息很有用,例如当前用户的主目录、可执行文件的搜索路径、配置选项等等。在容器化部署中,环境变量是传递程序所需凭证的便捷方式。
进程的环境变量由其父进程提供,但一旦进程启动,这些提供的环境变量的副本就会分配给子进程。因此,父进程在子进程开始运行后,无法更改子进程的环境变量。
# 如何操作...
- 要在启动子进程时使用与当前进程相同的环境变量,将
Command.Env
设置为nil
。这会将当前进程的环境变量复制到子进程。 - 要使用额外的环境变量启动子进程,可以将这些新变量附加到当前进程的变量中:
// 运行服务器
cmd:=exec.Command("./server")
// 复制当前进程的环境变量
cmd.Env=os.Environ()
// 附加新的环境变量
// 将认证密钥设置为当前进程的环境变量
cmd.Env=append(cmd.Env,fmt.Sprintf("AUTH_KEY=%s", authkey))
// 启动服务器进程。父进程的环境被复制到子进程
cmd.Start()
2
3
4
5
6
7
8
9
# 使用信号实现优雅终止
要优雅地终止一个程序,你应该做到以下几点:
- 不再接受新的请求
- 完成所有已接受但未完成的请求
- 给任何长时间运行的进程一定时间来完成,如果在给定时间内无法完成,则终止它们
优雅终止在基于云的服务开发中尤为重要,因为大多数云服务是临时的,经常会被新实例替换。本方法展示如何实现优雅终止。
# 如何操作...
- 处理中断和终止信号。中断信号(SIGINT)通常由用户发起(例如,通过按下Ctrl + C),终止信号(SIGTERM)通常由主机操作系统发起,对于容器化环境,则由容器编排系统发起。
- 停止接受任何新请求。
- 等待现有请求在超时时间内完成。
- 终止进程。
下面展示一个示例。这是一个简单的HTTP回显服务器。程序启动时,会创建一个goroutine,监听响应SIGINT和SIGTERM信号的通道。当接收到其中任何一个信号时,它会关闭服务器(首先停止接受新请求,然后等待现有请求在超时时间内完成),进而终止程序:
func main() {
// 创建一个简单的HTTP回显服务
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body)
})
server := &http.Server{Addr: ":8080"}
// 监听SIGINT和SIGTERM信号
// 使用信号终止服务器
sigTerm := make(chan os.Signal, 1)
signal.Notify(sigTerm, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigTerm
// 服务器关闭的超时时间为5秒
ctx, cancel := context.WithTimeout(context.Background(), 5*time.
Second)
defer cancel()
server.Shutdown(ctx)
}()
// 启动服务器。当服务器关闭时,程序将结束
server.ListenAndServe()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24