Golang守護進程用法示例分析
前言
golang實現守護進程,包含功能:
1. 守護進程只創(chuàng)建一次
2. 平滑創(chuàng)建業(yè)務進程
3. 業(yè)務進程掛起,守護進程能監(jiān)聽,并重啟新啟業(yè)務進程
4. 守護進程退出,也能保證業(yè)務進程退出
5. 業(yè)務進程≈子進程
6. 不影響業(yè)務進程邏輯
7. 以Linux平臺為主,其他平臺暫時沒有實施條件
分析
上一篇博文討論過如何以腳本的形式創(chuàng)建守護進程,這篇討論如何以純golang腳本實現守護進程的功能
- 在 Unix 中,創(chuàng)建一個進程,通過系統(tǒng)調用 fork 實現(及其一些變種,如 vfork、clone)。
- 在 Go 語言中,Linux 下創(chuàng)建進程使用的系統(tǒng)調用是 clone 。
在 C 語言中,通常會用到 2 種創(chuàng)建進程方法:
fork
pid = fork(); //pid > 0 父進程 //pid = 0 子進程 //pid < 0 出錯
程序會從 fork 處一分為二,父進程返回值大于0,并繼續(xù)運行;子進程獲得父進程的棧、數據段、堆和執(zhí)行文本段的拷貝,返回值等于0,并向下繼續(xù)運行。通過 fork 返回值可輕松判斷當前處于父進程還是子進程。
execve
execve(pathname, argv, envp); //pathname 可執(zhí)行文件路徑 //argv 參數列表 //envp 環(huán)境變量列表
execve 為加載一個新程序到當前進程的內存,這將丟棄現存的程序文本段,并為新程序重新創(chuàng)建棧、數據段以及堆。通常將這一動作稱為執(zhí)行一個新程序。
在 Go 語言中,創(chuàng)建進程方法主要有 3 種:
exec.Command
//判 斷當其是否是子進程,當父進程return之后,子進程會被 系統(tǒng)1 號進程接管
if os.Getppid() != 1 {
// 將命令行參數中執(zhí)行文件路徑轉換成可用路徑
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() // 開始執(zhí)行新進程,不等待新進程退出
os.Exit(0)
}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)
}方法1和方法2通過 os.Getppid()!=1進行判斷是否子進程,默認父進程退出之后,子進程會被1號進程接管。
但據Ubuntu Desktop 本地測試,接管孤兒進程的并不是1號進程,因此考慮到程序穩(wěn)定性和兼容性,不能夠以 ppid 作為判斷父子進程的依據。
方法3直接進行了系統(tǒng)調用,雖然可以通過 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
// 創(chuàng)建監(jiān)聽退出chan
c := make(chan os.Signal)
// 監(jiān)聽指定信號 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 ------------------------
// ------------------------ 業(yè)務進程 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,走創(chuàng)建守護進程代碼分支,執(zhí)行創(chuàng)建守護進程,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,走創(chuàng)建業(yè)務進程分支,執(zhí)行創(chuàng)建業(yè)務進程,exec.Command(./go_start -c param1 -d param2 -e param3);此時守護進程存在,每隔5秒監(jiān)聽一次業(yè)務進程是否存在,如果存在則不操作;不存在則重新執(zhí)行創(chuàng)建業(yè)務進程exec.Command(./go_start -c param1 -d param2 -e param3);
4、執(zhí)行具體的業(yè)務進程邏輯
驗證
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 -- ③
剛開始會出現三個進程,假設進程id如上,一會之后①會消失,這是正常的,因為剛開始的啟動就是①,然后只剩下進程②和③
]$ 111 1 ./go_start -c param1 -d param2 -c param3 -daemon -- ②
]$ 112 111 ./go_start -business -c param1 -d param2 -c param3 -- ③
驗證kill業(yè)務進程:會啟動新的業(yè)務進程,守護進程不變;所以執(zhí)行: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 -- ③
這里kill 112后,會出現一個僵尸進程,不影響實際業(yè)務進程的創(chuàng)建和運行,不需要理會;假設新創(chuàng)建的業(yè)務進程pid為113
驗證kill守護進程:整個程序退出,也就是執(zhí)行ps -ef | grep go_start后,沒有對應的守護進程和業(yè)務進程,同時僵尸進程也會消失;得益于以下代碼,進程組
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
綜上
純golang語言形式實現了守護進程,針對啟動業(yè)務進程,優(yōu)化點:可以使用go func(){}()協(xié)程方式啟動更優(yōu)雅,這里先不實施,待后續(xù)有空改進;
缺點:依然要通過參數控制守護進程和業(yè)務進程,-daemon -business,期望統(tǒng)一起來,不用參數控制
放在(3)實現
附錄
以下是關于信號量的一個記錄,當作參考文檔
| 信號 | 值 | 動作 | 說明 |
| SIGHUP | 1 | Term | 終端控制進程結束(終端連接斷開) |
| SIGINT | 2 | Term | 用戶發(fā)送INTR字符(Ctrl+C)觸發(fā) |
| SIGQUIT | 3 | Core | 用戶發(fā)送QUIT字符(Ctrl+/)觸發(fā) |
| SIGILL | 4 | Core | 非法指令(程序錯誤、試圖執(zhí)行數據段、棧溢出等) |
| SIGABRT | 6 | Core | 調用abort函數觸發(fā) |
| 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 | 繼續(xù)執(zhí)行已經停止的進程(不能被阻塞) |
| SIGSTOP | 17,19,23 | Stop | 停止進程(不能被捕獲、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止進程(可以被捕獲、阻塞或忽略) SIGTTIN 21,21,26 Stop 后臺程序從終端中讀取數據時觸發(fā) SIGTTOU 22,22,27 Stop 后臺程序向終端中寫數據時觸發(fā) |
到此這篇關于Golang守護進程用法示例分析的文章就介紹到這了,更多相關Golang守護進程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Go語言中比較兩個map[string]interface{}是否相等
本文主要介紹了Go語言中比較兩個map[string]interface{}是否相等,我們可以將其轉化成順序一樣的 slice ,然后再轉化未json,具有一定的參考價值,感興趣的可以了解一下2023-08-08
在Visual Studio Code中配置GO開發(fā)環(huán)境的詳細教程
這篇文章主要介紹了在Visual Studio Code中配置GO開發(fā)環(huán)境的詳細教程,需要的朋友可以參考下2017-02-02
Go基礎教程系列之import導入包(遠程包)和變量初始化詳解
這篇文章主要介紹了Go基礎教程系列之import導包和初始化詳解,需要的朋友可以參考下2022-04-04
詳解golang channel有無緩沖區(qū)的區(qū)別
這篇文章主要給大家介紹了golang channel有無緩沖區(qū)的區(qū)別,無緩沖是同步的,有緩沖是異步的,文中通過代碼示例給大家講解的非常詳細,需要的朋友可以參考下2024-01-01

