github.com/mier85/go-sensor@v1.30.1-0.20220920111756-9bf41b3bc7e0/process/stats_reader_linux.go (about) 1 // (c) Copyright IBM Corp. 2021 2 // (c) Copyright Instana Inc. 2020 3 4 // +build linux 5 6 package process 7 8 import ( 9 "bufio" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "path" 14 "strings" 15 ) 16 17 const ( 18 pageSize = 4 << 10 // standard setting, applicable for most systems 19 procPath = "/proc" 20 ) 21 22 type statsReader struct { 23 ProcPath string 24 Command string 25 } 26 27 // Stats returns a process resource stats reader for current process 28 func Stats() statsReader { 29 return statsReader{ 30 ProcPath: procPath, 31 Command: path.Base(os.Args[0]), 32 } 33 } 34 35 // Memory returns memory stats for current process 36 func (rdr statsReader) Memory() (MemStats, error) { 37 fd, err := os.Open(rdr.ProcPath + "/self/statm") 38 if err != nil { 39 return MemStats{}, nil 40 } 41 defer fd.Close() 42 43 var total, rss, shared int 44 45 // The fields come in order described in `/proc/[pid]/statm` section 46 // of https://man7.org/linux/man-pages/man5/proc.5.html 47 if _, err := fmt.Fscanf(fd, "%d %d %d", 48 &total, // size 49 &rss, // resident 50 &shared, // shared 51 // ... the rest of the fields are not used and thus omitted 52 ); err != nil { 53 return MemStats{}, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 54 } 55 56 return MemStats{ 57 Total: total * pageSize, 58 Rss: rss * pageSize, 59 Shared: shared * pageSize, 60 }, nil 61 } 62 63 // CPU returns CPU stats for current process and the CPU tick they were taken on 64 func (rdr statsReader) CPU() (CPUStats, int, error) { 65 fd, err := os.Open(rdr.ProcPath + "/self/stat") 66 if err != nil { 67 return CPUStats{}, 0, nil 68 } 69 defer fd.Close() 70 71 var ( 72 stats CPUStats 73 skipInt int 74 skipCh byte 75 ) 76 77 // The command in `/proc/self/stat` output is truncated to 15 bytes (16 including the terminating null byte) 78 comm := rdr.Command 79 if len(comm) > 15 { 80 comm = comm[:15] 81 } 82 83 // The fields come in order described in `/proc/[pid]/stat` section 84 // of https://man7.org/linux/man-pages/man5/proc.5.html. We skip parsing 85 // the `comm` field since it may contain space characters that break fmt.Fscanf format. 86 if _, err := fmt.Fscanf(fd, "%d ("+comm+") %c %d %d %d %d %d %d %d %d %d %d %d %d", 87 &skipInt, // pid 88 &skipCh, // state 89 &skipInt, // ppid 90 &skipInt, // pgrp 91 &skipInt, // session 92 &skipInt, // tty_nr 93 &skipInt, // tpgid 94 &skipInt, // flags 95 &skipInt, // minflt 96 &skipInt, // cminflt 97 &skipInt, // majflt 98 &skipInt, // cmajflt 99 &stats.User, // utime 100 &stats.System, // stime 101 // ... the rest of the fields are not used and thus omitted 102 ); err != nil { 103 return stats, 0, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 104 } 105 106 tick, err := rdr.currentTick() 107 if err != nil { 108 return stats, 0, fmt.Errorf("failed to get current CPU tick: %s", err) 109 } 110 111 return stats, tick, nil 112 } 113 114 // currentTick parses /proc/stat, sums up the total number of ticks spent on each CPU and averages them 115 // by the number of CPUs 116 func (rdr statsReader) currentTick() (int, error) { 117 fd, err := os.Open(rdr.ProcPath + "/stat") 118 if err != nil { 119 return 0, nil 120 } 121 defer fd.Close() 122 123 sc := bufio.NewScanner(fd) 124 sc.Split(bufio.ScanLines) 125 126 var ( 127 ticks, cpuCount int 128 user, nice, sys, idle, iowait, irq, softIRQ, steal int 129 skipStr string 130 ) 131 132 for sc.Scan() { 133 s := sc.Text() 134 if !strings.HasPrefix(s, "cpu") { 135 continue 136 } 137 138 if strings.HasPrefix(s, "cpu ") { // skip total CPU line 139 continue 140 } 141 142 // The fields come in order described in `/proc/stat` section 143 // of https://man7.org/linux/man-pages/man5/proc.5.html 144 if _, err := fmt.Sscanf(s, "%s %d %d %d %d %d %d %d %d", 145 &skipStr, // CPU label 146 &user, 147 &nice, 148 &sys, 149 &idle, 150 &iowait, 151 &irq, 152 &softIRQ, 153 &steal, 154 // ... the rest of the fields are not used and thus omitted 155 ); err != nil { 156 return 0, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 157 } 158 159 ticks += user + nice + sys + idle + iowait + irq + softIRQ + steal 160 cpuCount++ 161 } 162 163 if err := sc.Err(); err != nil { 164 return 0, fmt.Errorf("failed to read %s: %s", fd.Name(), err) 165 } 166 167 if cpuCount < 2 { 168 return ticks, nil 169 } 170 171 return ticks / cpuCount, nil 172 } 173 174 // Limits returns resource limits configured for current process 175 func (rdr statsReader) Limits() (ResourceLimits, error) { 176 fd, err := os.Open(rdr.ProcPath + "/self/limits") 177 if err != nil { 178 return ResourceLimits{}, nil 179 } 180 defer fd.Close() 181 182 sc := bufio.NewScanner(fd) 183 sc.Split(bufio.ScanLines) 184 185 var limits ResourceLimits 186 187 for sc.Scan() { 188 s := sc.Text() 189 if !strings.HasPrefix(s, "Max open files") { 190 continue 191 } 192 193 s = strings.TrimLeft(s[14:], " \t") // trim the "max open files" prefix along with trailing space 194 if !strings.HasPrefix(s, "unlimited") { 195 if _, err := fmt.Sscanf(s, "%d", &limits.OpenFiles.Max); err != nil { 196 return limits, fmt.Errorf("unexpected %s format: %s", fd.Name(), err) 197 } 198 } 199 200 break 201 } 202 203 if err := sc.Err(); err != nil { 204 return limits, fmt.Errorf("failed to read %s: %s", fd.Name(), err) 205 } 206 207 fdNum, err := rdr.currentOpenFiles() 208 if err != nil { 209 return limits, fmt.Errorf("failed to get the number of open files: %s", err) 210 } 211 212 limits.OpenFiles.Current = fdNum 213 214 return limits, nil 215 } 216 217 func (rdr statsReader) currentOpenFiles() (int, error) { 218 fds, err := ioutil.ReadDir(rdr.ProcPath + "/self/fd/") 219 if err != nil { 220 return 0, fmt.Errorf("failed to list %s: %s", rdr.ProcPath+"/fd/", err) 221 } 222 223 return len(fds), nil 224 }