查看原文
其他

如何让go程序以后台进程或daemon方式运行

Go开发大全 2021-07-20

(给Go开发大全加星标)

来源:zh-five

https://zhuanlan.zhihu.com/p/146192035

【导读】go进程如何在后台运行、如何处理父进程子进程间关系和实现守护进程?本文用实际例子做了详细介绍。

1.前言

因为最近用go语言开发了一个websocket服务,启动后需要后台运行,还希望异常退出时可以自动重启。整体思路是启动程序后,转为后台运行,这个后台程序暂且称为守护进程(daemon)。它不处理具体业务逻辑,只是再次按一样的参数调用自身,启动一个子进程,有子进程负责业务逻辑处理。守护进程监视子进程状态,若退出则再次启动一次。如此可以保证服务异常中止时可以及时重启。

网上找到了一个开源的库github.com/sevlyar/go-daemon,可以很方便保持参数不变另外启动一个后台进程,但如果后台进程再次尝试启动自身为另外一个后台进程,则会出现错误。后来阅读源码发现:为了区分当前进程是父进程还是子进程,作者巧妙的设计了一个环境变量标识,用于标记子进程。也正是因为这种识别策略,此库只能启动一次自身为后台进程,不能连续启动自身为后台进程。不过使用环境变量来区分进程身份的思路,对我启发很大。在此基础上经过延伸和优化,最终实现了在保持参数不变的情况可以连续启动自身为后台进程。向作者致敬!

另外还找了一些库,思路有所不同,基本是通过增加特殊参数来标记进程身份的,这让我感并没有完美的启动了自身进程,有些遗憾。

最终决定自己实现一个库解决我项目中的需求,同时也期望它是一个很通用的库,可以快速方便把go语言编写的服务程序转为后台运行,或者转为守护进程的模式运行。本文算是对这次探索的一次总结和梳理。

2.区分两个概念

后台运行daemon在平常沟通中我们可能不太区分,或者区分得比较模糊。在本文所指中,我要明确区分一下:

后台运行:是指进程在操作系统中非显示运行,未关联到任何命令行终端或程序界面。这中方式运行的进程则称为后台进程,如未关联到任何终端的命令行程序进程。

daemon:也叫守护进程,它首先是后台运行,然后它还有守护的职责。本文所指,是希望守护进程可以监视go服务程序进程的状态,若异常退出,可以自动重启服务程序。

3.首先排除的方案

nohub&setsid都可以让程序在后台运行,但这是平台相关的,只适用于类unix系统。若使用此类方案,那这个库在windows下是无法工作的,不太完美。先不说支持所有平台,我期望这个库至少能支持类unix系统和windows系统。

4.相关的标准库的探索

因为种种原因,在go语言中我们无法很好的直接操作 fork 调用。我们转换一下思路,启动自身为一个子进程,也可以看做是调用外部程序。标准库中找到下面三种方法: - syscall.ForkExec - os.StartProcess - exec.Cmd

syscall.ForkExec的文档说明不是很多,我没有深入研究。阅读那些开源库,发现基本都是使用os.StartProcessexec.Cmdos.StartProcess在文档里有明确说明,这是一个低水平的接口,建议使用os/exec包提供的高水平接口,也就是exec.Cmd

func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) StartProcess使用提供的属性、程序名、命令行参数开始一个新进程。StartProcess函数是一个低水平的接口。os/exec包提供了高水平的接口,应该尽量使用该包。如果出错,错误的底层类型会是*PathError。

查标准库文档,exec.Cmd结构体的说明如下,功能非常的强大,有很多属性可以定制。

type Cmd struct {
    // Path是将要执行的命令的路径。
    //
    // 该字段不能为空,如为相对路径会相对于Dir字段。
    Path string
    // Args保管命令的参数,包括命令名作为第一个参数;如果为空切片或者nil,相当于无参数命令。
    //
    // 典型用法下,Path和Args都应被Command函数设定。
    Args []string
    // Env指定进程的环境,如为nil,则是在当前进程的环境下执行。
    Env []string
    // Dir指定命令的工作目录。如为空字符串,会在调用者的进程当前目录下执行。
    Dir string
    // Stdin指定进程的标准输入,如为nil,进程会从空设备读取(os.DevNull)
    Stdin io.Reader
    // Stdout和Stderr指定进程的标准输出和标准错误输出。
    //
    // 如果任一个为nil,Run方法会将对应的文件描述符关联到空设备(os.DevNull)
    //
    // 如果两个字段相同,同一时间最多有一个线程可以写入。
    Stdout io.Writer
    Stderr io.Writer
    // ExtraFiles指定额外被新进程继承的已打开文件流,不包括标准输入、标准输出、标准错误输出。
    // 如果本字段非nil,entry i会变成文件描述符3+i。
    //
    // BUG: 在OS X 10.6系统中,子进程可能会继承不期望的文件描述符。
    // http://golang.org/issue/2603
    ExtraFiles []*os.File
    // SysProcAttr保管可选的、各操作系统特定的sys执行属性。
    // Run方法会将它作为os.ProcAttr的Sys字段传递给os.StartProcess函数。
    SysProcAttr *syscall.SysProcAttr
    // Process是底层的,只执行一次的进程。
    Process *os.Process
    // ProcessState包含一个已经存在的进程的信息,只有在调用Wait或Run后才可用。
    ProcessState *os.ProcessState
    // 内含隐藏或非导出字段
}

exec.Cmd相关的方法也有不少,后面我们将使用这些属性和方法完成让go程序后台运行的目标。

type Cmd
func Command(name string, arg ...string) *Cmd
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
func (c *Cmd) Run() error
func (c *Cmd) Start() error
func (c *Cmd) Wait() error
func (c *Cmd) Output() ([]byte, error)
func (c *Cmd) CombinedOutput() ([]byte, error)

5.尝试让go程序后台运行

5.1 go调用普通外部程序

我们比较常用的是exec.Command()方法,如以下例子是阻塞调用外部shell命令,并获得命令的执行结果输出

//示例:shell.go

package main

import (
    "fmt"
    "os/exec"
    "bytes"
)

func main() {
    str, err := execShell("ls -l /|head -n 3")
    fmt.Println(err)
    fmt.Println(str)
}

//@link https://www.zhihu.com/people/zh-five
func execShell(s string) (string, error) {
    //这里是一个小技巧, 以'/bin/bash -c xxx'的方式调用shell命令, 则可以在命令中使用管道符,组合多个命令
    cmd := exec.Command("/bin/bash""-c", s)
    var out bytes.Buffer
    cmd.Stdout = &out //把执行命令的标准输出定向到out
    cmd.Stderr = &out //把命令的错误输出定向到out

    //启动一个子进程执行命令,阻塞到子进程结束退出
    err := cmd.Run()
    if err != nil {
        return "", err
    }

    return out.String(), err
}

执行go run shell.go运行程序后,我的MAC系统输出了:

$ go run shell.go
<nil>
total 21
drwxrwxr-x+ 109 root admin 3488 6 4 16:17 Applications
drwxr-xr-x+ 69 root wheel 2208 12 22 23:19 Library

以上例子执行的是shell命令ls \-l /|head \-n 3,其实这个命令可以是任何可以在命令终端执行的程序。那么若这个程序是go程序自身,那就相当于是fork了一个子进程。后续我们将尝试完成这个工作。

5.2 go程序调用自身转为后台运行

我们调用自身程序成功后,是希望子进程可以独自运行,然后父进程退出。这与上面调用外部程序的例子有几点不一样了:

  • 调用自身程序时,父进程不能以阻塞的方式进行了。因为若阻塞了,那就无法提前退出了
  • 父进程不能等待获取子进程的结果输出了,同样是为了提前退出

非阻塞问题:查标准库,exec.Cmd是可以使用func (c *Cmd) Start() error非阻塞式运行外部程序的。若启动外部程序成功则返回nil,否则返回错误信息。

子进程的结果输出问题:查看本文之前引用的标准库文档exec.Cmd的两个属性StdoutStderr(标准输出和错误输出)都是io.Writer接口。那么我们就可以把标准输出和错误输出定向到日志文件中。当然若不需要,也可以不用设置StdoutStderr两个属性,系统将抛弃子进程标准输出和错误输出的信息。

按以上的解决方案,我代码修改一下,用于启动go程序自身

// !!! 切勿运行此程序 !!!
//示例:self.go

package main

import (
    "log"
    "os"
    "os/exec"
)

func main() {
    background("/tmp/daemon.log")
}

func background(logFile string) error {
    //os.Args 是一个切片,保管了命令行参数,第一个是程序名
    //go程序启动时不包含管道符了,就直接运行了
    cmd := exec.Command(os.Args[0], os.Args[1:]...)

    //若有日志文件, 则把子进程的输出导入到日志文件
    if logFile != "" {
        stdout, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
        if err != nil {
            log.Println(os.Getpid(), ": 打开日志文件错误:", err)
            return err
        }
        cmd.Stderr = stdout
        cmd.Stdout = stdout
    }

    //异步启动子进程
    err := cmd.Start()
    if err != nil {
        return err
    }

    return nil
}

以上代码粗看起来好像没有问题,但仔细一想,会存在一个问题:启动的子进程也会执行background()方法,再次启动一个子进程。如此循环,会不断的创建子进程。也就是说以上例子里,代码无法判断自身是父进程还是子进程。

解决怎么区分父进程子进程的问题

为了区分子进程父进程,大多数开源库的解决方案是设置特殊的参数。这种方案是入侵式的,新设置的参数,有可能和go程序原有参数冲突。虽然设置一些奇怪的参数名来降低冲突概率,但至少在使用过程中,并非完全保持参数原样启动子进程,可能会造成使用者的迷惑。这种方案不太完美,先舍弃。

前文提过github.com/sevlyar/go-daemon是巧妙的使用了环境变量,用来区分子进程和父进程。这种方案对go程序影响更小,产生冲突的可能性更小,也避免了使用者对参数变化的迷惑。其原理是利用的是exec.CmdEnv属性设置子进程的环境变量时,添加一个特殊的环境变量,用以标记子程序。用这个思路,我们把上面的例子修正一下。模仿C语言里的fork,返回一个可用用于判断是子进程还是父进程的数据。

//示例:self1.go

package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "time"
)

func main() {
    cmd, err := background("/tmp/daemon.log")
    if err != nil {
        log.Fatal("启动子进程失败:", err)
    }

    //根据返回值区分父进程子进程
    if cmd != nil { //父进程
        log.Println("我是父进程:", os.Getpid(), "; 启动了子进程:", cmd.Process.Pid, "; 运行参数", os.Args)
        return //父进程退出
    } else { //子进程
        log.Println("我是子进程:", os.Getpid(), "; 运行参数:",os.Args)
    }

    //以下代码只有子进程会执行
    log.Println("只有子进程会运行:", os.Getpid(), "; 开始...")
    time.Sleep(time.Second * 20//休眠20秒
    log.Println("只有子进程会运行:", os.Getpid(), "; 结束")
}

//@link https://www.zhihu.com/people/zh-five
func background(logFile string) (*exec.Cmd, error) {
    envName := "XW_DAEMON" //环境变量名称
    envValue := "SUB_PROC" //环境变量值

    val := os.Getenv(envName) //读取环境变量的值,若未设置则为空字符串
    if val == envValue {      //监测到特殊标识, 判断为子进程,不再执行后续代码
        return nilnil
    }

    /*以下是父进程执行的代码*/

    //因为要设置更多的属性, 这里不使用`exec.Command`方法, 直接初始化`exec.Cmd`结构体
    cmd := &exec.Cmd{
        Path: os.Args[0],
        Args: os.Args, //注意,此处是包含程序名的
        Env:  os.Environ(), //父进程中的所有环境变量
    }

    //为子进程设置特殊的环境变量标识
    cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envName, envValue))

    //若有日志文件, 则把子进程的输出导入到日志文件
    if logFile != "" {
        stdout, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
        if err != nil {
            log.Println(os.Getpid(), ": 打开日志文件错误:", err)
            return nil, err
        }
        cmd.Stderr = stdout
        cmd.Stdout = stdout
    }

    //异步启动子进程
    err := cmd.Start()
    if err != nil {
        return nil, err
    }

    return cmd, nil
}

注意此例子不建议使用go run直接运行,因为go run会先编译可执行文件到一个临时目录,然后再运行,其执行输出可能会有些让人迷惑。建议先编译为可执行文件后执行

#编译
$ go build self1.go

#随便设置一些参数,查看执行效果
$ ./self1 -a -b
2020/06/05 19:05:44 我是父进程: 37886 ; 启动了子进程: 37887 ; 运行参数 [./self1 -a -b]

#查看子进程 37887
$ ps -ef |grep self1
501 37887 1 0 7:05下午 ttys003 0:00.01 ./self1 -a -b

#查看子进程输出日志
$ tail /tmp/daemon.log
2020/06/05 19:05:44 我是子进程: 37887 ; 运行参数: [./self1 -a -b]
2020/06/05 19:05:44 只有子进程会运行: 37887 ; 开始...
2020/06/05 19:06:04 只有子进程会运行: 37887 ; 结束

从日志输出看,我们成功的把go程序已自身转为了一个后台运行的子进程,并且子进程的运行参数和父进程完全一样。

如此,我们的目标可以说是完成了一半。接下来是尝试完成守护进程的功能,肯定会涉及到的一个问题是:在子进程中再次启动子进程。那我们思考一下:现在的background方法可以完成此工作吗?答案是不能,我们还需继续优化。

5.3 如何在子进程中再次启动子进程

我们若在子进程中调用background方法,会发现是无法启动新的子进程的。原因是不管第几次调用background方法,环境变量的判断结果都是一样。有两个因素并没有考虑到:

  • 第几次调用background
  • 当前子进程是第几代子进程

结合这两个因素,我们似乎可以设计出一个判断策略,background知道什么时候我该启动子进程,什么时候不该启动。我们设计一个变量ruuIdx记录调用background的次数,启动子进程时把此计数写入到子进程的环境变量中,用于标记此进程是第几代子进程(envIdx)。显然,在子进程中,若runIdx等于envIdx时,那父进程正是调用了此次的background而启动了这个子进程。推导判断一下其它情况,可制定完成的策略如下:

  • runIdx = envIdx时:代表意义如上所述,不启动子进程
  • runIdx < envIdx时:表示是启动前几代子进程的调用,不启动子进程
  • runIdx > envIdx时:表示需要启动新启动一个子进程

按此思路继续改进background

//示例:self2.go

package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "strconv"
    "time"
)

func main() {
    logFile := "/tmp/daemon.log"
    background(logFile, true//启动子进程后退出
    background(logFile, true//启动子进程后退出
    background(logFile, true//启动子进程后退出

    //以下代码只有最后一代子进程会执行
    log.Println(os.Getpid(), "业务代码开始...")
    time.Sleep(time.Second * 20//休眠20秒
    log.Println(os.Getpid(), "业务代码结束")
}

var runIdx int = 0               //background调用计数
const ENV_NAME = "XW_DAEMON_IDX" //环境变量名

//@link https://www.zhihu.com/people/zh-five
func background(logFile string, isExit bool) (*exec.Cmd, error) {
    //判断子进程还是父进程
    runIdx++
    envIdx, err := strconv.Atoi(os.Getenv(ENV_NAME))
    if err != nil {
        envIdx = 0
    }
    if runIdx <= envIdx { //子进程, 退出
        return nilnil
    }

    /*以下是父进程执行的代码*/

    //因为要设置更多的属性, 这里不使用`exec.Command`方法, 直接初始化`exec.Cmd`结构体
    cmd := &exec.Cmd{
        Path: os.Args[0],
        Args: os.Args,      //注意,此处是包含程序名的
        Env:  os.Environ(), //父进程中的所有环境变量
    }

    //为子进程设置特殊的环境变量标识
    cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", ENV_NAME, runIdx))

    //若有日志文件, 则把子进程的输出导入到日志文件
    if logFile != "" {
        stdout, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
        if err != nil {
            log.Println(os.Getpid(), ": 打开日志文件错误:", err)
            return nil, err
        }
        cmd.Stderr = stdout
        cmd.Stdout = stdout
    }

    //异步启动子进程
    err = cmd.Start()
    if err != nil {
        log.Println(os.Getpid(), "启动子进程失败:", err)
        return nil, err
    } else {
        //执行成功
        log.Println(os.Getpid(), ":""启动子进程成功:""->", cmd.Process.Pid, "\n ")
    }

    //若启动子进程成功, 父进程是否直接退出
    if isExit {
        os.Exit(0)
    }

    return cmd, nil
}

编译后执行

#编译
$ go build self2.go

#随便设置一些参数执行
$ ./self2 -a -b -c 123
2020/06/05 19:58:27 38984 : 启动子进程成功: -> 38985

#查看进程,看到的是最终子进程
$ ps -ef |grep self2
501 38990 1 0 7:58下午 ttys003 0:00.01 ./self2 -a -b -c 123

#查看日志
$ tail /tmp/daemon.log
2020/06/05 19:58:27 38985 : 启动子进程成功: -> 38988

2020/06/05 19:58:27 38988 : 启动子进程成功: -> 38990

2020/06/05 19:58:28 38990 业务代码开始...
2020/06/05 19:58:48 38990 业务代码结束

由日志可以看出,成功的启动了3代子进程:38984(父进程)-> 38985 -> 38988 -> 38990。最终的38990子进程执行了业务代码。

注意:此种策略判断的前提条件是,逐代启动子进程。若某进程里重复启动了多个子进程,那么其子进程若想再启动子进程,可能会失败。如以下例子

//非逐代启动子进程的异常情况
func main() {
    logFile := "/tmp/daemon.log"
    cmd,err := background(logFile, false)//启动子进程后不自动退出
    if err != nil {
        log.Fatal("启动子进程失败:", err)
    }

    //根据返回值区分父进程子进程
    if cmd != nil { //父进程
        //父进程再次启动一个子进程, 非逐代启动了
        background(logFile, true//启动子进程后退出
        return //父进程退出
    }

    //父进程里第2次启动的子进程, 此处调用出现异常情况: 将不会启动子进程,而会直接略过执行后面的代码
    background(logFile, true//启动子进程后退出

    //以下代码只有最后一代子进程会执行
    log.Println(os.Getpid(), "业务代码开始...")
    time.Sleep(time.Second * 20//休眠20秒
    log.Println(os.Getpid(), "业务代码结束")
}

执行结果为

#编译
$ go build self2.go

#执行。启动了两个子进程,注意第2此启动39291进程将有异常
$ ./self2 -a -b -c 123
2020/06/05 20:16:58 39289 : 启动子进程成功: -> 39290

2020/06/05 20:16:58 39289 : 启动子进程成功: -> 39291

#查看进程
$ ps -ef |grep self2
501 39291 1 0 8:16下午 ttys003 0:00.01 ./self2 -a -b -c 123
501 39292 1 0 8:16下午 ttys003 0:00.01 ./self2 -a -b -c 123

#查看日志。主要只有39290再次启动了子进程,而39291则直接执行了业务代码
$ tail /tmp/daemon.log
2020/06/05 20:16:58 39290 : 启动子进程成功: -> 39292

2020/06/05 20:16:58 39291 业务代码开始...
2020/06/05 20:16:58 39292 业务代码开始...
2020/06/05 20:17:18 39291 业务代码结束
2020/06/05 20:17:18 39292 业务代码结束

若是重复启动的子进程不再启动子进程,则无影响。后续守护进程的实现,会有这种情况。

6.守护进程的实现

查标准库中有一个func (c *Cmd) Wait() error方法,可以阻塞等待子进程执行结束。守护进程的逻辑就是启动一个子进程(处理业务逻辑,可称为业务进程),然后Wait()住。若子进程退出了,则Wait()解除阻塞,再次重复一次之前的步骤。如此循环,则相当于守护了一个业务进程常驻内存,保证服务的持续性。

以下是示例代码

//守护进程的实现, 基于之前的 background() 。可以替换示例self2.go中的main()函数进行测试
func main(){
    logFile := "/tmp/daemon.log"

    //启动一个子进程作为守护进程
    background(logFile, true//启动子进程后退出

    //在守护进程中循环启动子进程
    for{
        cmd,err := background(logFile, false)//启动子进程后不自动退出
        if err != nil {
            log.Fatal("启动子进程失败:", err)
        }

        //根据返回值区分父进程子进程
        if cmd != nil { //父进程
            cmd.Wait() //等等子进程执行结束(监视子进程)
        } else { //子进程, 跳出让其执行后续业务代码
            break
        }
    }

    //以下是业务代码
    log.Println(os.Getpid(), "业务代码开始...")
    time.Sleep(time.Second * 20//休眠20秒
    log.Println(os.Getpid(), "业务代码结束")

}

执行结果为:

#编译
$ go build self2.go

#执行,启动的守护进程为 39541
$ ./self2 -a -b -c 123
2020/06/05 20:36:05 39540 : 启动子进程成功: -> 39541

#查看进程。可以看出,业务进程是39543,其父进程是39541
$ ps -ef |grep self2
501 39541 1 0 8:36下午 ttys003 0:00.01 ./self2 -a -b -c 123
501 39543 39541 0 8:36下午 ttys003 0:00.01 ./self2 -a -b -c 123

#查看日志。可以看到业务进程39543退出后,守护进程及时的启动了另一个业务进程39574
$ tail /tmp/daemon.log
2020/06/05 20:36:05 39541 : 启动子进程成功: -> 39543

2020/06/05 20:36:05 39543 业务代码开始...
2020/06/05 20:36:25 39543 业务代码结束
2020/06/05 20:36:25 39541 : 启动子进程成功: -> 39574

2020/06/05 20:36:25 39574 业务代码开始...

到此,守护进程的功能已经实现了。但作为一个库,对使用者还不太友好,我们需要封装一下。并且结合业务场景似乎还有一些细节问题需要考虑一下:

  • 一个正常服务进程一般不会异常退出,可能并不需要无限的循环重启,这可以让使用者自定义最大重启次数
  • 若业务进程连续不断的异常退出,是不应该继续不断重启了。可设置一个允许的最大连续异常退出次数
  • 实际编写的服务程序,异常退出时不一定退出码就是非0。可以设置一个最短运行时间,协助判断是否是异常退出

最后封装为xdaemon库,开源在https://github.com/zh-five/xdaemon

其核心代码如下

package xdaemon

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "strconv"
    "time"
)

const ENV_NAME = "XW_DAEMON_IDX"

//运行时调用background的次数
var runIdx int = 0

//守护进程
type Daemon struct {
    LogFile     string //日志文件, 记录守护进程和子进程的标准输出和错误输出. 若为空则不记录
    MaxCount    int    //循环重启最大次数, 若为0则无限重启
    MaxError    int    //连续启动失败或异常退出的最大次数, 超过此数, 守护进程退出, 不再重启子进程
    MinExitTime int64  //子进程正常退出的最小时间(秒). 小于此时间则认为是异常退出
}

// 把本身程序转化为后台运行(启动一个子进程, 然后自己退出)
// logFile 若不为空,子程序的标准输出和错误输出将记入此文件
// isExit  启动子加进程后是否直接退出主程序, 若为false, 主程序返回*os.Process, 子程序返回 nil. 需自行判断处理
func Background(logFile string, isExit bool) (*exec.Cmd, error) {
    //判断子进程还是父进程
    runIdx++
    envIdx, err := strconv.Atoi(os.Getenv(ENV_NAME))
    if err != nil {
        envIdx = 0
    }
    if runIdx <= envIdx { //子进程, 退出
        return nilnil
    }

    //设置子进程环境变量
    env := os.Environ()
    env = append(env, fmt.Sprintf("%s=%d", ENV_NAME, runIdx))

    //启动子进程
    cmd, err := startProc(os.Args, env, logFile)
    if err != nil {
        log.Println(os.Getpid(), "启动子进程失败:", err)
        return nil, err
    } else {
        //执行成功
        log.Println(os.Getpid(), ":""启动子进程成功:""->", cmd.Process.Pid, "\n ")
    }

    if isExit {
        os.Exit(0)
    }

    return cmd, nil
}

func NewDaemon(logFile string) *Daemon {
    return &Daemon{
        LogFile:     logFile,
        MaxCount:    0,
        MaxError:    3,
        MinExitTime: 10,
    }
}

// 启动后台守护进程
func (d *Daemon) Run() {
    //启动一个守护进程后退出
    Background(d.LogFile, true)

    //守护进程启动一个子进程, 并循环监视
    var t int64
    count := 1
    errNum := 0
    for {
        //daemon 信息描述
        dInfo := fmt.Sprintf("守护进程(pid:%d; count:%d/%d; errNum:%d/%d):",
            os.Getpid(), count, d.MaxCount, errNum, d.MaxError)
        if errNum > d.MaxError {
            log.Println(dInfo, "启动子进程失败次数太多,退出")
            os.Exit(1)
        }
        if d.MaxCount > 0 && count > d.MaxCount {
            log.Println(dInfo, "重启次数太多退出")
            os.Exit(0)
        }
        count++

        t = time.Now().Unix() //启动时间戳
        cmd, err := Background(d.LogFile, false)
        if err != nil { //启动失败
            log.Println(dInfo, "子进程启动失败;""err:", err)
            errNum++
            continue
        }

        //子进程,
        if cmd == nil {
            log.Printf("子进程pid=%d: 开始运行...", os.Getpid())
            break
        }

        //父进程: 等待子进程退出
        err = cmd.Wait()
        dat := time.Now().Unix() - t //子进程运行秒数
        if dat < d.MinExitTime {     //异常退出
            errNum++
        } else { //正常退出
            errNum = 0
        }
        log.Printf("%s 监视到子进程(%d)退出, 共运行了%d秒: %v\n", dInfo, cmd.ProcessState.Pid(), dat, err)
    }
}

func startProc(args, env []string, logFile string) (*exec.Cmd, error) {
    cmd := &exec.Cmd{
        Path:        args[0],
        Args:        args,
        Env:         env,
        SysProcAttr: NewSysProcAttr(),
    }

    if logFile != "" {
        stdout, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
        if err != nil {
            log.Println(os.Getpid(), ": 打开日志文件错误:", err)
            return nil, err
        }
        cmd.Stderr = stdout
        cmd.Stdout = stdout
    }

    err := cmd.Start()
    if err != nil {
        return nil, err
    }

    return cmd, nil
}

xdaemon库的使用示例

background模式

//本示例, 将把进程转为后台运行, 并保留所有参数不变

package main

import (
    "github.com/zh-five/xdaemon"
    "log"
    "os"
    "time"
)

func main() {
    logFile := "daemon.log"

    //启动一个子进程后主程序退出
    xdaemon.Background(logFile, true)

    //以下代码只有子程序会执行
    log.Println(os.Getpid(), "start...")
    time.Sleep(time.Second * 10)
    log.Println(os.Getpid(), "end")
}

daemon模式

//本示例, 将启动一个后台运行的守护进程. 然后由守护进程启动和维护最终子进程

package main

import (
    "github.com/zh-five/xdaemon"
    "flag"
    "log"
    "os"
    "time"
)

func main() {
    d := flag.Bool("d"false"是否后台守护进程方式运行")
    flag.Parse()

    //启动守护进程
    if *d {
        //创建一个Daemon对象
        logFile := "daemon.log"
        d := xdaemon.NewDaemon(logFile)
        //调整一些运行参数(可选)
        d.MaxCount = 2 //最大重启次数

        //执行守护进程模式
        d.Run()
    }

    //当 *d = true 时以下代码只有最终子进程会执行, 主进程和守护进程都不会执行
    log.Println(os.Getpid(), "start...")
    time.Sleep(time.Second * 10)
    log.Println(os.Getpid(), "end")

}


 - EOF -

推荐阅读(点击标题可打开)

1、IO多路复用与Go网络库的实现

2、Go语言错误处理的优雅实现

3、使用Go构建Kubernetes应用

Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

关注后获取

回复 Go 获取6万star的Go资源库



分享、点赞和在看

支持我们分享更多好文章,谢谢!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存