proc - 进程管理库
proc 是一个进程管理工具库,提供进程信息获取、信号处理、优雅关闭和命令执行等功能。支持跨平台的信号处理(Unix/Windows)和进程生命周期管理。最低支持 Go 1.21.0 版本。
安装
go get -u go-slim.dev/proc快速开始
package mainimport ( "syscall" "go-slim.dev/proc")func main() { // 获取进程信息 pid := proc.Pid() name := proc.Name() workdir := proc.WorkDir() // 注册信号处理器 proc.On(syscall.SIGTERM, func() { println("收到终止信号,准备退出...") cleanup() }) // 等待特定信号 proc.Wait(syscall.SIGUSR1)}核心功能
1. 进程信息
获取当前进程的基本信息:
import "go-slim.dev/proc"// 获取进程 IDpid := proc.Pid()fmt.Printf("当前进程 ID: %d\n", pid)// 获取进程名称(命令名)name := proc.Name()fmt.Printf("进程名称: %s\n", name)// 获取工作目录workdir := proc.WorkDir()fmt.Printf("工作目录: %s\n", workdir)// 构建相对于工作目录的路径configPath := proc.Path("config", "app.yaml")// 等价于: filepath.Join(workdir, "config", "app.yaml")// 使用格式化构建路径logPath := proc.Pathf("logs/app-%d.log", pid)// 等价于: filepath.Join(workdir, fmt.Sprintf("logs/app-%d.log", pid))2. 信号处理
On - 注册信号处理器
注册一个信号处理器,每次接收到信号都会执行:
import ( "syscall" "go-slim.dev/proc")// 注册 SIGUSR1 处理器id := proc.On(syscall.SIGUSR1, func() { fmt.Println("收到 SIGUSR1 信号") reloadConfig()})// 可以注册多个处理器proc.On(syscall.SIGUSR1, func() { fmt.Println("另一个处理器")})// 返回的 ID 可用于取消注册proc.Cancel(id)支持的信号(Unix):
SIGHUP(1): 挂起信号,通常用于重新加载配置SIGINT(2): 中断信号(Ctrl+C)SIGQUIT(3): 退出信号(Ctrl+\)SIGTERM(15): 终止信号(默认的 kill 信号)SIGUSR1(10): 用户自定义信号 1SIGUSR2(12): 用户自定义信号 2
Once - 一次性信号处理器
注册一个只执行一次的信号处理器:
// 注册一次性处理器proc.Once(syscall.SIGUSR1, func() { fmt.Println("这只会执行一次") performOneTimeAction()})// 发送多次信号,只有第一次会触发处理器syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 执行syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 不执行Wait - 等待信号
阻塞当前 goroutine,直到接收到指定信号:
import ( "syscall" "go-slim.dev/proc")func main() { fmt.Println("等待 SIGUSR1 信号...") proc.Wait(syscall.SIGUSR1) fmt.Println("收到 SIGUSR1,继续执行")}// 在测试中使用func TestSignalHandling(t *testing.T) { done := make(chan struct{}) go func() { proc.Wait(syscall.SIGUSR1) close(done) }() // 发送信号 syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 等待信号处理完成 <-done}Cancel - 取消信号处理器
移除已注册的信号处理器:
// 注册多个处理器id1 := proc.On(syscall.SIGUSR1, handler1)id2 := proc.On(syscall.SIGUSR1, handler2)id3 := proc.On(syscall.SIGUSR2, handler3)// 取消单个处理器proc.Cancel(id1)// 取消多个处理器proc.Cancel(id2, id3)// 取消不存在的 ID 是安全的proc.Cancel(999) // 不会出错// ID 为 0 会被忽略proc.Cancel(0, id1, 0) // 只取消 id1Notify - 手动触发信号
手动向所有注册的处理器发送信号:
import ( "syscall" "go-slim.dev/proc")// 注册处理器proc.On(syscall.SIGUSR1, func() { fmt.Println("处理器 1")})proc.On(syscall.SIGUSR1, func() { fmt.Println("处理器 2")})// 手动触发信号ok := proc.Notify(syscall.SIGUSR1)// 输出: 处理器 1// 处理器 2// ok = true(有处理器被触发)// 触发没有注册处理器的信号ok = proc.Notify(syscall.SIGUSR2)// ok = false(没有处理器)特性:
- 所有匹配的处理器并发执行
- 自动进行 panic 恢复
Once处理器执行后自动移除
3. 优雅关闭
Shutdown - 优雅关闭进程
触发优雅关闭流程,通知所有 SIGTERM 处理器:
import ( "syscall" "time" "go-slim.dev/proc")func main() { // 注册清理处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("开始清理资源...") db.Close() cache.Close() fmt.Println("清理完成") }) // 设置强制退出前的等待时间 proc.SetTimeToForceQuit(10 * time.Second) // 启动服务... // 触发优雅关闭 proc.Shutdown(syscall.SIGTERM)}SetTimeToForceQuit - 设置强制退出时间
配置在发送 SIGTERM 后等待多久才强制杀死进程:
import ( "time" "go-slim.dev/proc")// 等待 10 秒后强制退出proc.SetTimeToForceQuit(10 * time.Second)// 设置为 0 表示立即强制退出proc.SetTimeToForceQuit(0)关闭流程:
如果 SetTimeToForceQuit(duration) 设置了 duration > 0:
- 异步发送 SIGTERM 给所有处理器
- 等待
duration时间 - 如果进程仍在运行,强制杀死进程
如果 SetTimeToForceQuit(0) 或未设置:
- 同步发送 SIGTERM 给所有处理器
- 立即杀死进程
4. 命令执行
Exec - 执行外部命令
使用 context 和丰富的配置选项执行外部命令:
import ( "context" "time" "go-slim.dev/proc")// 基本执行err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "echo", Args: []string{"Hello, World!"},})// 带超时的执行ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()err := proc.Exec(ctx, proc.ExecOptions{ Command: "long-running-task", Args: []string{"--verbose"}, Timeout: 5 * time.Second,})// 设置工作目录err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "make", Args: []string{"build"}, WorkDir: "/path/to/project",})// 设置环境变量err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "node", Args: []string{"app.js"}, Env: []string{ "NODE_ENV=production", "PORT=8080", },})// 捕获输出var stdout, stderr bytes.Buffererr := proc.Exec(context.Background(), proc.ExecOptions{ Command: "git", Args: []string{"status"}, Stdout: &stdout, Stderr: &stderr,})fmt.Println(stdout.String())// 提供输入stdin := strings.NewReader("input data\n")err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "cat", Stdin: stdin,})// 配置杀死延迟err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "server", TTK: 2 * time.Second, // 中断后等待 2 秒再杀死})// 启动后回调err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "daemon", OnStart: func(cmd *exec.Cmd) { fmt.Printf("进程已启动,PID: %d\n", cmd.Process.Pid) // 可以将 PID 写入文件等 },})ExecOptions 配置
type ExecOptions struct { // WorkDir 指定命令的工作目录 // 如果为空,默认使用当前进程的工作目录 WorkDir string // Timeout 指定命令执行的最大时长 // 如果 > 0,将创建一个超时 context Timeout time.Duration // Env 指定传递给命令的额外环境变量 // 这些变量会追加到当前进程的环境变量中 Env []string // Stdin 指定命令的标准输入 Stdin io.Reader // Stdout 指定命令的标准输出 Stdout io.Writer // Stderr 指定命令的标准错误输出 Stderr io.Writer // Command 指定要执行的命令 Command string // Args 指定命令参数 Args []string // TTK (Time To Kill) 指定在取消命令时, // 从发送中断信号到发送杀死信号之间的延迟 TTK time.Duration // OnStart 是命令启动后调用的回调函数 OnStart func(cmd *exec.Cmd)}5. 进程上下文
获取全局进程上下文:
import "go-slim.dev/proc"// 获取进程上下文ctx := proc.Context()// 在其他地方使用select {case <-ctx.Done(): fmt.Println("进程上下文已取消")case <-time.After(5 * time.Second): fmt.Println("操作完成")}6. 调试日志
控制调试日志输出:
import ( "io" "os" "go-slim.dev/proc")// 默认输出到 os.Stdout// 可以设置为其他输出// 输出到文件logFile, _ := os.OpenFile("proc.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)proc.Logger = logFile// 禁用调试日志proc.Logger = io.Discard// 自定义 Writerproc.Logger = myCustomWriter使用场景
1. Web 服务优雅关闭
package mainimport ( "context" "net/http" "syscall" "time" "go-slim.dev/proc" "go-slim.dev/slim")func main() { s := slim.New() // 配置路由... // 设置优雅关闭等待时间 proc.SetTimeToForceQuit(10 * time.Second) // 注册关闭处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("收到关闭信号,准备停止服务器...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Shutdown(ctx); err != nil { fmt.Printf("服务器关闭错误: %v\n", err) } else { fmt.Println("服务器已优雅关闭") } }) // 启动服务器 fmt.Printf("服务器启动在端口 8080,PID: %d\n", proc.Pid()) if err := s.Start(":8080"); err != nil && err != http.ErrServerClosed { fmt.Printf("服务器错误: %v\n", err) }}2. 配置热重载
package mainimport ( "fmt" "syscall" "go-slim.dev/proc")var config *Configfunc main() { // 加载初始配置 config = loadConfig() // 注册 SIGHUP 信号处理器(重新加载配置) proc.On(syscall.SIGHUP, func() { fmt.Println("收到 SIGHUP,重新加载配置...") newConfig := loadConfig() if newConfig != nil { config = newConfig fmt.Println("配置重新加载成功") } else { fmt.Println("配置重新加载失败,保持旧配置") } }) // 启动应用... runApp()}func loadConfig() *Config { // 从文件加载配置 configPath := proc.Path("config", "app.yaml") // ... return cfg}// 重新加载配置的方式:// kill -HUP <pid>// 或// pkill -HUP myapp3. 数据库连接池管理
import ( "database/sql" "syscall" "go-slim.dev/proc")var db *sql.DBfunc initDatabase() { var err error db, err = sql.Open("postgres", "connection-string") if err != nil { panic(err) } // 注册清理处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("关闭数据库连接...") if err := db.Close(); err != nil { fmt.Printf("关闭数据库错误: %v\n", err) } else { fmt.Println("数据库连接已关闭") } })}4. 后台任务管理
import ( "context" "syscall" "time" "go-slim.dev/proc")func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 注册取消处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("取消所有后台任务...") cancel() }) // 启动后台任务 go backgroundWorker(ctx, "worker-1") go backgroundWorker(ctx, "worker-2") go backgroundWorker(ctx, "worker-3") // 等待终止信号 proc.Wait(syscall.SIGTERM) fmt.Println("主程序退出")}func backgroundWorker(ctx context.Context, name string) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): fmt.Printf("%s: 收到取消信号,退出\n", name) return case <-ticker.C: fmt.Printf("%s: 执行任务\n", name) // 执行实际工作... } }}5. 执行构建脚本
import ( "bytes" "context" "fmt" "time" "go-slim.dev/proc")func buildProject(projectPath string) error { var stdout, stderr bytes.Buffer err := proc.Exec(context.Background(), proc.ExecOptions{ Command: "make", Args: []string{"build"}, WorkDir: projectPath, Stdout: &stdout, Stderr: &stderr, Timeout: 5 * time.Minute, Env: []string{ "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64", }, OnStart: func(cmd *exec.Cmd) { fmt.Printf("开始构建,PID: %d\n", cmd.Process.Pid) }, }) if err != nil { fmt.Println("构建错误:") fmt.Println(stderr.String()) return err } fmt.Println("构建输出:") fmt.Println(stdout.String()) return nil}6. 测试工具
import ( "syscall" "testing" "time" "go-slim.dev/proc")func TestSignalHandler(t *testing.T) { executed := false // 注册处理器 id := proc.Once(syscall.SIGUSR1, func() { executed = true }) defer proc.Cancel(id) // 发送信号 syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 等待处理器执行 time.Sleep(100 * time.Millisecond) if !executed { t.Fatal("信号处理器未执行") }}func TestWait(t *testing.T) { done := make(chan struct{}) go func() { proc.Wait(syscall.SIGUSR1) close(done) }() // 发送信号 syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) // 等待完成 select { case <-done: // 成功 case <-time.After(1 * time.Second): t.Fatal("Wait 未在超时内完成") }}7. 守护进程
package mainimport ( "fmt" "syscall" "time" "go-slim.dev/proc")func main() { // 创建 PID 文件 pidFile := proc.Path("app.pid") writePidFile(pidFile, proc.Pid()) // 注册清理处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("清理 PID 文件...") os.Remove(pidFile) }) // 设置优雅关闭时间 proc.SetTimeToForceQuit(10 * time.Second) // 守护进程逻辑 runDaemon()}func writePidFile(path string, pid int) { content := fmt.Sprintf("%d\n", pid) os.WriteFile(path, []byte(content), 0644)}跨平台支持
proc 包支持 Unix 和 Windows 平台,但信号处理有所不同:
Unix (Linux, macOS, BSD)
支持完整的 POSIX 信号:
// 所有标准信号都支持syscall.SIGHUP // 挂起syscall.SIGINT // 中断(Ctrl+C)syscall.SIGQUIT // 退出(Ctrl+\)syscall.SIGTERM // 终止syscall.SIGUSR1 // 用户信号 1syscall.SIGUSR2 // 用户信号 2// ... 等等Windows
Windows 仅支持有限的信号:
syscall.SIGINT // Ctrl+Csyscall.SIGTERM // 终止(模拟)syscall.SIGKILL // 强制终止注意:在 Windows 上,某些 Unix 信号会被映射或忽略。
API 参考
进程信息
func Pid() int // 获取进程 IDfunc Name() string // 获取进程名称func WorkDir() string // 获取工作目录func Path(components ...string) string // 构建路径func Pathf(format string, args ...any) string // 格式化路径func Context() context.Context // 获取进程上下文信号处理
func On(sig os.Signal, fn func()) uint32 // 注册信号处理器func Once(sig os.Signal, fn func()) uint32 // 注册一次性处理器func Cancel(ids ...uint32) // 取消处理器func Wait(sig os.Signal) // 等待信号func Notify(sig os.Signal) bool // 手动触发信号优雅关闭
func SetTimeToForceQuit(duration time.Duration) // 设置强制退出时间func Shutdown(sig syscall.Signal) error // 优雅关闭命令执行
func Exec(ctx context.Context, opts ExecOptions) errortype ExecOptions struct { WorkDir string Timeout time.Duration Env []string Stdin io.Reader Stdout io.Writer Stderr io.Writer Command string Args []string TTK time.Duration OnStart func(cmd *exec.Cmd)}调试日志
var Logger io.Writer // 调试日志输出注意事项
-
信号处理是全局的:
- 信号处理器注册后对整个进程生效
- 多个 goroutine 可以注册同一信号
- 所有处理器会并发执行
-
自动处理的信号:
SIGHUP,SIGINT,SIGQUIT,SIGTERM会自动触发优雅关闭- 这些信号会先通知所有 SIGTERM 处理器,然后退出进程
- 要自定义这些信号的行为,需要在
proc初始化前注册
-
处理器执行顺序:
- 同一信号的多个处理器并发执行
- 不保证执行顺序
Once处理器执行后自动移除
-
Panic 恢复:
- 信号处理器中的 panic 会被自动恢复
- panic 信息会输出到调试日志
- 不会导致整个进程崩溃
-
Wait 函数实现:
- 内部使用
Once和 channel - 使用
close(chan)而非 channel 发送,避免 goroutine 泄漏 - 多个 goroutine 可以安全地调用 Wait
- 内部使用
-
命令执行超时:
Timeout和 context 超时都会生效- 超时会先发送中断信号,等待 TTK 后发送 kill 信号
- 默认 TTK 为 0,立即杀死
-
进程组管理:
- Unix 系统会设置进程组,防止僵尸进程
- Windows 使用不同的进程管理机制
- 参考
SetSysProcAttribute平台特定实现
-
工作目录:
- 初始化时自动获取当前工作目录
- 如果获取失败会 panic
- 使用
Path系列函数构建相对路径
常见模式
多资源清理
func main() { // 初始化资源 db := initDB() cache := initCache() queue := initQueue() // 注册清理处理器 proc.On(syscall.SIGTERM, func() { fmt.Println("清理数据库...") db.Close() }) proc.On(syscall.SIGTERM, func() { fmt.Println("清理缓存...") cache.Close() }) proc.On(syscall.SIGTERM, func() { fmt.Println("清理队列...") queue.Close() }) // 运行应用...}超时控制
func runWithTimeout(cmd string, args []string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return proc.Exec(ctx, proc.ExecOptions{ Command: cmd, Args: args, Timeout: timeout, // 双重保险 })}信号转发
func main() { // 捕获所有信号并转发给子进程 childPid := startChildProcess() proc.On(syscall.SIGUSR1, func() { syscall.Kill(childPid, syscall.SIGUSR1) }) proc.On(syscall.SIGUSR2, func() { syscall.Kill(childPid, syscall.SIGUSR2) })}