Go语言编程练习#2-守护进程


=Start=

缘由:

之前在第一次学习Go 语言的时候其实整理了一篇文章「Linux下守护进程的Golang语言实现」,但因为后续有其他事情,所以就没有继续下去了。最近刚好在复习Go 语言,所以想着再整理实践一下用Go 语言实现守护进程的功能,就有了这篇文章。

正文:

参考解答:

首先,如果你只是想实现类似于守护进程在后台默默运行的功能的话,可以考虑先用 go build 命令构建出可执行程序,然后(二者选其一即可):

  1. 借助 daemonize 或 upstart 将进程以daemon形式运行;
  2. 使用 nohup 结合 & 的方式,再加上 supervisor 管理。

 

如果你还是希望能用Go 语言实现一个daemon进程,可以参考一下这段代码(在Linux环境下测试验证OK,macOS未通过):

package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "syscall"
    "time"
)

func daemon(nochdir, noclose int) int {
    var ret, ret2 uintptr
    var err syscall.Errno
    darwin := runtime.GOOS == "darwin"
    // 如果当前进程的ppid等于1,则表明已经是一个守护进程了,直接返回即可(already a daemon)
    if syscall.Getppid() == 1 {
        return 0
    }
    // 通过 syscall 包调用 fork 派生子进程
    ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
    if err != 0 {
        log.Println("syscall.RawSyscall")
        return -1
    }
    // ret2 表示子进程收到的 fork 返回(暂不完全确定?),正常派生情况下 ret2 应该等于0
    if ret2 < 0 {
        os.Exit(-1)
    }
    // 针对 darwin 系统的错误处理(没啥用,在macOS上还是跑不了)
    if darwin && ret2 == 1 {
        log.Printf("OS: %v, ret: %v, ret2: %v\n", darwin, ret, ret2)
        // ret = 0
    }
    // ret 表示fork成功执行后产生的子进程的pid(由父进程接收)
    if ret > 0 {
        os.Exit(0) // 退出当前的父进程
    }

    // create a new SID for the child process
    s_ret, s_errno := syscall.Setsid()
    if s_errno != nil {
        log.Printf("Error: syscall.Setsid errno: %d", s_errno)
    }
    if s_ret < 0 {
        log.Println("syscall.Setsid")
        return -1
    }

    // 调用 fork 派生子进程
    ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
    if err != 0 {
        log.Println("syscall.RawSyscall")
        return -1
    }
    // ret2 表示子进程收到的 fork 返回,正常派生情况下 ret2 应该等于0
    if ret2 < 0 {
        os.Exit(-1)
    }
    // ret 表示fork成功执行后产生的子进程的pid(由父进程接收)
    if ret > 0 {
        os.Exit(0) // 退出当前的父进程
    }

    if nochdir == 0 {
        os.Chdir("/")
    }
    /* Change the file mode mask */
    _ = syscall.Umask(0)

    if noclose == 0 {
        f, e := os.OpenFile("/dev/null", os.O_RDWR, 0)
        if e == nil {
            fd := f.Fd()
            syscall.Dup2(int(fd), int(os.Stdin.Fd()))
            syscall.Dup2(int(fd), int(os.Stdout.Fd()))
            syscall.Dup2(int(fd), int(os.Stderr.Fd()))
        }
    }
    return 0
}

func main() {
    daemon(0, 1)
    for {
        fmt.Println("hello")
        time.Sleep(2 * time.Second)
    }
}

如果你用C或是其它编程语言开发过daemon进程的话你就会发现,实现daemon的主要逻辑步骤在各个语言中都是一样的:

1、创建子进程,父进程退出 fork()
2、子进程创建新会话 setsid()
3、再次创建子进程结束当前进程 fork() #通过使进程不再是会话首进程来禁止进程重新打开控制终端
4、改变当前工作目录 chdir()
5、重设文件权限掩码 umask()
6、关闭无关文件描述符 close()

只不过在Go 语言中,对我来说,现在实现起来最大的问题在于——不论是Syscall.RawSyscall()还是Syscall.Syscall()它们的资料都太少了,参数、返回值的功能和作用都只能靠猜……


一个进程是否是守护进程的验证方法(判断是否符合如下要求):
  • 守护进程不应该有控制终端,所以(TTY = ?)
  • 守护进程的父进程ID为1,即init进程
  • 其中 PID != SID 表明该进程不是会话的leader进程,因为第二个fork()的作用
  • 因为 PID != SID 所以该守护进程无法重新打开/控制一个TTY

 

参考链接:

=END=


《 “Go语言编程练习#2-守护进程” 》 有 7 条评论

  1. 如何在Go的函数中得到调用者函数名?
    https://colobu.com/2018/11/03/get-function-name-in-go/
    `
    有时候在Go的函数调用的过程中,我们需要知道函数被谁调用,比如打印日志信息等。例如下面的函数,我们希望在日志中打印出调用者的名字。

    func printMyName() string {
    pc, _, _, _ := runtime.Caller(1)
    return runtime.FuncForPC(pc).Name()
    }
    func printCallerName() string {
    pc, _, _, _ := runtime.Caller(2)
    return runtime.FuncForPC(pc).Name()
    }

    // func Caller(skip int) (pc uintptr, file string, line int, ok bool)
    Caller可以返回函数调用栈的某一层的程序计数器、文件信息、行号。
    0 代表当前函数,也是调用runtime.Caller的函数。1 代表上一层调用者,以此类推。
    `

  2. golang 后台服务设计精要
    http://litang.me/post/golang-server-design/
    `
    守护进程
    优雅的结束进程
     响应信号
     等待所有协程退出
    goroutine 生命期管理
    数据库操作与 ORM
     标准库中的数据库操作接口
     ORM
    HTTP 服务
     标准库 net/http 包
     httprouter
     middleware
     gin
    总结
    参考资料
    `

  3. Go配置文件热加载 – 发送系统信号
    https://mp.weixin.qq.com/s/fJy5iQqPJvyt2PSsdjYr0w
    https://github.com/apptut/go-labs/blob/master/hotload/signal/main/signal.go
    `
    在日常项目的开发中,我们经常会使用配置文件来保存项目的基本元数据,配置文件的类型有很多,如:JSON、xml、yaml、甚至可能是个纯文本格式的文件。不管是什么类型的配置数据,在某些场景下,我们可会有热更新当前配置文件内容的需求,比如:使用Go运行的一个常驻进程,运行了一个 Web Server 服务进程。

    此时,如果配置文件发生变化,我们如何让当前程序重新读取新的配置文件内容呢?接下来,我们将使用如下两种方式实现配置文件的更新:
    1、使用系统信号(手动式)。
    2、使用inotify, 监听文件修改事件。

    不管是哪一种方式,都会用到Go语言中 goroutine 的概念,我打算使用 goroutine 新起一个协程,新协程目的是用来接收系统信号(signal)或者监听文件被修改的事件,如果你对 goroutine 的概念不是很了解,那么建议你先查阅相关资料。

    鉴于文章篇幅考虑,本文中我们只实现了第一种文件更新方式。下一篇文章中,我们将使用第二种方式:使用inotify监听配置文件的变化,以实现配置文件的自动更新,期待你的关注~
    `

  4. Wikipedia: System call
    https://en.wikipedia.org/wiki/System_call

    The GNU C Library (glibc)
    https://www.gnu.org/software/libc/

    4.1 函数调用 · Go 语言设计与实现
    https://draven.co/golang/docs/part2-foundation/ch04-basic/golang-function-call/

    Measurements of system call performance and overhead
    http://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead

    s1bench – syscall benchmark 1. Tests a syscall & think loop.
    https://github.com/brendangregg/Misc/blob/master/s1bench/s1bench.c

    Fastest Linux system call
    https://stackoverflow.com/a/48913894

    Wikipedia: Interrupt
    https://en.wikipedia.org/wiki/Interrupt

    Hardware & Software interrupts
    https://en.wikipedia.org/wiki/Interrupt#Hardware_interrupts

    Michael Kerrisk. 2010. The Linux Programming Interface: A Linux and UNIX System Programming Handbook (1st. ed.). Chapter 3. P44. No Starch Press, USA.

    “int 0x80” system call path performance implications. P82.
    https://francescoquaglia.github.io/TEACHING/PMMC/SLIDES/kernel-programming-basics.pdf

    runtime, syscall: use int $0x80 to invoke syscalls on android/386
    https://go-review.googlesource.com/c/go/+/16996/

    runtime, syscall: switch linux/386 to use int 0x80
    https://go-review.googlesource.com/c/go/+/19833/

    Intel P6 vs P7 system call performance
    https://lkml.org/lkml/2002/12/9/13

    Wikipedia: Model-specific register
    https://en.wikipedia.org/wiki/Model-specific_register

    Wikipedia: vDSO
    https://en.wikipedia.org/wiki/VDSO

    Kernel and userspace setup
    https://vvl.me/pdf/LPC_vDSO.pdf

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注