您现在的位置是:首页 >其他 >golang实现守护进程(3)—网站首页其他
golang实现守护进程(3)—
前言
在操作系统中,每个进程都有自己的进程ID(PID),父进程ID(PPID)等信息。如果一个进程已经退出,但它的、它的资源和状态还留在系统中,这种进程被称为“僵尸进程”。僵尸进程不占用CPU时间和内存资源,但它们占用系统的进程表,因此可以说是一种资源泄漏。
正确处理僵尸进程
通常,进程在完成任务之后会向其父进程发送一个信号(SIGCHLD),表示自己已经退出。如果父进程没有正确地处理这个信号,会导致子进程沦为僵尸进程。因此,解决僵尸进程的方法通常是确保正确处理SIGCHLD信号。
以下是一些手动处理僵尸进程的方法:
1. 杀死父进程:如果一个进程的父进程已经退出,这个进程会被自动分配给PID=1的进程(通常是init)作为父进程。在这种情况下,如果你杀死PID=1的进程,所有的僵尸进程将被清除。
2. 重启服务:某些服务可能会创建僵尸进程,因此重启服务可以清除它们。
3. 使用kill命令:可以使用kill命令杀死僵尸进程。首先需要使用pstree命令获取僵尸进程的父进程PID,然后使用kill来杀死它。
```
pstree <zombie-pid>
kill -9 <parent-pid>
```
4. 编写清理脚本:可以编写一个脚本来定期扫描系统中的僵尸进程并将它们清理掉。
实际应用
处理syscall.SIGCHLD
syscall.SIGCHLD是一个信号常量,表示子进程状态发生变化,例如子进程终止或暂停等。在Unix/Linux系统中,当子进程状态发生变化时,内核会向父进程发送SIGCHLD信号,以通知父进程子进程已经发生了变化。父进程可以通过捕获SIGCHLD信号来获知子进程状态的变化,并采取相应的措施。常用的处理方式是调用wait或waitpid函数,以获取子进程的退出状态。
在Go中,如果子进程被挂起且父进程没有调用wait或waitpid函数来回收子进程,那么该子进程就会成为一个僵尸进程。为了避免僵尸进程的产生,可以通过使用os/exec包中的Cmd.Wait方法来等待子进程的退出并回收资源,如下所示:
cmd := exec.Command("your command")
err := cmd.Start()
if err != nil {
// handle error
}
// 等待子进程退出并回收资源
err = cmd.Wait()
if err != nil {
// handle error
}
在调用Wait方法时,Go会阻塞当前goroutine,直到被等待的进程退出并回收资源。当子进程退出时,操作系统会向父进程发送SIGCHLD信号,父进程会在等待过程中捕获此信号并回收子进程资源。
如果需要处理多个子进程,可以使用go语句在单独的goroutine中运行wait函数,在主goroutine中处理其他任务,如下所示:
cmd := exec.Command("your command")
err := cmd.Start()
if err != nil {
// handle error
}
// 在单独的goroutine中等待子进程退出并回收资源
go func() {
err = cmd.Wait()
if err != nil {
// handle error
}
}()
// 处理其他任务
通过在单独的goroutine中等待子进程退出,可以避免阻塞主goroutine的执行。当子进程退出时,wait函数会自动回收子进程的资源,避免了僵尸进程的产生。
设置子进程的系统属性
在Go语言中,如果需要设置子进程的系统属性,可以使用`os/exec`包中的`Cmd.SysProcAttr`字段,该字段的类型为`*syscall.SysProcAttr`。通过设置`Cmd.SysProcAttr`字段,可以设置子进程的属性。其中可设置的子进程属性包括信号处理方式、进程组、环境变量等。`Cmd.SysProcAttr`字段的默认值为nil,表示继承父进程的属性。
如果需要在子进程中禁用Ctrl+C信号,可以使用下面的代码:
cmd := exec.Command("your command")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 创建新进程组,以便Ctrl+C信号不会被传递到子进程
}
err := cmd.Start()
if err != nil {
// handle error
}
// 等待子进程退出并回收资源
err = cmd.Wait()
if err != nil {
// handle error
}
在上述代码中,通过设置`syscall.SysProcAttr`结构体中的`Setpgid`字段为`true`来创建一个新的进程组,以便Ctrl+C信号不会被传递到子进程。
设置进程组ID
在Go语言中,如果需要在子进程中设置进程组ID,可以使用`os/exec`包中的`Cmd.SysProcAttr`字段,该字段的类型为`*syscall.SysProcAttr`。通过设置`Cmd.SysProcAttr`字段的`Pgid`字段,可以设置子进程的进程组ID。如果不设置`Pgid`字段,则子进程会继承父进程的进程组ID。
下面的代码演示了如何在创建子进程时设置子进程的进程组ID:
cmd := exec.Command("your command")
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 创建新进程组,在新的进程组中运行子进程
Pgid: id, // 设置进程组ID
}
err := cmd.Start()
if err != nil {
// handle error
}
// 等待子进程退出并回收资源
err = cmd.Wait()
if err != nil {
// handle error
}
在上述代码中,通过设置`syscall.SysProcAttr`结构体中的`Setpgid`字段为`true`来创建一个新的进程组,以便子进程在新的进程组中运行。同时,通过设置`Pgid`字段,可以将子进程添加到指定的进程组中。
注意,设置进程组ID时需要保证指定的进程组ID不存在,否则会设置失败。一般来说,可以使用当前进程的进程组ID作为子进程的进程组ID。可以使用`syscall.Getpgid()`函数获取当前进程的进程组ID。
一个完整demo
在Go语言中,可以使用`syscall.Wait4()`函数来等待子进程退出并回收资源。该函数的第一个参数为子进程的进程ID,如果传入-1则表示等待任意一个子进程退出。第二个参数为传出参数,用于存储子进程的状态信息。第三个参数为附加选项,如果设置为`syscall.WNOHANG`则表示非阻塞模式,即立即返回,不等待子进程退出。如果不设置该选项,则函数会一直阻塞,直到子进程退出。第四个参数为资源使用信息,可以设置为nil。
下面的代码演示了如何使用`syscall.Wait4(-1, &ws, syscall.WNOHANG, nil)`函数来获取任意一个子进程的状态信息:
package main
import (
"fmt"
"syscall"
"time"
)
func main() {
// 创建多个子进程
for i := 0; i < 10; i++ {
go func() {
time.Sleep(1 * time.Second)
}()
}
// 等待任意一个子进程退出并回收资源
var ws syscall.WaitStatus
pid, _ := syscall.Wait4(-1, &ws, syscall.WNOHANG, nil)
if pid > 0 {
if ws.Exited() {
exitStatus := ws.ExitStatus()
fmt.Printf("子进程 %d 退出,退出状态码:%d
", pid, exitStatus)
} else if ws.Signaled() {
signal := ws.Signal()
fmt.Printf("子进程 %d 收到信号:%d
", pid, signal)
} else {
fmt.Printf("子进程 %d 退出,但状态未知
", pid)
}
} else {
fmt.Println("没有子进程退出")
}
}
在上述代码中,通过`syscall.Wait4(-1, &ws, syscall.WNOHANG, nil)`函数等待任意一个子进程退出并回收资源,并根据子进程的退出状态打印相应的信息。如果没有任何子进程退出,则打印提示信息。由于设置了`syscall.WNOHANG`选项,函数会立即返回,不会阻塞等待。需要注意的是,在多个子进程中,由于子进程退出的顺序是未知的,因此不能确定到底哪个子进程会先退出。
结合之前文章 : golang实现守护进程(2)_golang创建守护进程_dkjhl的博客-CSDN博客
// ------------------------ 守护进程 start ------------------------
global.G_LOG.Info(fmt.Sprintf("os.args is %v", os.Args))
join := strings.Join(os.Args, "")
if !strings.Contains(join, "-daemon") {
isE, ierr := utils.CheckProRunning("go_start | grep daemon")
if ierr != nil {
global.G_LOG.Error("check daemon process failed, " + ierr.Error())
return
}
if isE {
global.G_LOG.Info("daemon process exist!")
} else {
global.G_LOG.Info("start daemon process branch...")
// 启动守护进程
cmd := exec.Command(os.Args[0], "-c", argv.config, "-chost", argv.chHost, "-daemon")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
strerr := cmd.Start()
if strerr != nil {
global.G_LOG.Error("start daemon process fail," + strerr.Error())
return
}
global.G_LOG.Info("start daemon process success!")
time.Sleep(time.Second * 2)
isDae, daeErr := utils.CheckProRunning("go_start | grep daemon")
if daeErr != nil {
global.G_LOG.Error("check daemon process failed, " + daeErr.Error())
return
}
if isDae {
daePid := cmd.Process.Pid
global.G_LOG.Info(fmt.Sprintf("start daemon process success, pid is %d", daePid))
return
} else {
global.G_LOG.Error("warning! start daemon process fail...")
}
}
}
join = strings.Join(os.Args, "")
if strings.Contains(join, "-daemon") {
for {
exist, checkerr := utils.CheckProRunning("go_start | grep business")
if checkerr != nil {
global.G_LOG.Error("check business failed, " + checkerr.Error())
return
}
if exist {
global.G_LOG.Info("business process exist!")
time.Sleep(time.Second * 5)
continue
}
global.G_LOG.Info("start business process branch...")
command := exec.Command(fmt.Sprintf(path, "-business", "-c", argv.config, "-chost", argv.chHost))
command.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 创建新进程组,以便Ctrl+C信号不会被传递到子进程
}
// 设置命令输出和错误输出都打印到主进程的终端
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
if comerr := command.Start(); comerr != nil {
global.G_LOG.Error("start business process failed, " + comerr.Error())
return
}
// 父进程等待子进程完成并回收子进程,处理僵尸进程;os.exec命令自带回收僵尸进程逻辑,需要调用Wait()方法
werr := command.Wait()
if werr != nil {
global.G_LOG.Error(fmt.Sprintf("wait sub process collect error: %s", werr.Error()))
}
time.Sleep(time.Second * 5)
exist, checkerr = utils.CheckProRunning("go_start | grep business")
if checkerr != nil {
global.G_LOG.Error("check business process failed, " + checkerr.Error())
return
}
if exist {
businessPid := command.Process.Pid
global.G_LOG.Info(fmt.Sprintf("start business process suceess, pid is %d", businessPid))
} else {
global.G_LOG.Error("warning! start business process fail...")
}
}
}
上述代码,在原先的基础上,子进程启动时,添加了【父进程等待子进程完成并回收子进程,处理僵尸进程;os.exec命令自带回收僵尸进程逻辑,需要调用Wait()方法】,真正解决僵尸进程,此时程序启动,调用kill 子进程id,不会出现僵尸进程,kill 父进程id,整个进程组结束
结论
要避免僵尸进程问题,最好的方法是及时处理SIGCHLD信号。如果您正在编写应用程序,它需要管理子进程,并且需要忽略SIGCHLD信号,则可以将SIGCHLD的行为设置为SIG_IGN,指示内核自动清理终止的子进程。