Golang中如何判断通过`cmd.Start`启动的进程是否已完成
Golang中如何判断通过cmd.Start启动的进程是否已完成
问题: 我使用 cmd.Start 启动了一个系统命令。这是一个长时间运行的进程,我通过检查其输出来监控进度。如何检查该进程是否已完成/结束?
我没有调用 cmd.Wait,因为我不想等待进程结束(Wait 会阻塞直到进程完成,而我不希望有任何阻塞)。
我曾尝试向进程发送信号 0,但即使在进程的所有输出都完成后,它仍然返回相同的值。
你好,我只是猜测一下,既然你不喜欢阻塞,为什么不把 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)
}
}
关键点说明:
cmd.ProcessState在进程结束后会被自动设置- 发送信号0的方法在Unix-like系统有效,Windows需使用不同方法
- 最佳实践是使用goroutine配合
cmd.Wait(),通过channel传递完成状态 - 进程结束后,
cmd.Process.Signal()会返回os.ErrProcessDone错误
这些方案都能在非阻塞的情况下监控进程状态,具体选择取决于你的使用场景和平台要求。

