golang实现守护进程(2)
创始人
2025-06-01 02:40:54

前言

golang实现守护进程,包含功能:

1. 守护进程只创建一次

2. 平滑创建业务进程

3. 业务进程挂起,守护进程能监听,并重启新启业务进程

4. 守护进程退出,也能保证业务进程退出

5. 业务进程≈子进程

6. 不影响业务进程逻辑

7. 以Linux平台为主,其他平台暂时没有实施条件

分析

上一篇博文讨论过如何以脚本的形式创建守护进程,这篇讨论如何以纯golang脚本实现守护进程的功能

  • 在 Unix 中,创建一个进程,通过系统调用 fork 实现(及其一些变种,如 vfork、clone)。

  • 在 Go 语言中,Linux 下创建进程使用的系统调用是 clone 。

在 C 语言中,通常会用到 2 种创建进程方法:

  1. fork

pid = fork();
//pid > 0 父进程
//pid = 0 子进程
//pid < 0 出错

程序会从 fork 处一分为二,父进程返回值大于0,并继续运行;子进程获得父进程的栈、数据段、堆和执行文本段的拷贝,返回值等于0,并向下继续运行。通过 fork 返回值可轻松判断当前处于父进程还是子进程。

  1. execve

execve(pathname, argv, envp);
//pathname 可执行文件路径
//argv 参数列表
//envp 环境变量列表

execve 为加载一个新程序到当前进程的内存,这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。

  • 在 Go 语言中,创建进程方法主要有 3 种:

  1. exec.Command

//判 断当其是否是子进程,当父进程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.Stdoutcmd.Stderr = os.Stderrcmd.Start() // 开始执行新进程,不等待新进程退出os.Exit(0)
}
  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)
}

方法1和方法2通过 os.Getppid()!=1进行判断是否子进程,默认父进程退出之后,子进程会被1号进程接管。

但据Ubuntu Desktop 本地测试,接管孤儿进程的并不是1号进程,因此考虑到程序稳定性和兼容性,不能够以 ppid 作为判断父子进程的依据。

方法3直接进行了系统调用,虽然可以通过 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// 创建监听退出chanc := make(chan os.Signal)// 监听指定信号 ctrl+c killsignal.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, "")// step2if !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.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrstrerr := 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.PidisDae, 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...")}}}// step3join = 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.Pidexist, 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 ------------------------// ------------------------ 业务进程 start ------------------------fmt.Println("hello, welcome to business detail!")}

相关工具方法:

package utilsimport ("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 stringif 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,走创建守护进程代码分支,执行创建守护进程,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,走创建业务进程分支,执行创建业务进程,exec.Command(./go_start -c param1 -d param2 -e param3);此时守护进程存在,每隔5秒监听一次业务进程是否存在,如果存在则不操作;不存在则重新执行创建业务进程exec.Command(./go_start -c param1 -d param2 -e param3);

4、执行具体的业务进程逻辑

验证

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   -- ③
  1. 刚开始会出现三个进程,假设进程id如上,一会之后①会消失,这是正常的,因为刚开始的启动就是①,然后只剩下进程②和③

]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 ./go_start -business -c param1 -d param2 -c param3   -- ③
  1. 验证kill业务进程:会启动新的业务进程,守护进程不变;所以执行:kill 112

]$ 111  1   ./go_start -c param1 -d param2 -c param3 -daemon     -- ②
]$ 112  111 [go_start]                                  -- ③'
]$ 113  111 ./go_start -business -c param1 -d param2 -c param3   -- ③
  1. 这里kill 112后,会出现一个僵尸进程,不影响实际业务进程的创建和运行,不需要理会;假设新创建的业务进程pid为113

  1. 验证kill守护进程:整个程序退出,也就是执行ps -ef | grep go_start后,没有对应的守护进程和业务进程,同时僵尸进程也会消失;得益于以下代码,进程组

command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

综上

纯golang语言形式实现了守护进程,针对启动业务进程,优化点:可以使用go func(){}()协程方式启动更优雅,这里先不实施,待后续有空改进;

缺点:依然要通过参数控制守护进程和业务进程,-daemon -business,期望统一起来,不用参数控制

放在(3)实现

附录

以下是关于信号量的一个记录,当作参考文档

信号

动作

说明

SIGHUP

1

Term

终端控制进程结束(终端连接断开)

SIGINT

2

Term

用户发送INTR字符(Ctrl+C)触发

SIGQUIT

3

Core

用户发送QUIT字符(Ctrl+/)触发

SIGILL

4

Core

非法指令(程序错误、试图执行数据段、栈溢出等)

SIGABRT

6

Core

调用abort函数触发

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

继续执行已经停止的进程(不能被阻塞)

SIGSTOP

17,19,23

Stop

停止进程(不能被捕获、阻塞或忽略) SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略) SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发 SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

相关内容

热门资讯

16K纸有多大比A4的大还是小... 今天给各位分享16K纸有多大比A4的大还是小的知识,其中也会对16k的纸是a4纸的一半吗进行解释,如...
许昌新车上牌地址(许昌新车上牌... 今天给各位分享许昌新车上牌地址的知识,其中也会对许昌新车上牌地址查询进行解释,如果能碰巧解决你现在面...
八大艺术有哪些,艺术的八大类分... 八大艺术有哪些目录八大艺术有哪些艺术的八大类分别是什么世界公认的七大艺术是什么?什么被称八大艺术八大...
离我最近的北京现代4s店(离我... 本篇文章极速百科给大家谈谈离我最近的北京现代4s店,以及离我最近的北京现代4s店保养车对应的知识点,...
导轨油和机油的区别?(导轨油和... 今天给各位分享导轨油和机油的区别?的知识,其中也会对导轨油和机油的区别是什么进行解释,如果能碰巧解决...
免检卡板有什么好的,与熏蒸卡板... 本篇文章极速百科给大家谈谈免检卡板有什么好的,与熏蒸卡板有什么区别呢?,以及免检卡板图片对应的知识点...
东风雪铁龙c3xr怎么样(20... 本篇文章极速百科给大家谈谈东风雪铁龙c3xr怎么样,以及2021款东风雪铁龙c3xr对应的知识点,希...
圣诞节的意义(圣诞节的意义讲章... 本篇文章极速百科给大家谈谈圣诞节的意义,以及圣诞节的意义讲章对应的知识点,希望对各位有所帮助,不要忘...
c中getchar表示什么,g... c中getchar表示什么目录c中getchar表示什么getchar在c是什么意思getchar在...
军考网上哪里报名,直招军官的招... 军考网上哪里报名目录军考网上哪里报名直招军官的招录程序是什么?怎样在网上报名考兵2020军校报名时间...
微信红包祝福语8字,发红包祝福... 微信红包祝福语8字目录微信红包祝福语8字发红包祝福语八个字过年红包祝福语微信红包祝福语8字 微信红包...
王字的三横一竖是什么意思,三横... 王字的三横一竖是什么意思目录王字的三横一竖是什么意思三横一竖是什么字?王字的含义是?王的含义是什么王...
精英危险在哪买,精英危险怎么买... 精英危险在哪买目录精英危险在哪买精英危险怎么买船精英危险如何挖矿2022年精英危险值得购买吗精英危险...
尼桑两厢车有哪几款(尼桑两厢车... 本篇文章极速百科给大家谈谈尼桑两厢车有哪几款,以及尼桑两厢车有哪几款红色对应的知识点,希望对各位有所...
端午风俗有哪些传统,端午节有哪... 端午风俗有哪些传统目录端午风俗有哪些传统端午节有哪些习俗?有哪些寓意?端午节的习俗有哪些?端午节的风...
世界十大凶猛龟是哪些,十大凶猛... 世界十大凶猛龟是哪些目录世界十大凶猛龟是哪些十大凶猛龟排名是怎样的?最凶猛的乌龟有哪几种,最猛的三种...
简短情话8个字,很甜很撩的句子... 简短情话8个字目录简短情话8个字很甜很撩的句子八个字 又甜又撩的短句八个字8字爱情名言佳句用八个字形...
高考数学满分多少,高考数学总分... 高考数学满分多少目录高考数学满分多少高考数学总分多少分满分高中考试各科分数是多少?高考数学满分是多少...
吃鸡攻略和技巧手游,手游吃鸡怎... 吃鸡攻略和技巧手游目录吃鸡攻略和技巧手游手游吃鸡怎么练技术绝地求生大逃杀怎么玩 绝地求生各系统玩法技...
开了5年的帝豪后,我换成了奥迪... 今天给各位分享开了5年的帝豪后,我换成了奥迪A4L的知识,其中也会对进行解释,如果能碰巧解决你现在面...
ecco什么牌子,ecco什么... ecco什么牌子目录ecco什么牌子ecco什么牌子 ecco是哪个国家的品牌eoco是什么牌子ec...
汽车内外饰设计-仪表板设计构想... 今天给各位分享汽车内外饰设计-仪表板设计构想-连载05的知识,其中也会对仪表盘内饰进行解释,如果能碰...
酱油生抽老抽有什么区别,生抽老... 酱油生抽老抽有什么区别目录酱油生抽老抽有什么区别生抽老抽酱油有啥区别酱油、生抽、老抽有什么不同?老抽...
北京车牌外地违章怎么处理(北京... 本篇文章极速百科给大家谈谈北京车牌外地违章怎么处理,以及北京牌外地违章本地能处理吗对应的知识点,希望...
13寸电脑尺寸是多少厘米,13... 13寸电脑尺寸是多少厘米目录13寸电脑尺寸是多少厘米13英寸等于多少厘米13寸大概有多大?13.3寸...
管理员的英文缩写是什么,管理员... 管理员的英文缩写是什么目录管理员的英文缩写是什么管理员英文怎么写admn具体指的是什么?管理员的英文...
为什么喊张艺兴孙艺兴,张艺兴为... 为什么喊张艺兴孙艺兴目录为什么喊张艺兴孙艺兴张艺兴为什么叫孙兴?,孙艺兴和张艺兴是同一个人吗为什么喊...
肉沫土豆泥的做法,土豆泥怎么做... 肉沫土豆泥的做法目录肉沫土豆泥的做法土豆泥怎么做?土豆泥怎么做好吃,肉末土豆泥的家常做法土豆打成泥怎...
洛阳市区有哪些大学,洛阳有几所... 洛阳市区有哪些大学目录洛阳市区有哪些大学洛阳有几所大学求洛阳各大高校地址洛阳的大学有哪些洛阳市区有哪...
霎时间的近义词是什么,“ 霎时... 霎时间的近义词是什么目录霎时间的近义词是什么“ 霎时间 ”的近义词与“霎时间”意思相近的词有什么?霎...