"主程序挂了?别慌,我诈尸给你看!"
—— 一位敬业的守护进程如是说
大家好,我是你们的 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日
状态:正在被自己的守护进程盯着写博客
往期部分文章列表
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载