package main import ( "bytes" "fmt" "log" "os/exec" ) // 堆代码 duidaima.com func main() { var output bytes.Buffer cmd := exec.Command("top", "-b", "-n", "1") cmd.Stdout = &output err := cmd.Run() if err != nil { log.Fatal(err) } else { fmt.Printf("top result: \n%v\n", output.String()) } }执行命令:
$ go run main.go top result: top - 15:07:24 up 1 day, 1:13, 0 users, load average: 0.00, 0.00, 0.00 Tasks: 24 total, 1 running, 23 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 8056768 total, 5245460 free, 750332 used, 2060976 buff/cache KiB Swap: 2097152 total, 2097152 free, 0 used. 7003500 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 root 20 0 2324 1708 1600 S 0.0 0.0 0:01.13 init(Ubunt+ 4 root 20 0 2948 308 68 S 0.0 0.0 6:34.42 init ... 20997 someone 20 0 19680 9512 5420 S 0.0 0.1 0:00.30 zsh 29176 someone 20 0 1610576 21080 9272 S 0.0 0.3 0:00.09 go 29268 someone 20 0 711488 3008 872 S 0.0 0.0 0:00.00 main 29273 someone 20 0 29444 3656 3228 R 0.0 0.0 0:00.00 top从输出的结果中可以看到,虽然上面的方法可以获取到 CPU 相关数据,但是输出结果仅仅只是便于人眼阅读,如果我们希望将相关数据值单独取出来在程序中使用, 就需要基于结果字符串进行解析操作,这个过程就会非常麻烦而且容易出错,所以我们需要一个更好的方案。
和 /proc/stat 提供的数据类似,但是数据对应的是单个进程。
$ cat /proc/stat # 笔者的测试机器输出如下 cpu 40493 6954 65486 76990150 9113 0 25483 0 0 0 cpu0 5181 995 9564 9615416 1201 0 20111 0 0 0 ... cpu7 4382 609 7231 9627991 626 0 203 0 0 0 intr 11137544 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1310 0 181 1 1 10 0 3 0 3 0 77853 0 1408 0 1408 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 37107675 btime 1694670818 processes 276043 procs_running 1 procs_blocked 0 softirq 21005941 0 2478101 7 16554 0 0 3790140 6921044 0 7800095然后再对照看一下 /proc/stat 对应的文档:
列序号 | 名称 | 描述 |
---|---|---|
1 | user | 用户态 CPU 时间 |
2 | nice | 低优先级用户态 CPU 时间 (进程的 nice 值被调整为 1-19 之间) |
3 | system | 内核态 CPU 时间 |
4 | idle | CPU 空闲时间 (不包括 IO 等待时间) |
5 | iowait | 等待 I/O 的 CPU 时间 |
6 | irq | 处理硬中断的 CPU 时间 |
7 | softirq | 处理软中断的 CPU 时间 |
8 | steal | 当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间 |
9 | guest | 通过虚拟化运行其他操作系统的时间 |
10 | guest_nice | 低优先级运行虚拟机的时间 |
字段 | 作用 |
---|---|
intr | 系统中断相关数据 |
ctxt | 系统上下文切换次数 |
btime | 系统启动以来的时间 |
processes | 创建的进程数量 |
procs_running | 运行进程数量 |
procs_blocked | 阻塞进程数量 |
softirq | 不同类型软中断的处理次数 |
package main import ( "fmt" "log" "math" "math/rand" "os" "strconv" "strings" "time" ) const ( cpuStatFile = "/proc/stat" ) // 采样结果对象 type result struct { used uint64 // CPU 使用时间 idle uint64 // CPU 闲置时间 } // CPU 指标采样函数 func sample() (*result, error) { data, err := os.ReadFile(cpuStatFile) if err != nil { return nil, err } res := &result{} lines := strings.Split(string(data), "\n") for _, line := range lines { fields := strings.Fields(line) // 为了简化演示 // 这里只取所有 CPU 总的统计数据 if len(fields) == 0 || fields[0] != "cpu" { continue } // 将第一行数据分割为数组 n := len(fields) for i := 1; i < n; i++ { if i > 8 { continue } // 解析每一列的数值 val, err := strconv.ParseUint(fields[i], 10, 64) if err != nil { return nil, err } // 第 4 列表示 CPU 空闲时间 // 第 5 列表示 等待 I/O 的 CPU 时间 if i == 4 || i == 5 { res.idle += val } else { res.used += val } } return res, nil } return res, nil } func main() { // 获取第一次采样结果 first, err := sample() if err != nil { log.Fatal(err) } // 模拟一些 CPU 密集型任务 rand.Seed(time.Now().UnixNano()) for i := 0; i < 10000; i++ { _ = math.Sqrt(rand.Float64()) } // 获取第二次采样结果 second, err := sample() if err != nil { log.Fatal(err) } // 计算两次采样期间 CPU 的空闲时间 idle := float64(second.idle - first.idle) // 计算两次采样期间 CPU 的使用时间 used := float64(second.used - first.used) // CPU 利用率 = CPU 使用时间 / (CPU 闲置时间 + CPU 使用时间) var usage float64 if idle+used > 0 { usage = used / (idle + used) * 100 } fmt.Printf("CPU usage is %f%%\n", usage) }运行上面的代码
$ go run main.go CPU usage is 32.558140%上面的代码演示了如何获取系统中所有 CPU 总的利用率,感兴趣的读者可以在这个代码基础上进行改进,实现获取单个 CPU 的利用率。
package main import ( "fmt" "github.com/shirou/gopsutil/v3/cpu" "log" "math" "math/rand" "time" ) func main() { done := make(chan struct{}) go func() { for i := 0; i < 5; i++ { // 获取 CPU 利用率 (每 100 毫秒获取一次) percent, err := cpu.Percent(100*time.Millisecond, false) if err != nil { log.Fatalf("get CPU usage: %v\n", err) return } for _, v := range percent { fmt.Printf("CPU usage is %.2f%%\n", v) } } // 模拟程序结束后通过 channel 发送通知 done <- struct{}{} }() // 模拟一些 CPU 密集型任务 rand.Seed(time.Now().UnixNano()) for i := 0; i < 10000000; i++ { _ = math.Sqrt(rand.Float64()) } <-done close(done) }运行上面的代码
$ go run main.go CPU usage is 32.558140% CPU usage is 12.35% CPU usage is 5.00% CPU usage is 0.00% CPU usage is 0.00% CPU usage is 0.00%最后,我们追踪下 gopsutil 源代码的调用链路,学习一下内部的实现细节。
type TimesStat struct { CPU string `json:"cpu"` User float64 `json:"user"` System float64 `json:"system"` Idle float64 `json:"idle"` Nice float64 `json:"nice"` Iowait float64 `json:"iowait"` Irq float64 `json:"irq"` Softirq float64 `json:"softirq"` Steal float64 `json:"steal"` Guest float64 `json:"guest"` GuestNice float64 `json:"guestNice"` }Percent 方法
// https://github.com/shirou/gopsutil/blob/2fabf15a16dca198f735a5de2722158576e986a9/cpu/cpu.go#L148 // Percent 方法计算 CPU 利用率 // 可以计算总的 CPU 利用率,也可以计算单个 CPU 的利用率 (取决于第二个参数) // 在刚才的例子中,我们计算的是总的 CPU 利用率 func Percent(interval time.Duration, percpu bool) ([]float64, error) { return PercentWithContext(context.Background(), interval, percpu) } // Percent 方法的内部具体实现 func PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) { ... // 第一次采样 cpuTimes1, err := TimesWithContext(ctx, percpu) if err != nil { return nil, err } // 获取指标的间隔时间 // 期间直接进入休眠 if err := common.Sleep(ctx, interval); err != nil { return nil, err } // 第二次采样 cpuTimes2, err := TimesWithContext(ctx, percpu) if err != nil { return nil, err } // 根据两次采样数据计算出 CPU 利用率 return calculateAllBusy(cpuTimes1, cpuTimes2) }TimesWithContext
func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { // 获取对应的指标数据文件名称,也就是 /proc/stat filename := common.HostProc("stat") lines := []string{} if percpu { // 获取单个 CPU 数据 ... } else { // 获取总的 CPU 数据 lines, _ = common.ReadLinesOffsetN(filename, 0, 1) } ret := make([]TimesStat, 0, len(lines)) for _, line := range lines { // 将 /proc/stat 文件中的单行文本数据解析为 TimesStat 指标对象 ct, err := parseStatLine(line) if err != nil { continue } ret = append(ret, *ct) } return ret, nil }calculateAllBusy
func calculateAllBusy(t1, t2 []TimesStat) ([]float64, error) { ... ret := make([]float64, len(t1)) for i, t := range t2 { ret[i] = calculateBusy(t1[i], t) } return ret, nil } // 堆代码 duidaima.com func calculateBusy(t1, t2 TimesStat) float64 { t1All, t1Busy := getAllBusy(t1) t2All, t2Busy := getAllBusy(t2) ... return math.Min(100, math.Max(0, (t2Busy-t1Busy)/(t2All-t1All)*100)) } func getAllBusy(t TimesStat) (float64, float64) { busy := t.User + t.System + t.Nice + t.Iowait + t.Irq + t.Softirq + t.Steal return busy + t.Idle, busy }从上面的代码可以看到,calculateBusy 方法内部的计算公式为: