. "$BST_LIB" require cpulimit cpulimit/present # ok iff exits AND its output holds . cpulimit reports # every usage error on a non-zero exit, which neither check_out (needs rc 0) nor # check_rc (ignores output) can express on its own. check_rc_out() { _id=$1; _want=$2; _needle=$3; shift 3 _o=$("$@" 2>&1); _r=$? case "$_o" in *"$_needle"*) _h=1 ;; *) _h=0 ;; esac if [ "$_r" -eq "$_want" ] && [ "$_h" -eq 1 ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_r want=$_want needle:'$_needle' got:'$(_bst_1line "$_o")'"; fi } # ok iff holds -- lets one captured output answer several # questions without paying for another exec. has() { case "$2" in *"$3"*) bst_ok "$1" ;; *) bst_notok "$1" "want:'$3' got:'$(_bst_1line "$2")'" ;; esac } TMP=${TMPDIR:-/tmp} BURN=$TMP/bst-cpulimit.$$ GO=$TMP/bst-cpulimit-go.$$ # Start an append-only busy loop; sets $BPID. Two deliberate choices: # # * It stops when the $GO sentinel is REMOVED, never by a signal. A # compute-bound task on this arm survives SIGKILL (brintos/linux#21, #37), so # a burner cleaned up with `kill` keeps running for the rest of the boot and # saturates the single CPU -- which starves every later suite in the run, not # just this one. The sentinel makes teardown deterministic without signals. # * The pid comes back in a global, never through $(...): a backgrounded child # inherits the command substitution's stdout pipe and holds it open, so the # substitution never sees EOF and the suite hangs. Its stdout goes to # /dev/null for the same reason. # # Progress is counted in bytes appended rather than /proc utime, because a # compute-bound task accrues zero utime here (brintos/linux#38) -- byte count is # the only throttling signal that holds on both the target and an x86_64 control. spawn_burner() { : > "$BURN"; : > "$GO" sh -c 'while [ -e '"$GO"' ]; do echo . >> '"$BURN"'; done' >/dev/null 2>&1 & BPID=$! } stop_burner() { rm -f "$GO"; sleep 1; } # Block until the burner is actually producing, bounded. Both throttle windows # must be timed from first output, not from a fixed offset: `cpulimit -- cmd` # pays its own exec plus a fork and the child's execvp before the burner starts, # and on this runtime that startup is a large fraction of a short window. Timing # from t=0 charges that latency to "throttling" and passes the case on a tool # that never throttled -- measured at limited=1172 vs unlimited=22440 bytes with # cpulimit aborting immediately and limiting nothing at all. wait_start() { _n=0 while [ ! -s "$BURN" ] && [ "$_n" -lt 20 ]; do sleep 1; _n=$((_n + 1)); done } # ---------------------------------------------------------------- presence --- check cpulimit/path test -x /usr/sbin/cpulimit # One -h run, five assertions. help=$(cpulimit -h 2>&1) has cpulimit/help "$help" "Usage: cpulimit" has cpulimit/help-pid "$help" "-p, --pid=N" has cpulimit/help-exe "$help" "-e, --exe=FILE" has cpulimit/help-children "$help" "-i, --include-children" # NCPU comes from sysconf(_SC_NPROCESSORS_ONLN) and the usage line prints # 100*NCPU. Cross-check against nproc rather than hardcoding the image's CPU # count: it catches a sysconf resolving to -1 or 0 (either would reject every # --limit as out of range) and it still holds on a many-core x86_64 control. has cpulimit/ncpu "$help" "from 0 to $(( $(nproc 2>/dev/null || echo 1) * 100 ))" # ------------------------------------------------------- argument validation --- check_rc_out cpulimit/no-limit 1 "You must specify a cpu limit percentage" cpulimit check_rc_out cpulimit/no-target 1 "You must specify one target process" cpulimit -l 50 check_rc_out cpulimit/two-targets 1 "You must specify exactly one target process" cpulimit -l 50 -p 100 -e foo check_rc_out cpulimit/pid-too-low 1 "Invalid value for argument PID" cpulimit -l 50 -p 1 check_rc_out cpulimit/bad-option 1 "invalid option" cpulimit -x # Target by name, not by pid: the --pid bound check runs first and would mask # this one on an image where /proc/sys/kernel/pid_max is unreadable (#36). check_rc_out cpulimit/limit-negative 1 "limit must be in the range" cpulimit -l -5 -e foo # The --pid upper bound is read from /proc/sys/kernel/pid_max at run time, so # assert that file first -- it is the input every -p invocation depends on. pidmax=$(cat /proc/sys/kernel/pid_max 2>/dev/null) case "$pidmax" in ''|*[!0-9]*) bst_notok cpulimit/pid-max-readable "/proc/sys/kernel/pid_max unreadable (got:'$pidmax') -- brintos/linux#36" ;; *) bst_ok cpulimit/pid-max-readable ;; esac # ------------------------------------------------------------ target lookup --- spawn_burner; b=$BPID; sleep 1 # Bounded: where -p works, `-z` keeps cpulimit alive until the target dies, so # an unbounded call never returns. out=$(timeout 4 cpulimit -l 20 -p "$b" -z 2>&1) stop_burner # get_pid_max() returns -1 when the sysctl cannot be read, and then # `pid >= get_pid_max()` holds for EVERY pid, so cpulimit refuses a live target. case "$out" in *"Invalid value for argument PID"*) bst_notok cpulimit/accepts-live-pid "live pid $b rejected: pid_max unreadable so get_pid_max()=-1 -- brintos/linux#36" ;; *"Process $b found"*) bst_ok cpulimit/accepts-live-pid ;; *) bst_notok cpulimit/accepts-live-pid "want:'Process $b found' got:'$(_bst_1line "$out")'" ;; esac # check_proc() gates every mode on statfs("/proc"), comparing the magic against # 0x9fa0; statfs(2) is ENOSYS here, so cpulimit exits 254 before limiting # anything. /proc IS mounted and readable -- only the syscall is missing, which # makes the message misleading. # # Probe it through -e, NOT -p: with #36 standing, -p exits at the pid bound check # ABOVE, before check_proc() is ever reached, so a -p-derived assertion passes # vacuously and reports a green for a tool that never got that far. out=$(timeout 4 cpulimit -l 20 -e nosuchproc -z 2>&1) case "$out" in *"procfs is not mounted"*) bst_notok cpulimit/procfs-detect "statfs(2) ENOSYS aborts cpulimit -- brintos/linux#28" ;; *) bst_ok cpulimit/procfs-detect ;; esac # ----------------------------------------------------- does it LIMIT at all --- # There is deliberately NO throttling measurement here. It would need two # multi-second burner windows, and on this arm it cannot come out green anyway: # cpulimit aborts at check_proc() before it limits anything, which # cpulimit/procfs-detect and cpulimit/command-mode-limits below already assert # deterministically and for a fraction of the cost. Those two are the red # witnesses for #28, and they flip green exactly when cpulimit stops aborting -- # which is the point at which a throttling assertion becomes meaningful and # should be added. # # The cost is not hypothetical: the guest is single-CPU with a small RAM budget # and does not reclaim memory on exit (brintos/linux#25), so two extra burner # windows starved the burners in this very suite down to zero bytes -- and this # suite shares a machine with ~140 others in a full run. # Command mode still runs the command and propagates its exit status even though # the limiter half aborts -- so a caller that only checks cpulimit's rc sees a # success. Assert the three halves separately. out=$(cpulimit -l 30 -- /bin/sh -c 'echo CMDMODE-RAN; exit 7' 2>&1); rc=$? has cpulimit/command-mode-runs "$out" CMDMODE-RAN check_eq cpulimit/command-mode-status 7 "$rc" case "$out" in *"procfs is not mounted"*) bst_notok cpulimit/command-mode-limits "limiter aborted: statfs(2) ENOSYS -- brintos/linux#28" ;; *) bst_ok cpulimit/command-mode-limits ;; esac # -------------------------------------- the kernel mechanisms cpulimit needs --- # Fixing #28 alone would turn today's honest abort into a tool that silently # fails to limit, so the SIGSTOP/SIGCONT pair its throttle is built from is # asserted directly rather than inferred. # # The third gap -- a compute-bound task accruing zero utime/stime # (brintos/linux#38), which starves cpulimit's controller of its only input -- # is NOT asserted here on purpose: measuring it needs a pure-compute task with # no syscalls, and such a task cannot be stopped on this arm (no sentinel check # without a syscall, and SIGKILL does not reach it), so the case would leave a # permanent CPU hog behind and starve every later suite in the run. It carries # its own standalone reproducer in #38 instead. # 1. SIGSTOP must stop the target -- cpulimit's throttle IS SIGSTOP/SIGCONT. # A pre-signal baseline gates the verdict: on a machine this small a burner # can be starved to zero bytes on its own, and "made no progress" would then # read as "SIGSTOP worked". Only a burner demonstrably producing BEFORE the # signal can testify about what the signal did. spawn_burner; b=$BPID wait_start p0=$(wc -c < "$BURN"); sleep 2; p1=$(wc -c < "$BURN") kill -STOP "$b" 2>/dev/null; sleep 1 n1=$(wc -c < "$BURN"); sleep 3; n2=$(wc -c < "$BURN") st=$(awk '{print $3}' /proc/$b/stat 2>/dev/null) kill -CONT "$b" 2>/dev/null stop_burner echo "cpulimit/sigstop measurement: pre=$((p1 - p0)) post=$((n2 - n1)) state=$st" # Beyond the baseline, "halted" is still not enough on its own: SIGSTOP KILLS a # target that enters syscalls here, and a dead burner appends nothing either. # Require a live, genuinely stopped (T) task. if [ "$((p1 - p0))" -le 0 ]; then bst_notok cpulimit/sigstop-stops-target "burner was not producing before the signal -- inconclusive" elif [ "$st" = "Z" ]; then bst_notok cpulimit/sigstop-stops-target "SIGSTOP TERMINATED the target (state=Z) instead of stopping it -- brintos/linux#37" elif [ "$((n2 - n1))" -ne 0 ]; then bst_notok cpulimit/sigstop-stops-target "ran on after SIGSTOP (+$((n2 - n1)) bytes, state=$st) -- brintos/linux#37" elif [ "$st" != "T" ]; then bst_notok cpulimit/sigstop-stops-target "progress halted but state=$st, not T (stopped) -- brintos/linux#37" else bst_ok cpulimit/sigstop-stops-target fi # 2. SIGCONT must not be fatal -- cpulimit sends it once per control slot and # again on exit, to guarantee it never leaves a target stopped. sleep 30 & s=$! sleep 1; before=$(awk '{print $3}' /proc/$s/stat 2>/dev/null) kill -CONT $s 2>/dev/null; sleep 1 after=$(awk '{print $3}' /proc/$s/stat 2>/dev/null) kill -9 $s 2>/dev/null if [ "$after" = "Z" ]; then bst_notok cpulimit/sigcont-not-fatal "SIGCONT terminated a live process ($before -> $after) -- brintos/linux#37" else bst_ok cpulimit/sigcont-not-fatal; fi rm -f "$BURN" "$GO"