golang实现守护进程(2)

前言

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

1. 守护进程只创建一次

2. 平滑创建业务进程

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

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

5. 业务进程≈子进程

6. 不影响业务进程逻辑

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

分析

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

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

  • 在 Go 语言中,Linux 下创建进程使用的系统调用是 clone 。

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

  1. fork

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

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

  1. execve

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

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

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

  1. 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)
}
  1. 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)
}
  1. 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   -- ③
  1. 刚开始会出现三个进程,假设进程id如上,一会之后①会消失,这是正常的,因为刚开始的启动就是①,然后只剩下进程②和③

]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 ./go_start -business -c param1 -d param2 -c param3   -- ③
  1. 验证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   -- ③
  1. 这里kill 112后,会出现一个僵尸进程,不影响实际业务进程的创建和运行,不需要理会;假设新创建的业务进程pid为113

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

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

综上

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

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

放在(3)实现

附录

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

信号

动作

说明

SIGHUP

1

Term

终端控制进程结束(终端连接断开)

SIGINT

2

Term

用户发送INTR字符(Ctrl+C)触发

SIGQUIT

3

Core

用户发送QUIT字符(Ctrl+/)触发

SIGILL

4

Core

非法指令(程序错误、试图执行数据段、栈溢出等)

SIGABRT

6

Core

调用abort函数触发

SIGFPE

8

Core

算术运行错误(浮点运算错误、除数为零等)

SIGKILL

9

Term

无条件结束程序(不能被捕获、阻塞或忽略)

SIGSEGV

11

Core

无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)

SIGPIPE

13

Term

消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)

SIGALRM

14

Term

时钟定时信号

SIGTERM

15

Term

结束程序(可以被捕获、阻塞或忽略)

SIGUSR1

30,10,16

Term

用户保留

SIGUSR2

31,12,17

Term

用户保留

SIGCHLD

20,17,18

Ign

子进程结束(由父进程接收)

SIGCONT

19,18,25

Cont

继续执行已经停止的进程(不能被阻塞)

SIGSTOP

17,19,23

Stop

停止进程(不能被捕获、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略) SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发 SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dkjhl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值