Golang中如何判断通过`cmd.Start`启动的进程是否已完成

Golang中如何判断通过cmd.Start启动的进程是否已完成 问题: 我使用 cmd.Start 启动了一个系统命令。这是一个长时间运行的进程,我通过检查其输出来监控进度。如何检查该进程是否已完成/结束?

我没有调用 cmd.Wait,因为我不想等待进程结束(Wait 会阻塞直到进程完成,而我不希望有任何阻塞)。

我曾尝试向进程发送信号 0,但即使在进程的所有输出都完成后,它仍然返回相同的值。

6 回复

你好,我只是猜测一下,既然你不喜欢阻塞,为什么不把 cmd.Wait 放在另一个 goroutine 里执行呢?

我猜可以这样:先创建一个负责等待的 goroutine,然后监控进程,接着在一个由该等待 goroutine 发送数据的 channel 上进行等待。

或者,也许可以不用专门的等待 goroutine,而是在额外的 goroutine 中进行监控,并对来自那里的数据流进行序列化处理。

不管怎样,我并没有完全理解根本问题。 此致

更多关于Golang中如何判断通过`cmd.Start`启动的进程是否已完成的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


确实,问题没有被理解清楚。

关键在于:阻塞(cmd.Wait)意味着等待外部进程 100% 完成。而我想要的是获取进程的进度信息,比如是否完成了 10%,然后是 30%,等等。

我找到了一个解决方案,而且相当简单:我使用 ps -q <pid> -o state --no-headers 来获取进程状态,然后简单地等待,直到状态变为 ‘Z’(Z = 僵尸进程,表示进程已完成,但尚未被其父进程回收)。在此期间,我可以从运行进程的 stdout/stderr 获取进度信息。

我只是以为会有一个更简单的解决方案,利用 cmd 进程的属性或函数……

func main() {
    fmt.Println("hello world")
}

不知道,但是当你在其他 goroutine/进程写入时,直接调用 String() 读取字符串构建器的内容,这难道不是一种竞态条件吗?通常你会从管道中读取,而管道不是字符串构建器,因此操作系统会确保你可以读取,进程也可以写入,不会出现问题。

要么你这样做,要么按照 Copilot 的建议(?),使用一个安全的字符串构建器,它序列化了 Write()String() 操作。这看起来有点道理?

type SafeStringBuilder struct {
    mu       sync.RWMutex
    builder  strings.Builder
}

func (sb *SafeStringBuilder) Write(s string) {
    sb.mu.Lock()
    defer sb.mu.Unlock()
    sb.builder.WriteString(s)
}

func (sb *SafeStringBuilder) String() string {
    sb.mu.RLock()
    defer sb.mu.RUnlock()
    return sb.builder.String()
}

我更习惯在 Windows 上调用操作系统函数,所以,我可能帮不上什么忙,但这代码看起来不太正确。

此致

我完全没头绪,所以问了 Copilot。它基本上告诉我,如果你使用 waitpid 读取退出状态,那么进程就不应该不必要地保持在 Z 状态(这取决于释放资源具体指什么,我猜?)。

它建议了这段代码:

        var ws syscall.WaitStatus
        _, err := syscall.Wait4(int(pid), &ws, 0, nil)
        if err != nil {
            fmt.Println("Error during waitpid:", err)
            return
        }
        if ws.Exited() {
            fmt.Println("Child exited with status:", ws.ExitStatus())
        }

这段代码是阻塞的,所以我想它应该在一个 goroutine 中运行。根据这位 AI 朋友的说法,这应该是最简单的方法,并且应该不会让进程滞留。

不过,很抱歉,我目前用的是 Windows,而且我也不太清楚你具体遇到了什么问题。我记得上次我用 Linux 时,多线程(甚至还不是多进程)用了一段时间后让我重新考虑了自己的选择。也许可以找找现成的、用 Go 监控进程的代码。

总之,我没有完全理解(它滞留在 Z 状态?速度慢且不可预测?),但祝你好运 🙂 致以亲切的问候

假设你启动了一个长时间运行的进程。它可能需要几分钟才能完成。该进程会提供输出(标准输出/标准错误),你可以对其进行分析,并向用户提供有关进程进度的一些信息/报告(例如,已经完成了多少工作)。你希望(1)监控该进程,并提供此类进度状态,以及(2)检测进程何时完成(以任何方式:它不再活动)。这就是我想要/需要做的全部事情。

我做了一个简单的实验。我写了以下 Bash 脚本:

for i in `seq 10`; do
  sleep 5
  echo $i
done

这是一个简单的进程,它计数到 10,每次计数前都有一些延迟。它模拟了一个长时间运行的进程,该进程将无中断地完成。

现在,我想要一个 Go 程序,它能够: (1) 启动该进程(Bash 脚本), (2) 报告最后计数的数字, (3) 检测到启动的进程已完成。

并且所有这些操作都不能有任何阻塞,因为报告可以发送到其他服务/goroutine。

我最终得到了以下代码:

func processStatus(pid int) string {
  str_pid := strconv.Itoa(pid)
  cmd := exec.Command("ps", "-q", str_pid, "-o", "state", "--no-headers")
  var out strings.Builder
  cmd.Stdout = &out
  err := cmd.Run()
  if err != nil {
    log.Fatal(err)
  }
  return out.String()
}

func main() {
  // set up the command
  cmd := exec.Command("./test1.sh")
  var out strings.Builder
  cmd.Stdout = &out

  // start the command
  err := cmd.Start()
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println("Command has been started...")
  
  // wait for the command/process to finish
  for strings.HasPrefix(processStatus(cmd.Process.Pid), "S") {
    fmt.Println("...command still running")

    // report the progress
    arr := strings.Split(out.String(), "\n")
    if len(arr) > 1 {
      fmt.Println( arr[ len(arr) - 2 ] )
    } else if len(arr) == 1 {
      fmt.Println( arr[0] )
    }

    // just to clean up the output a little
    out.Reset()

    // we don't know 
    time.Sleep(5 * time.Second)
  }

  err = cmd.Wait()
  if err != nil {
    log.Fatal(err)
  }

我发现的问题是,如果你使用 cmd.Start() 启动命令,在它完成后,它不会创建 cmd.ProcessState 结构体/字段。所以我不得不使用 ps 系统命令来检查进程状态。

在Go中判断通过cmd.Start启动的进程是否已完成,最可靠的方法是使用ProcessState或结合Wait的非阻塞方式。以下是几种实现方案:

方案一:使用cmd.Process.Wait()(推荐)

虽然cmd.Wait()会阻塞,但可以通过goroutine实现非阻塞监控:

package main

import (
    "fmt"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("sleep", "5")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    done := make(chan error, 1)
    go func() {
        done <- cmd.Wait()
    }()

    select {
    case err := <-done:
        fmt.Printf("进程已完成,错误信息: %v\n", err)
    case <-time.After(2 * time.Second):
        fmt.Println("进程仍在运行...")
    }
}

方案二:定期检查ProcessState

package main

import (
    "fmt"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("sleep", "3")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    for {
        if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
            fmt.Println("进程已完成")
            break
        }
        
        // 检查进程是否仍在运行
        if err := cmd.Process.Signal(os.Signal(syscall.Signal(0))); err != nil {
            fmt.Printf("进程已结束: %v\n", err)
            break
        }
        
        fmt.Println("进程仍在运行...")
        time.Sleep(1 * time.Second)
    }
}

方案三:使用os.FindProcess检查

package main

import (
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("sleep", "2")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    pid := cmd.Process.Pid
    
    for {
        p, err := os.FindProcess(pid)
        if err != nil {
            fmt.Printf("进程查找失败: %v\n", err)
            break
        }
        
        // 发送信号0检查进程是否存在
        err = p.Signal(os.Signal(syscall.Signal(0)))
        if err != nil {
            fmt.Printf("进程已结束: %v\n", err)
            break
        }
        
        fmt.Println("进程仍在运行...")
        time.Sleep(500 * time.Millisecond)
    }
}

方案四:结合context和Wait

package main

import (
    "context"
    "fmt"
    "os/exec"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    cmd := exec.CommandContext(ctx, "sleep", "7")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    go func() {
        err := cmd.Wait()
        if err != nil {
            fmt.Printf("进程结束状态: %v\n", err)
        } else {
            fmt.Println("进程正常结束")
        }
    }()

    // 主程序继续执行其他任务
    for i := 0; i < 5; i++ {
        fmt.Println("执行其他任务...")
        time.Sleep(1 * time.Second)
    }
}

关键点说明:

  1. cmd.ProcessState在进程结束后会被自动设置
  2. 发送信号0的方法在Unix-like系统有效,Windows需使用不同方法
  3. 最佳实践是使用goroutine配合cmd.Wait(),通过channel传递完成状态
  4. 进程结束后,cmd.Process.Signal()会返回os.ErrProcessDone错误

这些方案都能在非阻塞的情况下监控进程状态,具体选择取决于你的使用场景和平台要求。

回到顶部