您现在的位置是:首页 >其他 >golang实现守护进程(3)—网站首页其他

golang实现守护进程(3)—

dkjhl 2024-06-12 12:01:02
简介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,指示内核自动清理终止的子进程。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。