+var v1keys = map[string]bool{
+ "blkio": true,
+ "cpuacct": true,
+ "cpuset": true,
+ "memory": true,
+}
+
+// Find cgroup hierarchies in /proc/mounts, e.g.,
+//
+// {
+// "blkio": "/sys/fs/cgroup/blkio",
+// "unified": "/sys/fs/cgroup/unified",
+// }
+func (r *Reporter) cgroupMounts() map[string]string {
+ procmounts, err := fs.ReadFile(r.FS, "proc/mounts")
+ if err != nil {
+ r.Logger.Printf("error reading /proc/mounts: %s", err)
+ return nil
+ }
+ mounts := map[string]string{}
+ for _, line := range bytes.Split(procmounts, []byte{'\n'}) {
+ fields := bytes.SplitN(line, []byte{' '}, 6)
+ if len(fields) != 6 {
+ continue
+ }
+ switch string(fields[2]) {
+ case "cgroup2":
+ // cgroup /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime 0 0
+ mounts["unified"] = string(fields[1])
+ case "cgroup":
+ // cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
+ options := bytes.Split(fields[3], []byte{','})
+ for _, option := range options {
+ option := string(option)
+ if v1keys[option] {
+ mounts[option] = string(fields[1])
+ break
+ }
+ }
+ }
+ }
+ return mounts
+}
+
+// generate map of cgroup controller => path for r.pid.
+//
+// the "unified" controller represents cgroups v2.
+func (r *Reporter) cgroupPaths(mounts map[string]string) map[string]string {
+ if len(mounts) == 0 {
+ return nil
+ }
+ procdir := fmt.Sprintf("proc/%d", r.pid)
+ buf, err := fs.ReadFile(r.FS, procdir+"/cgroup")
+ if err != nil {
+ r.Logger.Printf("error reading cgroup file: %s", err)
+ return nil
+ }
+ paths := map[string]string{}
+ for _, line := range bytes.Split(buf, []byte{'\n'}) {
+ // The entry for cgroup v2 is always in the format
+ // "0::$PATH" --
+ // https://docs.kernel.org/admin-guide/cgroup-v2.html
+ if bytes.HasPrefix(line, []byte("0::/")) && mounts["unified"] != "" {
+ paths["unified"] = mounts["unified"] + string(line[3:])
+ continue
+ }
+ // cgroups v1 entries look like
+ // "6:cpu,cpuacct:/user.slice"
+ fields := bytes.SplitN(line, []byte{':'}, 3)
+ if len(fields) != 3 {
+ continue
+ }
+ for _, key := range bytes.Split(fields[1], []byte{','}) {
+ key := string(key)
+ if mounts[key] != "" {
+ paths[key] = mounts[key] + string(fields[2])
+ }
+ }
+ }
+ // In unified mode, /proc/$PID/cgroup doesn't have a cpuset
+ // entry, but we still need it -- there's no cpuset.cpus file
+ // in the cgroup2 subtree indicated by the 0::$PATH entry. We
+ // have to get the right path from /proc/$PID/cpuset.
+ if _, found := paths["cpuset"]; !found && mounts["unified"] != "" {
+ buf, _ := fs.ReadFile(r.FS, procdir+"/cpuset")
+ cpusetPath := string(bytes.TrimRight(buf, "\n"))
+ paths["cpuset"] = mounts["unified"] + cpusetPath
+ }
+ return paths
+}
+
+func (r *Reporter) findStatFiles() {
+ mounts := r.cgroupMounts()
+ paths := r.cgroupPaths(mounts)
+ done := map[*string]bool{}
+ for _, try := range []struct {
+ statFile *string
+ pathkey string
+ file string
+ }{
+ {&r.statFiles.cpuMax, "unified", "cpu.max"},
+ {&r.statFiles.cpusetCpus, "cpuset", "cpuset.cpus.effective"},
+ {&r.statFiles.cpusetCpus, "cpuset", "cpuset.cpus"},
+ {&r.statFiles.cpuacctStat, "cpuacct", "cpuacct.stat"},
+ {&r.statFiles.cpuStat, "unified", "cpu.stat"},
+ // blkio.throttle.io_service_bytes must precede
+ // blkio.io_service_bytes -- on ubuntu1804, the latter
+ // is present but reports 0
+ {&r.statFiles.ioServiceBytes, "blkio", "blkio.throttle.io_service_bytes"},
+ {&r.statFiles.ioServiceBytes, "blkio", "blkio.io_service_bytes"},
+ {&r.statFiles.ioStat, "unified", "io.stat"},
+ {&r.statFiles.memoryStat, "unified", "memory.stat"},
+ {&r.statFiles.memoryStat, "memory", "memory.stat"},
+ {&r.statFiles.memoryCurrent, "unified", "memory.current"},
+ {&r.statFiles.memorySwapCurrent, "unified", "memory.swap.current"},
+ } {
+ startpath, ok := paths[try.pathkey]
+ if !ok || done[try.statFile] {
+ continue
+ }
+ // /proc/$PID/cgroup says cgroup path is
+ // /exa/mple/exa/mple, however, sometimes the file we
+ // need is not under that path, it's only available in
+ // a parent cgroup's dir. So we start at
+ // /sys/fs/cgroup/unified/exa/mple/exa/mple/ and walk
+ // up to /sys/fs/cgroup/unified/ until we find the
+ // desired file.
+ //
+ // This might mean our reported stats include more
+ // cgroups in the cgroup tree, but it's the best we
+ // can do.
+ for path := startpath; path != "" && path != "/" && (path == startpath || strings.HasPrefix(path, mounts[try.pathkey])); path, _ = filepath.Split(strings.TrimRight(path, "/")) {
+ target := strings.TrimLeft(filepath.Join(path, try.file), "/")
+ buf, err := fs.ReadFile(r.FS, target)
+ if err != nil || len(buf) == 0 || bytes.Equal(buf, []byte{'\n'}) {
+ if r.Debug {
+ if os.IsNotExist(err) {
+ // don't stutter
+ err = os.ErrNotExist
+ }
+ r.Logger.Printf("skip /%s: %s", target, err)
+ }
+ continue
+ }
+ *try.statFile = target
+ done[try.statFile] = true
+ r.Logger.Printf("notice: reading stats from /%s", target)
+ break
+ }
+ }
+
+ netdev := fmt.Sprintf("proc/%d/net/dev", r.pid)
+ if buf, err := fs.ReadFile(r.FS, netdev); err == nil && len(buf) > 0 {
+ r.statFiles.netDev = netdev
+ r.Logger.Printf("using /%s", netdev)
+ }
+}
+