需求
-daemon
功能:為任意 Go 程序創建守護進程,使 Go 業務進程脫離終端運行;-forever
功能:創建監控重啟進程,使 Go 業務進程被殺死后能夠重啟;- 不影響業務進程邏輯;
- 以Linux平台為主,其他平台暫不考慮。
分析
創建守護進程首先要了解go語言如何實現創建進程。在 Unix 中,創建一個進程,通過系統調用 fork
實現(及其一些變種,如 vfork
、clone
)。在 Go 語言中,Linux 下創建進程使用的系統調用是 clone
。
在 C 語言中,通常會用到 2 種創建進程方法:
fork
pid = fork();
//pid > 0 父進程
//pid = 0 子進程
//pid < 0 出錯
程序會從 fork
處一分為二,父進程返回值大於0,並繼續運行;子進程獲得父進程的棧、數據段、堆和執行文本段的拷貝,返回值等於0,並向下繼續運行。通過 fork
返回值可輕松判斷當前處於父進程還是子進程。
但在 Go 語言中,沒有直接提供 fork
系統調用的封裝,如果想只調用 fork,需要通過 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
實現。
execve
execve(pathname, argv, envp);
//pathname 可執行文件路徑
//argv 參數列表
//envp 環境變量列表
execve
為加載一個新程序到當前進程的內存,這將丟棄現存的程序文本段,並為新程序重新創建棧、數據段以及堆。通常將這一動作稱為執行一個新程序。
在 Go 語言中,創建進程方法主要有 3 種:
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)
}
}
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)
}
可以參考例子: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個好處:
- 參數不同實現啟動不同進程;
- 守護進程和重啟進程對業務進程透明,不影響業務進程邏輯。
直接上代碼
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