golang 守護進程(daemon)實例——后台運行,重啟進程


需求

  1. -daemon功能:為任意 Go 程序創建守護進程,使 Go 業務進程脫離終端運行;
  2. -forever功能:創建監控重啟進程,使 Go 業務進程被殺死后能夠重啟;
  3. 不影響業務進程邏輯;
  4. 以Linux平台為主,其他平台暫不考慮。

分析

創建守護進程首先要了解go語言如何實現創建進程。在 Unix 中,創建一個進程,通過系統調用 fork 實現(及其一些變種,如 vforkclone)。在 Go 語言中,Linux 下創建進程使用的系統調用是 clone

在 C 語言中,通常會用到 2 種創建進程方法:

  1. fork
pid = fork();
//pid > 0 父進程
//pid = 0 子進程
//pid < 0 出錯

程序會從 fork 處一分為二,父進程返回值大於0,並繼續運行;子進程獲得父進程的棧、數據段、堆和執行文本段的拷貝,返回值等於0,並向下繼續運行。通過 fork 返回值可輕松判斷當前處於父進程還是子進程。

但在 Go 語言中,沒有直接提供 fork 系統調用的封裝,如果想只調用 fork,需要通過 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 實現。

  1. execve
execve(pathname, argv, envp);
//pathname 可執行文件路徑
//argv 參數列表
//envp 環境變量列表

execve 為加載一個新程序到當前進程的內存,這將丟棄現存的程序文本段,並為新程序重新創建棧、數據段以及堆。通常將這一動作稱為執行一個新程序。

在 Go 語言中,創建進程方法主要有 3 種:

  1. exec.Command
package main

import (
    "os"
    "os/exec"
    "path/filepath"
    "time"
)

func main() {
    //判 斷當其是否是子進程,當父進程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)
        // return
    } else {
        // dosomething
        time.Sleep(time.Second * 10)
    }
}
  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)
}

可以參考例子:https://studygolang.com/articles/3597

在分析這個例子的時候,方法1和2針對於如何判斷該進程是否是子進程,例子的方法是 os.Getppid()!=1,也就是默認了父進程退出之后,子進程會被1號進程接管。

但在我 Ubuntu Desktop 本地測試時卻發現,接管孤兒進程的並不是1號進程,因此考慮到程序穩定性和兼容性,不能夠以 ppid 作為判斷父子進程的依據。

方法3直接進行了系統調用,雖然可以通過 pid 進行判斷父子進程,但該方法過於底層,例子中也沒有推薦使用,所以也沒有采納。

1. 守護進程

考慮利用方法1進行進程創建,由於 exec.Command 包含了參數傳遞,可以通過傳入不同的參數,實現判斷啟動守護進程還是啟動業務進程。

func main(){
	daemon := flag.Bool("daemon", false, "run in daemon")
	if *daemon {//父進程,守護進程
		cmd := exec.Command(os.Args[0])
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err := cmd.Start()
		if err != nil {
			fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
		}
		os.Exit()
	} else {//子進程,業務進程
		DoSomething()
	}
}

運行時存在 -daemon 參數,則運行 exec.Command 生成子進程,在傳參時刪掉 -daemon 參數,再次進入 main 時就進入子進程業務邏輯了,這時父進程也退出,子進程就被系統進程接管。

2. 重啟進程

通過上述分析,基本已經實現了守護進程創建,重啟進程就依葫蘆畫瓢了。

forever := flag.Bool("forever", false, "run forever")
if *forever{
	for {
		cmd := exec.Command(args[0])
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err := cmd.Start()
		if err != nil {
			fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
		}
		cmd.Wait()
	}
}

在死循環(for{...})中,創建新的業務進程,通過 cmd.Wait() 等待業務進程退出狀態,如果業務進程退出,則再次循環創建進程。

By the way,重啟進程和守護進程是可以解耦合的,可以單獨判斷參數 -daemon-forever,不必再進行參數個數判斷。

實現

本次實現主要通過方法1進行進程創建,以main函數作為程序入口點,通過傳參數不同,來判斷父子進程,這樣有2個好處:

  1. 參數不同實現啟動不同進程;
  2. 守護進程和重啟進程對業務進程透明,不影響業務進程邏輯。

直接上代碼

go-daemon.go

package main

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

const (
	DAEMON = "daemon"
	FOREVER = "forever"
)

func DoSomething(){
	fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	log.SetOutput(fp)
	for{
		log.Printf("DoSomething running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
		time.Sleep(time.Second * 5)
	}
}

func StripSlice(slice []string, element string) []string {
	for i := 0; i < len(slice); {
		if slice[i] == element && i != len(slice)-1 {
			slice = append(slice[:i], slice[i+1:]...)
		} else if slice[i] == element && i == len(slice)-1 {
			slice = slice[:i]
		} else {
			i++
		}
	}
	return slice
}

func SubProcess(args []string) *exec.Cmd {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Start()
	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
	}
	return cmd
}

func main(){
	daemon := flag.Bool(DAEMON, false, "run in daemon")
	forever := flag.Bool(FOREVER, false, "run forever")
	flag.Parse()
	fmt.Printf("[*] PID: %d PPID: %d ARG: %s\n", os.Getpid(), os.Getppid(), os.Args)
	if *daemon {
		SubProcess(StripSlice(os.Args, "-"+DAEMON))
		fmt.Printf("[*] Daemon running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
		os.Exit(0)
	} else if *forever {
		for {
			cmd := SubProcess(StripSlice(os.Args, "-"+FOREVER))
			fmt.Printf("[*] Forever running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
			cmd.Wait()
		}
		os.Exit(0)
	} else {
		fmt.Printf("[*] Service running in PID: %d PPID: %d\n", os.Getpid(), os.Getppid())
	}
	DoSomething()
}

使用

編譯

go build -ldflags "-s -w" go-daemon.go

運行

./go-daemon -daemon -forever

代碼及參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM