golang执行shell命令,实时输出,支持kill

本文介绍了如何在Golang中执行shell命令,并实现命令执行过程中的实时输出,同时提供了如何在需要时kill相关进程的方法。

工具类 

package cmd

import (
	"bufio"
	"logger"
	"fmt"
	"io"
	"os/exec"
	"sync"
	"syscall"
)

type Command struct {
	CmdStr          string
	Pid             int
	ExitCode        int
	StdOutput       string
	ErrOutput       string
	isPrintRealTime bool
}

var log = logger.GetLogger()

const (
	ExitCodeDefault    = -999
	ExitCodeIoError    = -998
	ExitCodeStartError = -997
)

/**
创建命令执行实例
*/
func NewCmd(cmdStr string) *Command {
	return &Command{
		CmdStr:          cmdStr,
		ExitCode:        ExitCodeDefault,
		isPrintRealTime: false,
	}
}

/**
创建命令执行实例,并且实时打印输出
*/
func NewCmdWithPrint(cmdStr string) *Command {
	return &Command{
		CmdStr:          cmdStr,
		ExitCode:        ExitCodeDefault,
		isPrintRealTime: true,
	}
}

/**
执行命令
*/
func (cmd *Command) Start() {
	wg := &sync.WaitGroup{}
	wg.Add(2)
	cmdExec := exec.Command("sh", "-c", cmd.CmdStr)
	cmdExec.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // 将PGID设置成与PID相同的值 保证kill
在分布式代理架构中,`SERVER` 是实际执行命令的核心模块。如果不对命令执行过程进行有效管理,**一个长时间运行或资源消耗巨大的命令(如 `find / -name "*.log"` 或死循环脚本)可能导致:** - 整个 `SERVER` 进程阻塞 - 后续请求无法处理 - 内存溢出、CPU 占满 - 甚至被系统 OOM Killer 终止 --- ## ✅ 目标 确保 `SERVER` 在执行用户命令时具备以下能力: | 要求 | 实现方式 | |------|----------| | 🔹 不阻塞主线程 | 使用 Goroutine 异步执行 | | 🔹 支持超时控制 | Context 超时中断 | | 🔹 限制资源使用 | CPU/内存/IO 隔离(cgroups/ns) | | 🔹 流式输出日志 | 实时返回 stdout/stderr | | 🔹 可安全终止 | 支持优雅 kill 和强制 killall | | 🔹 防止 fork 炸弹 | 设置进程数限制 | --- ## 🧩 解决方案总览 我们采用 **“异步 + 上下文控制 + 资源隔离 + 流式传输”** 的组合策略。 --- ## ✅ 核心代码实现(Go) ```go // server/command.go package main import ( "context" "fmt" "io" "os/exec" "syscall" "time" "unsafe" "golang.org/x/sys/unix" "github.com/google/uuid" ) type CommandRequest struct { Cmd string `json:"cmd"` Args []string `json:"args"` Timeout int `json:"timeout"` // 秒 MaxMem int64 `json:"max_mem"` // 字节 MaxCPU int `json:"max_cpu"` // 百分比(仅提示) WorkDir string `json:"work_dir"` Env map[string]string `json:"env"` } type CommandResponse struct { RequestId string `json:"request_id"` Pid int `json:"pid"` Status string `json:"status"` // running, success, failed, timeout ExitCode int `json:"exit_code,omitempty"` Stdout string `json:"stdout,omitempty"` Stderr string `json:"stderr,omitempty"` Error string `json:"error,omitempty"` Timestamp int64 `json:"timestamp"` } // 正在运行的命令池(用于 kill 控制) var runningCmds sync.Map // request_id -> *exec.Cmd func ExecuteCommand(req CommandRequest) <-chan CommandResponse { out := make(chan CommandResponse, 100) go func() { defer close(out) // 1. 创建带超时的 Context ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Timeout)*time.Second) defer cancel() // 2. 构建命令 cmd := exec.CommandContext(ctx, req.Cmd, req.Args...) if req.WorkDir != "" { cmd.Dir = req.WorkDir } if req.Env != nil { var env []string for k, v := range req.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } cmd.Env = env } // 3. 捕获输出支持流式) stdoutPipe, _ := cmd.StdoutPipe() stderrPipe, _ := cmd.StderrPipe() // 4. 开始执行 if err := cmd.Start(); err != nil { sendError(out, "启动命令失败: "+err.Error()) return } requestId := uuid.New().String() runningCmds.Store(requestId, cmd) defer runningCmds.Delete(requestId) // 发送开始事件 out <- CommandResponse{ RequestId: requestId, Pid: cmd.Process.Pid, Status: "running", Timestamp: time.Now().Unix(), } // 5. 并发读取 stdout/stderr 流 go func() { buffer := make([]byte, 4096) for { n, err := stdoutPipe.Read(buffer) if n > 0 { out <- CommandResponse{ RequestId: requestId, Status: "stream", Stdout: string(buffer[:n]), Timestamp: time.Now().Unix(), } } if err == io.EOF { break } if err != nil { out <- CommandResponse{ RequestId: requestId, Status: "error", Error: "读取 stdout 错误: " + err.Error(), } break } } }() go func() { buffer := make([]byte, 4096) for { n, err := stderrPipe.Read(buffer) if n > 0 { out <- CommandResponse{ RequestId: requestId, Status: "stream", Stderr: string(buffer[:n]), Timestamp: time.Now().Unix(), } } if err == io.EOF { break } if err != nil { out <- CommandResponse{ RequestId: requestId, Status: "error", Error: "读取 stderr 错误: " + err.Error(), } break } } }() // 6. 等待命令结束 err := cmd.Wait() exitCode := 0 if exitError, ok := err.(*exec.ExitError); ok { if status, ok := exitError.Sys().(syscall.WaitStatus); ok { exitCode = status.ExitStatus() } } status := "success" if ctx.Err() == context.DeadlineExceeded { status = "timeout" } else if err != nil { status = "failed" } // 最终结果 out <- CommandResponse{ RequestId: requestId, Status: status, ExitCode: exitCode, Timestamp: time.Now().Unix(), } }() return out } ``` --- ## 🔐 如何防止系统级阻塞?—— 四层防护机制 ### 1️⃣ 第一层:上下文超时自动中断(Soft Kill) ```go ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "sleep", "100") _ = cmd.Run() // 30秒后自动中断 ``` > ✅ 利用 Go 的 `context` 机制,在超时后发送 `SIGINT` → `SIGKILL` --- ### 2️⃣ 第二层:资源限制(Linux cgroups) 使用 `cgroup v1` 或 `v2` 限制单个命令的资源使用。 #### 示例:通过 shell 封装命令 ```bash #!/bin/sh # run_limited.sh CGROUP_NAME="agent-$RANDOM" MEMORY_LIMIT="100M" CPU_QUOTA="50000" # 5% of one core # 创建临时 cgroup mkdir /sys/fs/cgroup/memory/$CGROUP_NAME echo $MEMORY_LIMIT > /sys/fs/cgroup/memory/$CGROUP_NAME/memory.limit_in_bytes mkdir /sys/fs/cgroup/cpu/$CGROUP_NAME echo $CPU_QUOTA > /sys/fs/cgroup/cpu/$CGROUP_NAME/cpu.cfs_quota_us # 执行命令并加入 cgroup echo $$ > /sys/fs/cgroup/memory/$CGROUP_NAME/cgroup.procs echo $$ > /sys/fs/cgroup/cpu/$CGROUP_NAME/cgroup.procs exec "$@" ``` 然后在 Go 中调用: ```go cmd := exec.Command("/path/run_limited.sh", "find", "/", "-name", "*.log") ``` > ⚠️ 需要 root 或 CAP_SYS_ADMIN 权限 --- ### 3️⃣ 第三层:进程组控制(避免孤儿进程) 防止子进程脱离控制: ```go cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, // 设置独立进程组 } // 终止时 kill 整个进程组 func KillProcessGroup(pid int) { unix.Kill(-pid, syscall.SIGTERM) // 负号表示 kill pgid time.Sleep(2 * time.Second) unix.Kill(-pid, syscall.SIGKILL) } ``` --- ### 4️⃣ 第四层:并发控制(限制并行数量) 防止大量并发命令拖垮系统: ```go var sem = make(chan struct{}, 10) // 最多同时运行 10 个命令 func ExecuteCommand(req CommandRequest) <-chan CommandResponse { out := make(chan CommandResponse, 100) go func() { sem <- struct{}{} // 获取信号量 defer func() { <-sem }() // 释放 // 执行命令逻辑... }() return out } ``` --- ## 📈 流式输出设计(防内存溢出) 不缓存全部输出,而是边读边发: ```go go func() { buffer := make([]byte, 4096) for { n, err := stdoutPipe.Read(buffer) if n > 0 { select { case out <- streamResponse(buffer[:n], ""): case <-time.After(5 * time.Second): // 客户端消费太慢,丢弃或限速 } } if err == io.EOF { break } } }() ``` > ✅ 避免将 GB 级日志加载进内存 --- ## 🛑 强制终止 API(Kill by Request ID) 提供外部接口终止正在运行的命令: ```go // DELETE /api/v1/exec/{request_id} func HandleKill(w http.ResponseWriter, r *http.Request) { reqID := mux.Vars(r)["request_id"] if rawCmd, ok := runningCmds.Load(reqID); ok { cmd := rawCmd.(*exec.Cmd) _ = cmd.Process.Kill() // SIGKILL runningCmds.Delete(reqID) w.WriteHeader(200) json.NewEncoder(w).Encode(map[string]string{"status": "killed"}) } else { http.NotFound(w, r) } } ``` --- ## ✅ 总结:防阻塞六大原则 | 原则 | 实现方式 | |------|----------| | **异步非阻塞** | 每个命令启用独立 goroutine | | **上下文控制** | `context.WithTimeout` 自动中断 | | **资源隔离** | cgroups 限制 CPU/Memory | | **进程管控** | 使用进程组防止泄漏 | | **流式传输** | 分块返回 stdout/stderr | | **并发节流** | 信号量控制最大并行数 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值