Slim

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): 用户自定义信号 1
  • SIGUSR2 (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)  // 只取消 id1

Notify - 手动触发信号

手动向所有注册的处理器发送信号:

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

  1. 异步发送 SIGTERM 给所有处理器
  2. 等待 duration 时间
  3. 如果进程仍在运行,强制杀死进程

如果 SetTimeToForceQuit(0) 或未设置:

  1. 同步发送 SIGTERM 给所有处理器
  2. 立即杀死进程

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 myapp

3. 数据库连接池管理

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  // 调试日志输出

注意事项

  1. 信号处理是全局的

    • 信号处理器注册后对整个进程生效
    • 多个 goroutine 可以注册同一信号
    • 所有处理器会并发执行
  2. 自动处理的信号

    • SIGHUP, SIGINT, SIGQUIT, SIGTERM 会自动触发优雅关闭
    • 这些信号会先通知所有 SIGTERM 处理器,然后退出进程
    • 要自定义这些信号的行为,需要在 proc 初始化前注册
  3. 处理器执行顺序

    • 同一信号的多个处理器并发执行
    • 不保证执行顺序
    • Once 处理器执行后自动移除
  4. Panic 恢复

    • 信号处理器中的 panic 会被自动恢复
    • panic 信息会输出到调试日志
    • 不会导致整个进程崩溃
  5. Wait 函数实现

    • 内部使用 Once 和 channel
    • 使用 close(chan) 而非 channel 发送,避免 goroutine 泄漏
    • 多个 goroutine 可以安全地调用 Wait
  6. 命令执行超时

    • Timeout 和 context 超时都会生效
    • 超时会先发送中断信号,等待 TTK 后发送 kill 信号
    • 默认 TTK 为 0,立即杀死
  7. 进程组管理

    • Unix 系统会设置进程组,防止僵尸进程
    • Windows 使用不同的进程管理机制
    • 参考 SetSysProcAttribute 平台特定实现
  8. 工作目录

    • 初始化时自动获取当前工作目录
    • 如果获取失败会 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)    })}

相关链接

切换 选择 esc 关闭搜索