//go:build linux package utils import ( "bytes" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "golang.org/x/sys/unix" ) // platformProgressSeen: the kernel's account of a thread shows its progress. const platformProgressSeen = true // platformWorkProbe watches a stage of teardown work through the thread it // runs on: the calling goroutine is locked to its thread until the stage ends // (threadProbe.release), so the thread the kernel accounts for is the one // doing the work -- the one in the removal's system call, or waiting on the // CA tool it started. func platformWorkProbe() workProbe { runtime.LockOSThread() tid := unix.Gettid() p := &threadProbe{tid: tid, dir: "/proc/self/task/" + strconv.Itoa(tid)} // The processes the thread had already started, for other goroutines // that ran on it before, are not the stage's: their running is no // progress of its. Each is known by its pid and start time, so a pid // reused meanwhile is the stage's. Known only where the kernel lists a // thread's own children; otherwise every child of the agent counts // (threadChildren). if !childrenFiles() { return p // nothing to snapshot: every child of the agent counts } if pids, own := threadChildren(p.dir); own { for _, pid := range pids { if start, ok := processStart(pid); ok { if p.before == nil { p.before = map[int]uint64{} } p.before[pid] = start } } } return p } // threadProbe is a stage of teardown work's thread, and what it has seen of // it. type threadProbe struct { tid int dir string // the thread's /proc/self/task/ before map[int]uint64 // the thread's children when the stage began: pid -> start time last schedMark seen bool } // schedMark is the kernel's account of a set of tasks: the work's thread, and // every thread of every process it runs (and theirs). Any change in it is // progress: a task scheduled, a process started or reaped. type schedMark struct { onCPU, waited, slices uint64 // schedstat: ns run, ns waited to run, times run tasks int runnable bool // a task is running, or waiting to run, now } func (p *threadProbe) release() { // Only from the goroutine that locked it (BeginTeardownWork): another // goroutine's unlock would let go of a thread that one locked itself. if unix.Gettid() == p.tid { runtime.UnlockOSThread() } } func (p *threadProbe) moved() bool { var m schedMark if !markThread(&m, p.dir) { return true // cannot tell: not taken for no progress } seen := map[int]bool{} children, _ := threadChildren(p.dir) for _, pid := range children { if start, ok := p.before[pid]; ok { if now, ok := processStart(pid); !ok || now == start { continue // the thread's from before the stage, or gone } } markProcess(&m, pid, seen) } runnable := m.runnable m.runnable = false // compared by what the kernel has counted alone moved := !p.seen || runnable || m != p.last p.last, p.seen = m, true return moved } // describe reads how the stage's thread stands: its state (stat), and the // kernel function it sleeps in (wchan), which the kernel gives as "0" for a // thread that runs, and to a reader it does not let see kernel addresses. func (p *threadProbe) describe() (state, wait string) { if b, err := os.ReadFile(p.dir + "/stat"); err == nil { if st, _, _, ok := parseTaskStat(b); ok { state = string(st) } } if b, err := os.ReadFile(p.dir + "/wchan"); err == nil { if w := strings.TrimSpace(string(b)); w != "" && w != "0" { wait = w } } return state, wait } // markThread adds the task at dir (a /proc//task/ directory) to m, // and reports whether it could be read. func markThread(m *schedMark, dir string) bool { stat, err := os.ReadFile(dir + "/stat") if err != nil { return false } state, utime, stime, ok := parseTaskStat(stat) if !ok { return false } m.tasks++ if state == 'R' { m.runnable = true } if onCPU, waited, slices, ok := readSchedstat(dir + "/schedstat"); ok { m.onCPU += onCPU m.waited += waited m.slices += slices } else { // A kernel that keeps no schedstat: CPU time in ticks, and the // context switches the status file counts. m.onCPU += utime + stime m.slices += contextSwitches(dir + "/status") } return true } // markProcess adds every thread of process pid to m, and then, once each, // the processes it started and theirs. A process that exits meanwhile is // simply not counted. func markProcess(m *schedMark, pid int, seen map[int]bool) { if seen[pid] { return } seen[pid] = true dir := "/proc/" + strconv.Itoa(pid) + "/task" // Not a bare listing, which, taken while threads exit, can leave out // one that is there throughout (taskIDs). tasks, _, gone, err := taskIDs("/proc", pid) if err != nil || gone { return } for _, t := range tasks { markThread(m, dir+"/"+t) } for _, child := range processChildren(pid, dir, tasks) { markProcess(m, child, seen) } } // childrenFiles says whether this kernel lists a thread's own children // (/proc//task//children, CONFIG_PROC_CHILDREN): known once, so a // thread that exits between a listing and a read is not taken for a kernel // without them. var childrenFiles = sync.OnceValue(func() bool { _, err := os.Stat("/proc/thread-self/children") return err == nil }) // threadChildren returns the processes the thread at dir started and has not // yet reaped, from its children file (own: true); on a kernel without one, // every process whose parent is the thread's process (own: false) -- more // than the thread's own, which can only make a stage seen as progressing that // is not, never the reverse. func threadChildren(dir string) (pids []int, own bool) { if childrenFiles() { b, err := os.ReadFile(dir + "/children") if err != nil { return nil, true } return parsePids(b), true } parent, err := processOf(dir) if err != nil { return nil, false } return childrenByParent(parent), false } // processChildren returns the processes process pid started and has not yet // reaped: the union of its threads' children files (tasks, read from dir), or // on a kernel without them, every process whose parent is pid. func processChildren(pid int, dir string, tasks []string) []int { if !childrenFiles() { return childrenByParent(pid) } var pids []int for _, t := range tasks { b, err := os.ReadFile(dir + "/" + t + "/children") if err != nil { continue // the thread exited meanwhile } pids = append(pids, parsePids(b)...) } return pids } // childrenByParent returns every process whose parent is parent, by each // process's stat file. func childrenByParent(parent int) []int { ents, err := os.ReadDir("/proc") if err != nil { return nil } var pids []int for _, e := range ents { pid, err := strconv.Atoi(e.Name()) if err != nil { continue } b, err := os.ReadFile("/proc/" + e.Name() + "/stat") if err != nil { continue } if ppid, ok := parseParent(b); ok && ppid == parent { pids = append(pids, pid) } } return pids } // processStart returns process pid's start time (stat field 22, in clock // ticks since boot): with its pid, what tells one process from a later one // given the same pid. func processStart(pid int) (uint64, bool) { b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") if err != nil { return 0, false } i := bytes.LastIndexByte(b, ')') if i < 0 { return 0, false } f := bytes.Fields(b[i+1:]) // starttime is field 22 of the file; the fields here begin at 3. if len(f) < 20 { return 0, false } return parseUint(f[19]), true } func parsePids(b []byte) []int { var pids []int for _, f := range bytes.Fields(b) { if pid, err := strconv.Atoi(string(f)); err == nil { pids = append(pids, pid) } } return pids } // processOf returns the id of the process the task at dir belongs to: // /proc/self/task/ is this process's, /proc//task/ pid's. func processOf(dir string) (int, error) { proc := filepath.Dir(filepath.Dir(dir)) if filepath.Base(proc) == "self" { return os.Getpid(), nil } return strconv.Atoi(filepath.Base(proc)) } // readSchedstat reads a task's schedstat file: ns on a CPU, ns waited to run, // times run. A kernel that keeps no such account either has no file or // writes zeros in it (a kernel booted without delay accounting): both are no // account, and ok is false. func readSchedstat(path string) (onCPU, waited, slices uint64, ok bool) { b, err := os.ReadFile(path) if err != nil { return 0, 0, 0, false } return parseSchedstat(b) } // parseSchedstat parses a task's schedstat file (readSchedstat): zeros, or a // file too short, are no account, and ok is false. func parseSchedstat(b []byte) (onCPU, waited, slices uint64, ok bool) { f := bytes.Fields(b) if len(f) < 3 { return 0, 0, 0, false } onCPU, waited, slices = parseUint(f[0]), parseUint(f[1]), parseUint(f[2]) return onCPU, waited, slices, onCPU != 0 || waited != 0 || slices != 0 } // parseTaskStat reads a task's state and its user and system CPU time (in // ticks) from its stat file. The command name, in parentheses, may itself // hold spaces and parentheses: the fields are counted from the last ')'. func parseTaskStat(b []byte) (state byte, utime, stime uint64, ok bool) { i := bytes.LastIndexByte(b, ')') if i < 0 { return 0, 0, 0, false } f := bytes.Fields(b[i+1:]) // state is field 3 of the file, utime 14, stime 15. if len(f) < 13 || len(f[0]) == 0 { return 0, 0, 0, false } return f[0][0], parseUint(f[11]), parseUint(f[12]), true } // parseParent reads a process's parent id from its stat file. func parseParent(b []byte) (int, bool) { i := bytes.LastIndexByte(b, ')') if i < 0 { return 0, false } f := bytes.Fields(b[i+1:]) if len(f) < 2 { return 0, false } ppid, err := strconv.Atoi(string(f[1])) return ppid, err == nil } // contextSwitches sums the voluntary and involuntary context switches a // task's status file counts. func contextSwitches(path string) uint64 { b, err := os.ReadFile(path) if err != nil { return 0 } var n uint64 for _, line := range bytes.Split(b, []byte("\n")) { k, v, found := bytes.Cut(line, []byte(":")) if !found { continue } switch string(k) { case "voluntary_ctxt_switches", "nonvoluntary_ctxt_switches": n += parseUint(bytes.TrimSpace(v)) } } return n } func parseUint(b []byte) uint64 { n, _ := strconv.ParseUint(string(b), 10, 64) return n }