当你的程序学会了“诈尸“:Go 实现 Windows 进程守护术

"主程序挂了?别慌,我诈尸给你看!"

—— 一位敬业的守护进程如是说

大家好,我是你们的 Go 语言小助手(兼业余段子手)。今天我要给大家讲一个关于"程序诈尸"的故事——不是恐怖片,而是一个正经(但带点调皮)的 Windows 进程守护方案。

一、背景:老板说"程序不能死"

你有没有遇到过这种情况?

你写了一个后台服务,老板说:"这玩意儿必须24小时跑着,挂了就扣你鸡腿!"

你:"……好的老板。"(内心:我连自己都守护不了,怎么守护程序?)

别怕!今天我们就用 Go 写一个"双进程互保"系统:主程序 + 守护进程。主程序负责干活,守护进程负责盯着主程序——一旦主程序挂了,立刻原地复活,比僵尸片还快!

二、核心思想:两个程序,互相盯梢

我们的策略很简单:

  • 主程序:干正事(比如每5秒打印一句"我还活着"),同时悄悄启动一个守护进程。
  • 守护进程:默默蹲在后台,每3秒检查一次主程序是否还活着。如果发现主程序"凉了",立刻拉它起来继续打工。

而且,为了防止守护进程自己也挂了,主程序还会定期检查守护进程是否存在——双向奔赴,才是真爱 ❤️

三、代码亮点:优雅又带点小聪明

1. 启动方式靠"暗号"

我们用命令行参数 /s 来区分身份:

if len(os.Args) > 1 && os.Args[1] == "/s" {
    runGuardian() // 守护模式
    return
}

主程序启动时是"光杆司令",守护进程启动时自带"暗号" /s。就像特工接头:"天王盖地虎?"——"/s!"

2. PID 文件:进程的"身份证"

两个进程各自把自己的 PID 写到文件里:

  • main.pid:主程序身份证
  • guard.pid:守护进程身份证

对方靠读这个文件知道"你在哪",再用 Windows API 确认你是不是真的还活着。

handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))

如果打不开?说明你已经"社会性死亡"了,该诈尸了!

3. 启动新进程:用 cmd /c start

为了让新进程独立运行(不被父进程拖累),我们这样启动:

exec.Command("cmd", "/c", "start", "", exe, "/s")

这招相当于在 Windows 上"另起炉灶",新进程和老爹彻底断奶,就算主程序崩溃,守护进程也能活得好好的。

小贴士:注释里还留了 HideWindow: true 的彩蛋,可以让守护进程彻底隐身,连任务栏都不露脸——真正的幕后黑手!

4. 无限循环?不怕,我们有节制

主程序只跑 20 次(100秒)就主动退出,模拟"意外崩溃"。而守护进程一旦发现它挂了,立刻重启:

fmt.Println("检测到主程序已退出,正在重启...")
startMainProcess()

于是你就会看到这样的输出:

【主程序】启动...
主程序正在运行业务逻辑...
主程序工作 #1...
...
主程序工作 #20...
主程序退出
【守护进程】启动...
main.pid : 12345
检测到主程序已退出,正在重启...
【主程序】启动...
...

主程序:我死了?

守护进程:不,你只是睡了个回笼觉。

四、使用场景 & 注意事项

适合场景

  • Windows 下无服务权限的后台任务
  • 需要高可用的小工具(比如数据采集、心跳上报)
  • 老板要求"程序不能停",但又不想写 Windows 服务

⚠️ 注意事项

  • PID 文件可能残留,建议加个清理逻辑(比如启动时检查旧 PID 是否有效)
  • 如果主程序频繁崩溃,可能会陷入"重启地狱",建议加冷却时间
  • 真正的生产环境,还是推荐用 Windows 服务 或 systemd(Linux)

五、结语:程序也要有"复活甲"

在这个充满不确定性的世界里,连程序都需要一个"复活甲"。而我们的守护进程,就是那件默默无闻却至关重要的装备。

下次老板再说"程序不能死",你可以微微一笑:"放心,它死了也会自己爬起来打卡。"

附:完整代码

package main

import (
    "fmt"
    "os"
    "os/exec"
    "runtime"
    "strconv"
    "strings"
    "time"

    "golang.org/x/sys/windows"
)

const (
    pidFileMain  = "main.pid"
    pidFileGuard = "guard.pid"
)

func main() {
    if runtime.GOOS != "windows" {
        panic("仅支持 Windows")
    }

    // 判断是否是守护模式
    if len(os.Args) > 1 && os.Args[1] == "/s" {
        runGuardian()
        return
    }

    // 主程序逻辑
    runMain()
}

// 主程序:运行业务逻辑 + 启动守护进程
func runMain() {
    fmt.Println("【主程序】启动...")

    // 1. 启动守护进程(如果未运行)
    go startGuardianIfNeeded()

    // 2. 保存自己的 PID
    pid := os.Getpid()
    if err := writePID(pidFileMain, pid); err != nil {
        fmt.Printf("无法写入主程序 PID: %v\n", err)
    }

    // 3. 运行业务逻辑(示例:每5秒打印一次)
    fmt.Println("主程序正在运行业务逻辑...")
    for i := 0; i < 20; i++ { // 示例运行100秒后退出
        fmt.Printf("主程序工作 #%d...\n", i+1)
        time.Sleep(5 * time.Second)
    }

    fmt.Println("主程序退出")
}

// 守护进程:监控主程序是否存活
func runGuardian() {
    // 隐藏控制台窗口(可选,但推荐)
    //hideConsoleWindow()

    fmt.Println("【守护进程】启动...")

    // 保存自己的 PID
    pid := os.Getpid()
    if err := writePID(pidFileGuard, pid); err != nil {
        // 守护进程无输出,记录到日志或忽略
    }

    // 每3秒检查一次主程序
    for {
        if !isProcessRunningByPIDFile(pidFileMain) {
            fmt.Println("检测到主程序已退出,正在重启...")
            startMainProcess()
        }
        time.Sleep(3 * time.Second)
    }
}

// 启动守护进程(/s 模式)
func startGuardianIfNeeded() {
    for {
        if !isProcessRunningByPIDFile(pidFileGuard) {
            exe, _ := os.Executable()
            cmd := exec.Command("cmd", "/c", "start", "", exe, "/s")
            // 设置为 GUI 子系统(无窗口)
            // cmd.SysProcAttr = &windows.SysProcAttr{
            //  HideWindow:    true,
            //  CreationFlags: windows.CREATE_NO_WINDOW,
            // }
            if err := cmd.Start(); err != nil {
                fmt.Printf("启动守护进程失败: %v\n", err)
            } else {
                fmt.Println("守护进程已启动")
            }
        }
        time.Sleep(3 * time.Second)
    }

}

// 启动主程序(由守护进程调用)
func startMainProcess() {
    exe, _ := os.Executable()
    // 获取原始命令行参数(排除 /s)
    args := []string{"/c", "start", "", exe}
    for _, arg := range os.Args[1:] {
        if arg != "/s" {
            args = append(args, arg)
        }
    }
    cmd := exec.Command("cmd", args...)
    // 主程序可以有窗口(如果是控制台程序)
    if err := cmd.Start(); err != nil {
        // 守护进程无输出,可写日志
    }
}

// 写入 PID 文件
func writePID(filename string, pid int) error {
    return os.WriteFile(filename, []byte(strconv.Itoa(pid)), 0644)
}

// 从 PID 文件读取 PID
func readPID(filename string) (int, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return 0, err
    }
    pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
    return pid, err
}

// 检查进程是否存活(通过 PID 文件 + 进程快照)
func isProcessRunningByPIDFile(pidFile string) bool {
    pid, err := readPID(pidFile)
    if err != nil {
        fmt.Println(err)
        return false
    }
    fmt.Println(pidFile, ":", pid)
    // 使用 Windows API 检查 PID 是否存在
    handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
    if err != nil {
        fmt.Println(err)
        return false
    }
    windows.CloseHandle(handle)
    return true
}

// 隐藏当前控制台窗口(仅用于守护进程)
func hideConsoleWindow() {
    user32 := windows.NewLazySystemDLL("user32.dll")

    getConsoleWindow := user32.NewProc("GetConsoleWindow")
    showWindow := user32.NewProc("ShowWindow")

    hwnd, _, _ := getConsoleWindow.Call()
    if hwnd != 0 {
        const SW_HIDE = 0
        showWindow.Call(hwnd, SW_HIDE)
    }
}

语言:Go 1.19+

平台:仅限 Windows(毕竟用了 golang.org/x/sys/windows)

本文纯属娱乐,如有雷同,说明你也经历过"程序猝死"的痛 😭

但没关系,有守护进程在,一切都能重来。

作者:九江Mgx

日期:2025年10月24日

状态:正在被自己的守护进程盯着写博客

往期部分文章列表

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容