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

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

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.StartProcess或exec.Cmd。 os.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

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的两个属性Stdout和Stderr(标准输出和错误输出)都是io.Writer接口。那么我们就可以把标准输出和错误输出定向到日志文件中。当然若不需要,也可以不用设置Stdout和Stderr两个属性,系统将抛弃子进程标准输出和错误输出的信息。

按以上的解决方案,我代码修改一下,用于启动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.Cmd的Env属性设置子进程的环境变量时,添加一个特殊的环境变量,用以标记子程序。用这个思路,我们把上面的例子修正一下。模仿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 nil, nil
}

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

//因为要设置更多的属性, 这里不使用`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 nil, nil
}

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

//因为要设置更多的属性, 这里不使用`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 nil, nil
}

//设置子进程环境变量
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")

}
有bug或建议,欢迎联系我。本博客留言或https://github.com/zh-five/xdaemon都行。写得有点长了,感谢您的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值