Ctrl K
ComunidadGuías…

utils/teardown_work_linux.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//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/<tid>
	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/<pid>/task/<tid> 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/<pid>/task/<tid>/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/<tid> is this process's, /proc/<pid>/task/<tid> 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
}