Golang守护进程用法示例分析

 更新时间:2023年05月16日 11:35:14   作者:dkjhl  
这篇文章主要介绍了Golang守护进程用法示例,创建守护进程首先要了解go语言如何实现创建进程,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习吧

前言

golang实现守护进程,包含功能:

1. 守护进程只创建一次

2. 平滑创建业务进程

3. 业务进程挂起,守护进程能监听,并重启新启业务进程

4. 守护进程退出,也能保证业务进程退出

5. 业务进程≈子进程

6. 不影响业务进程逻辑

7. 以Linux平台为主,其他平台暂时没有实施条件

分析

上一篇博文讨论过如何以脚本的形式创建守护进程,这篇讨论如何以纯golang脚本实现守护进程的功能

  • 在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。
  • 在 Go 语言中,Linux 下创建进程使用的系统调用是 clone 。

在 C 语言中,通常会用到 2 种创建进程方法:

fork

pid = fork();
//pid > 0 父进程
//pid = 0 子进程
//pid < 0 出错

程序会从 fork 处一分为二,父进程返回值大于0,并继续运行;子进程获得父进程的栈、数据段、堆和执行文本段的拷贝,返回值等于0,并向下继续运行。通过 fork 返回值可轻松判断当前处于父进程还是子进程。

execve

execve(pathname, argv, envp);
//pathname 可执行文件路径
//argv 参数列表
//envp 环境变量列表

execve 为加载一个新程序到当前进程的内存,这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。

在 Go 语言中,创建进程方法主要有 3 种:

exec.Command

//判 断当其是否是子进程,当父进程return之后,子进程会被 系统1 号进程接管
if os.Getppid() != 1 {
    // 将命令行参数中执行文件路径转换成可用路径
    filePath, _ := filepath.Abs(os.Args[0])
    cmd := exec.Command(filePath, os.Args[1:]...)
    // 将其他命令传入生成出的进程
    cmd.Stdin = os.Stdin // 给新进程设置文件描述符,可以重定向到文件中
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Start() // 开始执行新进程,不等待新进程退出
    os.Exit(0)
}

os.StartProcess

if os.Getppid()!=1{   
    args:=append([]string{filePath},os.Args[1:]...)
    os.StartProcess(filePath,args,&os.ProcAttr{Files:[]*os.File{os.Stdin,os.Stdout,os.Stderr}})
    os.Exit(0)
}

syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)

pid, _, sysErr := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
if sysErr != 0 {
    Utils.LogErr(sysErr)
    os.Exit(0)
}

方法1和方法2通过 os.Getppid()!=1进行判断是否子进程,默认父进程退出之后,子进程会被1号进程接管。

但据Ubuntu Desktop 本地测试,接管孤儿进程的并不是1号进程,因此考虑到程序稳定性和兼容性,不能够以 ppid 作为判断父子进程的依据。

方法3直接进行了系统调用,虽然可以通过 pid 进行判断父子进程,但该方法过于底层。

综上,以exec.Command方式,通过控制参数实现守护进程

实现

func main() {
    // ------------------------ 守护进程 start ------------------------
    basePath, _ := os.Getwd()
    baseDir := filepath.Dir(basePath)
    fmt.Println(fmt.Sprintf("basePath is %s and baseDir is %s", basePath, baseDir))
    // step1
    // 创建监听退出chan
    c := make(chan os.Signal)
    // 监听指定信号 ctrl+c kill
    signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    go func() {
        for s := range c {
            switch s {
            case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
                utils.StopBusinessProcess(fmt.Sprintf("go_start | grep business"))
                os.Exit(0)
            default:
                fmt.Println("test stop others...")
            }
        }
    }()
    fmt.Println(fmt.Sprintf("os.args is %v", os.Args))
    join := strings.Join(os.Args, "")
    // step2
    if !strings.Contains(join, "-daemon") {
        fmt.Println("enter daemon branch...")
        isE, ierr := utils.CheckProRunning("go_start | grep daemon")
        if ierr != nil {
            fmt.Println("check daemon process failed, " + ierr.Error())
            return
        }
        if isE {
            fmt.Println("daemon process exist!")
        } else {
            fmt.Println("start daemon process...")
            // 启动守护进程
            cmd := exec.Command(os.Args[0], "-c", os.Args[2], "-d", os.Args[4], "-e", os.Args[6], "-daemon")
            cmd.Stdin = os.Stdin
            cmd.Stdout = os.Stdout
            cmd.Stderr = os.Stderr
            strerr := cmd.Start()
            if strerr != nil {
                fmt.Println("start daemon process fail," + strerr.Error())
                return
            }
            fmt.Println("start daemon process success!")
            time.Sleep(time.Second * 2)
            daePid := cmd.Process.Pid
            isDae, daeErr := utils.CheckProRunning("go_start | grep daemon")
            if daeErr != nil {
                fmt.Println("check daemon process failed, " + daeErr.Error())
                return
            }
            if isDae {
                fmt.Println(fmt.Sprintf("start daemon process success, pid is %d", daePid))
                return
            } else {
                fmt.Println("warning! start business process fail...")
            }
        }
    }
    // step3
    join = strings.Join(os.Args, "")
    if strings.Contains(join, "-daemon") {
        fmt.Println("enter business branch...")
        for {
            exist, checkerr := utils.CheckProRunning("go_start | grep business")
            if checkerr != nil {
                fmt.Println("check business failed, " + checkerr.Error())
                return
            }
            if exist {
                fmt.Println("business process exist!")
                time.Sleep(time.Second * 5)
                continue
            }
            fmt.Println("start business process...")
            command := exec.Command(fmt.Sprintf(fmt.Sprintf("%s/go_start", basePath), "-business", "-c", os.Args[2], "-d", os.Args[4], "-e", os.Args[6]))
            command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
            if comerr := command.Start(); comerr != nil {
                fmt.Println("start business process failed, " + comerr.Error())
                return
            }
            time.Sleep(time.Second * 5)
            businessPid := command.Process.Pid
            exist, checkerr = utils.CheckProRunning("go_start | grep business")
            if checkerr != nil {
                fmt.Println("check business process failed, " + checkerr.Error())
                return
            }
            if exist {
                fmt.Println(fmt.Sprintf("start business process suceess, pid is %d", businessPid))
            } else {
                fmt.Println("warning! start business process fail...")
            }
        }
    }
    // ------------------------ 守护进程 end ------------------------
    // ------------------------ 业务进程 start ------------------------
    fmt.Println("hello, welcome to business detail!")
}

相关工具方法:

package utils
import (
    "os"
    "fmt"
    "go_start/core/global"
    "os/exec"
    "runtime"
    "strconv"
    "strings"
    "syscall"
)
func StopBusinessProcess(serverName string) {
    global.G_LOG.Info("start to stop business...")
    pid, _ := GetPid(serverName)
    if pid > 0 {
        global.G_LOG.Info(fmt.Sprintf("stop %s ...", serverName))
        syscall.Kill(pid, syscall.SIGKILL)
        global.G_LOG.Info(fmt.Sprintf("stop business success, pid is %d", pid))
    }
}
//根据进程名判断进程是否运行
func CheckProRunning(serverName string) (bool, error) {
    a := `ps -ef|grep ` + serverName + `|grep -v grep|awk '{print $2}'`
    pid, err := runCommand(a)
    if err != nil {
        return false, err
    }
    return pid != "", nil
}
//根据进程名称获取进程ID
func GetPid(serverName string) (pid int, err error) {
    a := `ps -ef|grep ` + serverName + `|grep -v grep|awk '{print $2}'`
    var pidStr string
    if pidStr, err = runCommand(a); err != nil {
        return
    }
    pid, err = strconv.Atoi(pidStr)
    return
}
func runCommand(cmd string) (string, error) {
    if runtime.GOOS == "windows" {
        return runInWindows(cmd)
    } else {
        return runInLinux(cmd)
    }
}
func runInWindows(cmd string) (string, error) {
    result, err := exec.Command("cmd", "/c", cmd).Output()
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(result)), err
}
func runInLinux(cmd string) (string, error) {
    result, err := exec.Command("/bin/sh", "-c", cmd).Output()
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(result)), err
}

说明

1、启动go_start二进制文件,方式:./go_start -c param1 -d param2 -e param3,这里第一次进入main方法

2、main方法中,os.Args = [./go_start -c param1 -d param2 -c param3],此时不包含"-daemon"参数,进入step2,走创建守护进程代码分支,执行创建守护进程,exec.Command(./go_start -c param1 -d param2 -e param3 -daemon),第二次进入main方法

3、main方法中,os.Args = [./go_start -c param1 -d param2 -c param3 -daemon],此时包含"-daemon",进入step3,走创建业务进程分支,执行创建业务进程,exec.Command(./go_start -c param1 -d param2 -e param3);此时守护进程存在,每隔5秒监听一次业务进程是否存在,如果存在则不操作;不存在则重新执行创建业务进程exec.Command(./go_start -c param1 -d param2 -e param3);

4、执行具体的业务进程逻辑

验证

ps -ef | grep go_start

]$ 110  1   ./go_start -c param1 -d param2 -c param3             -- ①
]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 ./go_start -business -c param1 -d param2 -c param3   -- ③

刚开始会出现三个进程,假设进程id如上,一会之后①会消失,这是正常的,因为刚开始的启动就是①,然后只剩下进程②和③

]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 ./go_start -business -c param1 -d param2 -c param3   -- ③

验证kill业务进程:会启动新的业务进程,守护进程不变;所以执行:kill 112

]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 [go_start] <defunct>                                 -- ③'
]$ 113  111 ./go_start -business -c param1 -d param2 -c param3   -- ③

这里kill 112后,会出现一个僵尸进程,不影响实际业务进程的创建和运行,不需要理会;假设新创建的业务进程pid为113

验证kill守护进程:整个程序退出,也就是执行ps -ef | grep go_start后,没有对应的守护进程和业务进程,同时僵尸进程也会消失;得益于以下代码,进程组

command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

综上

纯golang语言形式实现了守护进程,针对启动业务进程,优化点:可以使用go func(){}()协程方式启动更优雅,这里先不实施,待后续有空改进;

缺点:依然要通过参数控制守护进程和业务进程,-daemon -business,期望统一起来,不用参数控制

放在(3)实现

附录

以下是关于信号量的一个记录,当作参考文档

信号动作说明
SIGHUP1Term终端控制进程结束(终端连接断开)
SIGINT2Term用户发送INTR字符(Ctrl+C)触发
SIGQUIT3Core用户发送QUIT字符(Ctrl+/)触发
SIGILL4Core非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT6Core调用abort函数触发
SIGFPE8Core算术运行错误(浮点运算错误、除数为零等)
SIGKILL9Term无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV11Core无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE13Term消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM14Term时钟定时信号
SIGTERM15Term结束程序(可以被捕获、阻塞或忽略)
SIGUSR130,10,16Term用户保留
SIGUSR231,12,17Term用户保留
SIGCHLD20,17,18Ign子进程结束(由父进程接收)
SIGCONT19,18,25Cont继续执行已经停止的进程(不能被阻塞)
SIGSTOP17,19,23Stop停止进程(不能被捕获、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略) SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发 SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

到此这篇关于Golang守护进程用法示例分析的文章就介绍到这了,更多相关Golang守护进程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • golang 实现json类型不确定时的转换

    golang 实现json类型不确定时的转换

    这篇文章主要介绍了golang 实现json类型不确定时的转换操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • 详解如何在Go语言中调用C源代码

    详解如何在Go语言中调用C源代码

    这篇文章主要为大家详细介绍了如何在Go语言中调用C语言源代码,文中的示例代码讲解详细,对我们学习或工作有一定的帮助,需要的可以参考一下
    2022-05-05
  • Go语言模型:string的底层数据结构与高效操作详解

    Go语言模型:string的底层数据结构与高效操作详解

    这篇文章主要介绍了Go语言模型:string的底层数据结构与高效操作详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go映射的使用

    Go映射的使用

    Go提供了另一个重要的数据类型,称为map,它将唯一键映射到值,本文主要介绍了Go映射的使用,包括声明映射、初始化映射、操作映射等,感兴趣的可以了解一下
    2023-11-11
  • go语言用八百行代码实现一个JSON解析器

    go语言用八百行代码实现一个JSON解析器

    这篇文章主要为大家介绍了go语言用八百行代码实现一个JSON解析器实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Golang channel关闭后是否可以读取剩余的数据详解

    Golang channel关闭后是否可以读取剩余的数据详解

    这篇文章主要介绍了Golang channel关闭后是否可以读取剩余的数据,文章通过一个测试例子给大家详细的介绍了是否可以读取剩余的数据,需要的朋友可以参考下
    2023-09-09
  • 浅析Golang中类型嵌入的简介与使用

    浅析Golang中类型嵌入的简介与使用

    类型嵌入指的就是在一个类型的定义中嵌入了其他类型,Go 语言支持两种类型嵌入,分别是接口类型的类型嵌入和结构体类型的类型嵌入,下面我们就来详细一下类型嵌入的使用吧
    2023-11-11
  • GoLang context包的使用方法介绍

    GoLang context包的使用方法介绍

    日常Go开发中,Context包是用的最多的一个了,几乎所有函数的第一个参数都是ctx,那么我们为什么要传递Context呢,Context又有哪些用法,底层实现是如何呢?相信你也一定会有探索的欲望,那么就跟着本篇文章,一起来学习吧
    2023-03-03
  • 用go写的五子棋预测算法的实现

    用go写的五子棋预测算法的实现

    这篇文章主要介绍了用go写的五子棋预测算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • golang如何部署到服务器及应注意问题解析

    golang如何部署到服务器及应注意问题解析

    这篇文章主要为大家介绍了golang如何部署到服务器及应注意问题解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01

最新评论