CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
  • 第2章 字符串处理
  • 第3章 处理日期和时间
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
  • 第12章 进程
    • 第13章 网络编程
    • 第14章 流式输入/输出
    • 第15章 数据库
    • 第16章 日志记录
    • 第17章 测试、基准测试和性能分析
    目录

    第12章 进程

    # 第12章 进程

    本章的方法将展示如何运行外部程序、如何与其交互以及如何优雅地终止进程。在处理外部进程时,有几个关键点需要牢记:

    • 启动外部进程时,它会与你的程序并发运行。
    • 如果你需要与子进程通信,就必须使用进程间通信机制,比如管道。
    • 运行子进程时,它的标准输入和标准输出流对于父进程而言,就像是独立的并发流。你不能依赖从这些流中接收数据的顺序。

    本节涵盖以下主要方法:

    • 运行外部程序
    • 向进程传递参数
    • 使用管道处理子进程的输出
    • 向子进程提供输入
    • 修改子进程的环境变量
    • 使用信号优雅地终止进程

    # 运行外部程序

    在许多场景下,你可能希望执行外部程序来完成某项任务。通常,这是因为在自己的程序中执行相同任务要么不可行,要么难度较大。例如,你可能会选择执行多个外部图像处理程序实例来修改一组图像。另一种场景是,你希望使用设备制造商提供的程序来配置某些设备。本方法将介绍几种执行外部程序的方式。

    # 如何实现……

    使用exec.Command或exec.CommandContext从你的程序中运行另一个程序。如果你不需要取消(终止)子进程或设置超时,exec.Command就很适用。否则,使用exec.CommandContext,并通过取消或让上下文超时来终止子进程:

    1. 使用程序名称及其参数创建exec.Command(或exec.CommandContext)对象:
      • 如果你需要在平台的可执行命令路径中搜索程序,不要包含任何路径分隔符。
      • 如果你在程序名称中使用路径分隔符,它必须是相对于exec.Command.Dir的路径;如果exec.Command.Dir为空,它必须是相对于当前工作目录的路径。
      • 如果你知道可执行文件的位置,就使用绝对路径。
    2. 准备输入和输出流,以便捕获程序输出,或者通过标准输入流发送输入。
    3. 启动程序。
    4. 等待程序结束。

    以下示例使用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))
        }
    }
    
    1
    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)
        }
    }
    
    1
    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)
        }
    }
    
    1
    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处理,请遵循以下步骤:

    1. 从参数中删除特定于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对引号的特殊处理。
    2. 展开模式。./prog *.txt变为cmd:=exec.Command("./prog",listFiles("*.txt")...),其中listFiles是一个返回文件名切片的函数。 | 提示
      传递用空格分隔的文件列表会将它们作为单个参数传递。也就是说,cmd:=exec.Command("./prog","file1.txt file2.txt")会将file1.txt file2.txt作为单个参数传递给进程。 | | ------------------------------------------------------------ |
    3. 替换环境变量。./prog $HOME变为cmd:=exec.Command("./prog", os.Getenv("HOME"))。运行cmd:=exec.Command("./prog", "$HOME")会将字符串$HOME传递给程序,而不是传递环境中的实际值。
    4. 最后,你必须手动处理管道。也就是说,对于./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()
    
    1
    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)的通信机制,有两端:一个写入端和一个读取端。读取端会阻塞,直到写入端写入内容;写入端会阻塞,直到读取端读取数据。当你使用完管道后,关闭写入端,这会同时关闭管道的读取端。当子进程终止时就会发生这种情况。如果你关闭了管道的读取端,然后再向其写入数据,程序将收到一个信号,甚至可能终止。如果父程序在子程序之前终止,就会出现这种情况。

    1. 创建命令,并获取其标准输出管道:
    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)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    1. 创建一个新的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)
        }
    }()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    1. 启动进程:
    err = cmd.Start()
    if err != nil {
    	panic(err)
    }
    
    1
    2
    3
    4
    1. 等待进程结束:
    err = cmd.Wait()
    if err != nil {
    	fmt.Println(err)
    }
    
    1
    2
    3
    4

    为子进程提供输入 有两种方法可以为子进程提供输入:将cmd.Stdin设置为一个流,或者使用cmd.StdinPipe获取一个写入器,将输入发送给子进程。 如何操作...

    1. 创建命令:
    // 运行grep命令并搜索一个单词
    cmd := exec.Command("grep", word)
    
    1
    2
    1. 通过设置标准输入流为进程提供输入:
    // 打开一个文件
    input, err := os.Open("input.txt")
    if err != nil {
    	panic(err)
    }
    
    cmd.Stdin = input
    
    1
    2
    3
    4
    5
    6
    7
    1. 运行程序并等待其结束:
    if err = cmd.Start(); err != nil {
    	panic(err)
    }
    
    if err = cmd.Wait(); err != nil {
    	panic(err)
    }
    
    1
    2
    3
    4
    5
    6
    7

    或者,你可以使用管道提供流式输入。 4. 创建命令:

    // 运行grep命令并搜索一个单词
    cmd := exec.Command("grep", word)
    
    1
    2
    1. 获取输入管道:
    input, err:=cmd.StdinPipe()
    if err != nil {
    	panic(err)
    }
    
    1
    2
    3
    4
    1. 通过管道将输入发送给程序。完成后,关闭管道:
    go func() {
        // 延迟关闭管道
        defer input.Close()
        // 打开一个文件
        file, err := os.Open("input.txt")
        if err != nil {
        	panic(err)
        }
        
        defer file.Close()
        
        io.Copy(input,file)
    }()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    1. 运行程序并等待其结束:
    if err = cmd.Start(); err != nil {
    	panic(err)
    }
    
    if err = cmd.Wait(); err != nil {
    	panic(err)
    }
    
    1
    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()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 使用信号实现优雅终止

    要优雅地终止一个程序,你应该做到以下几点:

    • 不再接受新的请求
    • 完成所有已接受但未完成的请求
    • 给任何长时间运行的进程一定时间来完成,如果在给定时间内无法完成,则终止它们

    优雅终止在基于云的服务开发中尤为重要,因为大多数云服务是临时的,经常会被新实例替换。本方法展示如何实现优雅终止。

    # 如何操作...

    1. 处理中断和终止信号。中断信号(SIGINT)通常由用户发起(例如,通过按下Ctrl + C),终止信号(SIGTERM)通常由主机操作系统发起,对于容器化环境,则由容器编排系统发起。
    2. 停止接受任何新请求。
    3. 等待现有请求在超时时间内完成。
    4. 终止进程。

    下面展示一个示例。这是一个简单的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()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    第11章 处理JSON数据
    第13章 网络编程

    ← 第11章 处理JSON数据 第13章 网络编程→

    最近更新
    01
    第二章 关键字static及其不同用法
    03-27
    02
    第一章 auto与类型推导
    03-27
    03
    第四章 Lambda函数
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式