//go:build linux package sysinfo import ( "os" "path/filepath" "strconv" "strings" "golang.org/x/sys/unix" ) var pageSize = int64(os.Getpagesize()) // clockTicks — USER_HZ. На всех практически встречающихся Linux-сборках 100. const clockTicks = 100.0 // Sample обходит /proc и суммирует статистику всех процессов, входящих в // группу pgid: сервер PZ — это скрипт-обёртка плюс порождённая им JVM. func (s *Sampler) Sample(pgid int) ProcStats { var ( stats ProcStats cpuSecs float64 ) if pgid <= 0 { s.Reset() return stats } entries, err := os.ReadDir("/proc") if err != nil { return stats } for _, e := range entries { if !e.IsDir() { continue } pid, err := strconv.Atoi(e.Name()) if err != nil { continue } utime, stime, threads, group, ok := readProcStat(pid) if !ok || group != pgid { continue } cpuSecs += (utime + stime) / clockTicks stats.Threads += threads stats.RSSBytes += readRSS(pid) } now := uptimeSeconds() s.mu.Lock() if s.lastWall > 0 && now > s.lastWall { stats.CPUPercent = (cpuSecs - s.lastCPU) / (now - s.lastWall) * 100 if stats.CPUPercent < 0 { stats.CPUPercent = 0 } } s.lastCPU, s.lastWall = cpuSecs, now s.mu.Unlock() return stats } // readProcStat разбирает /proc//stat. Имя процесса заключено в скобки и // может содержать пробелы, поэтому режем строку после последней ')'. func readProcStat(pid int) (utime, stime float64, threads, pgrp int, ok bool) { raw, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat")) if err != nil { return 0, 0, 0, 0, false } line := string(raw) idx := strings.LastIndex(line, ")") if idx < 0 || idx+2 >= len(line) { return 0, 0, 0, 0, false } // Поля после comm: state(3) ppid(4) pgrp(5) ... utime(14) stime(15) ... num_threads(20). fields := strings.Fields(line[idx+2:]) if len(fields) < 18 { return 0, 0, 0, 0, false } pgrp, _ = strconv.Atoi(fields[2]) utime, _ = strconv.ParseFloat(fields[11], 64) stime, _ = strconv.ParseFloat(fields[12], 64) threads, _ = strconv.Atoi(fields[17]) return utime, stime, threads, pgrp, true } // readRSS берёт резидентные страницы из /proc//statm (второе поле). func readRSS(pid int) int64 { raw, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "statm")) if err != nil { return 0 } fields := strings.Fields(string(raw)) if len(fields) < 2 { return 0 } pages, _ := strconv.ParseInt(fields[1], 10, 64) return pages * pageSize } func uptimeSeconds() float64 { raw, err := os.ReadFile("/proc/uptime") if err != nil { return 0 } fields := strings.Fields(string(raw)) if len(fields) == 0 { return 0 } v, _ := strconv.ParseFloat(fields[0], 64) return v } // Host собирает состояние машины; diskPath задаёт раздел для расчёта места. func Host(diskPath string) HostStats { h := HostStats{CPUCount: cpuCount()} if raw, err := os.ReadFile("/proc/meminfo"); err == nil { for _, line := range strings.Split(string(raw), "\n") { key, value, found := strings.Cut(line, ":") if !found { continue } kb, _ := strconv.ParseInt(strings.Fields(strings.TrimSpace(value))[0], 10, 64) switch key { case "MemTotal": h.MemTotalBytes = kb * 1024 case "MemAvailable": h.MemAvailableBytes = kb * 1024 } } } if raw, err := os.ReadFile("/proc/loadavg"); err == nil { if fields := strings.Fields(string(raw)); len(fields) > 0 { h.Load1, _ = strconv.ParseFloat(fields[0], 64) } } if diskPath != "" { var st unix.Statfs_t if err := unix.Statfs(diskPath, &st); err == nil { h.DiskTotalBytes = int64(st.Blocks) * int64(st.Bsize) h.DiskFreeBytes = int64(st.Bavail) * int64(st.Bsize) } } return h } func cpuCount() int { raw, err := os.ReadFile("/proc/cpuinfo") if err != nil { return 0 } return strings.Count(string(raw), "processor\t:") }