Golang中使用exec.Command启动新进程的方法
Golang中使用exec.Command启动新进程的方法
我编写了一个程序,用于下载YouTube视频(使用youtube-dl的分支:yt-dlp),然后使用exec.Command通过例如smplayer来播放视频。问题是,每次我启动一个视频,之后又启动一个新视频时,第一个视频的“僵尸”进程会残留在内存中:
当然,这不是一个大问题,但几天后可能会有上百个这样的僵尸进程。唯一能清除这些僵尸进程的方法就是重启我的程序,所以这些smplayer进程由于某种原因附着在主进程(我的程序)上。
我使用的是Linux Mint 20.2和go 1.15。
以下是启动视频播放器的代码:
func (v *VideoList) playVideo(video *database.Video) {
videoPath := v.getVideoPath(video.ID)
{...}
command := fmt.Sprintf("smplayer '%s'", videoPath)
cmd := exec.Command("/bin/bash", "-c", command)
// Starts a sub process (smplayer)
err := cmd.Start()
if err != nil {
logger.LogError(err)
}
}
我尝试过的一些方法,包括以下各项的所有组合:
- 在exec.Command之后和cmd.Start之前添加以下代码。显然这应该以某种方式分离进程,但我无法让它工作:
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
- 将调用函数包装在goroutine中:
go func() {
v.PlayVideo(video)
}()
- 我尝试过使用其他播放器,比如mpv,而不是smplayer。
有人知道如何解决这个问题吗?
更多关于Golang中使用exec.Command启动新进程的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你没有在调用 Start 之后调用 (*os.Cmd).Wait,而 Wait 会等待命令完成并释放所有资源。你也可以使用 (*os.Cmd).Run 来代替 Start,这本质上结合了对 Start 和 Wait 的调用。因为你是在自己的 goroutine 中运行该进程,并且没有阻塞任何操作,所以我建议切换到使用 Run。
func main() {
fmt.Println("hello world")
}
更多关于Golang中使用exec.Command启动新进程的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go中,使用exec.Command启动子进程时,如果父进程不等待子进程退出,可能会导致僵尸进程。这是因为子进程退出后,其退出状态需要被父进程收集(通过Wait()),否则它会保持为僵尸状态。
在你的代码中,你使用了cmd.Start()但没有调用cmd.Wait()。对于播放器这类需要独立运行的程序,正确的做法是让子进程完全脱离父进程的控制。以下是几种解决方案:
1. 使用syscall.SysProcAttr设置进程组(推荐)
你之前的尝试方向正确,但需要调整参数。在Linux系统上,可以设置Setpgid和Pdeathsig来确保子进程独立:
func (v *VideoList) playVideo(video *database.Video) {
videoPath := v.getVideoPath(video.ID)
// {...}
cmd := exec.Command("smplayer", videoPath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 设置新的进程组
Pdeathsig: syscall.SIGKILL, // 父进程退出时发送SIGKILL
}
// 启动进程并立即释放资源
err := cmd.Start()
if err != nil {
logger.LogError(err)
return
}
// 重要:立即调用Wait释放资源,但不阻塞
go cmd.Wait()
}
2. 使用nohup或setsid启动进程
另一种方法是使用系统工具来分离进程:
func (v *VideoList) playVideo(video *database.Video) {
videoPath := v.getVideoPath(video.ID)
// {...}
// 使用setsid创建新的会话
cmd := exec.Command("setsid", "smplayer", videoPath)
// 或者使用nohup
// cmd := exec.Command("nohup", "smplayer", videoPath, "&>/dev/null", "&")
err := cmd.Start()
if err != nil {
logger.LogError(err)
return
}
// 立即释放资源
go cmd.Wait()
}
3. 使用os.StartProcess进行更精细的控制
exec.Command底层调用的是os.StartProcess,你可以直接使用后者进行更精细的控制:
func (v *VideoList) playVideo(video *database.Video) {
videoPath := v.getVideoPath(video.ID)
// {...}
args := []string{"smplayer", videoPath}
attr := &os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
Sys: &syscall.SysProcAttr{
Setsid: true, // 创建新会话
Setpgid: true, // 设置进程组ID
},
}
// 启动进程
proc, err := os.StartProcess("/usr/bin/smplayer", args, attr)
if err != nil {
logger.LogError(err)
return
}
// 立即释放进程句柄
proc.Release()
}
4. 完整的示例代码
结合你的具体需求,这里是一个完整的解决方案:
func (v *VideoList) playVideo(video *database.Video) {
videoPath := v.getVideoPath(video.ID)
// {...}
cmd := exec.Command("smplayer", videoPath)
// 关键配置:使子进程独立
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 设置新的进程组
Setsid: true, // 创建新会话
Pdeathsig: syscall.SIGTERM, // 父进程死亡信号
}
// 启动进程
if err := cmd.Start(); err != nil {
logger.LogError(err)
return
}
// 在goroutine中等待,避免僵尸进程
go func() {
if err := cmd.Wait(); err != nil {
// 这里可以记录进程退出状态,但不是必须的
if exitErr, ok := err.(*exec.ExitError); ok {
_ = exitErr.ExitCode() // 忽略退出码
}
}
}()
}
关键点说明:
Setpgid: true:为子进程创建新的进程组,使其不受父进程终端控制Setsid: true:创建新的会话,使进程完全独立cmd.Wait()在goroutine中调用:确保收集子进程退出状态,避免僵尸进程- 不要使用bash包装命令:直接调用
smplayer可执行文件,避免额外的shell进程
这些修改应该能解决你的僵尸进程问题。smplayer进程现在会完全独立于你的Go程序运行,即使你的Go程序退出,smplayer也会继续运行(除非设置了Pdeathsig)。

