Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 34 · Defensive Automation Around EDA Tools

Timeouts and Hung Jobs

A tool that neither succeeds nor fails consumes a whole night and leaves nothing to report. This chapter bounds every invocation on a machine where the obvious tool for the job is not installed, and measures why the bound must record its own verdict: the simulator exits zero when it is politely killed, and ignores the signal most scripts send first.

Advanced25 min readLinuxPart IIEDATimeoutsSignals

Part II · Module 34 · Chapter 34.4 · Timeouts and Hung Jobs

Chapter 34.3 reacted to a failure. A hang produces none: the tool is running, the log is growing or not, and nothing will ever decide. An unbounded invocation is a flow with no upper bound on its own runtime, and the cost is not the hung job — it is the two hundred queued behind it.

1. The Obvious Tool Is Not Installed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ command -v timeout  || echo "NOT INSTALLED"
NOT INSTALLED
$ command -v gtimeout || echo "NOT INSTALLED"
NOT INSTALLED
$ ls /opt/homebrew/opt/coreutils/libexec/gnubin/timeout 2>/dev/null || echo no
no

timeout(1) is GNU coreutils and this is BSD userland. The registry's own command hint for this chapter is

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
timeout 2h ./simv +UVM_TESTNAME="$t"

which is the right command on a Linux compute farm and does not run here. That is worth stating rather than quietly substituting, because the same gap appears on any machine where a flow is developed on a Mac and deployed on Linux, and the failure mode is timeout: command not found at two in the morning.

So the rest of this chapter builds the bound by hand — and the hand-rolled version turns out to need things timeout would not have given you anyway.

2. What the Tool Does When You Ask It to Stop

A bound has to end the process, and which signal you send determines what you learn. vvp as the direct child of the shell, with a sleep through the identical harness as a control:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  kill -TERM  ended, wait rc=0    log 105 bytes
  kill -INT   IGNORED (still running after 1.5s)
  kill -HUP   ended, wait rc=0    log 105 bytes
  kill -KILL  ended, wait rc=137  log 0 bytes
  control: sleep + TERM -> 143

Four findings, and the control row is what makes them trustworthy — sleep gives the textbook 128 + 15 = 143, so the harness is not what distorts the simulator's numbers.

TERM yields exit status 0. Not 143. The tool installs a handler, shuts down and exits successfully. A timeout wrapper that kills a hung run and then reads the exit status concludes that the run passed.

INT is ignored entirely. Ctrl-C does nothing to it, so a script escalating INT → TERM → KILL wastes its first step, and an engineer interrupting a regression at the terminal leaves simulators running. Chapter 30.4 measured the same tool declining INT, and Chapter 30.1 found it resetting SIGHUP's ignore request so nohup did not protect it.

HUP works and also gives 0, which is the same trap as TERM.

And KILL gives 137 with a zero-byte log. The status is finally informative and the evidence is gone — the buffered output never reached the file. Chapter 30.4 measured that cost at exactly 88 log lines.

As a tool, because every tool needs asking

Those four rows are a property of one tool, and the next tool in the flow will answer differently. So the probe is worth keeping:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# signalprobe COMMAND... -- how does this tool respond to each stop signal?
# Run once per tool. The answers decide how a timeout must be written, and
# they are not guessable: this simulator exits 0 on TERM and ignores INT.
set -u
[ $# -gt 0 ] || { echo "signalprobe: give me a command that runs for a while" >&2; exit 2; }
w=$(mktemp -d) || exit 2
trap 'rm -rf "$w"' EXIT INT TERM
printf '  %-8s %-22s %-10s %s\n' SIGNAL BEHAVIOUR 'wait rc' 'log bytes'
one() {
  local sig=$1
  shift                      # BEFORE launching: "$@" must not include the signal
  ( exec "$@" ) > "$w/$sig.log" 2>&1 & local p=$!
  sleep 0.8
  kill -"$sig" "$p" 2>/dev/null
  local n=0
  while kill -0 "$p" 2>/dev/null && [ "$n" -lt 15 ]; do n=$((n+1)); sleep 0.1; done
  if kill -0 "$p" 2>/dev/null; then
    kill -9 "$p" 2>/dev/null; wait "$p" 2>/dev/null
    printf '  %-8s %-22s %-10s %s\n' "$sig" "IGNORED" "-" "$(wc -c < "$w/$sig.log" | tr -d ' ')"
  else
    wait "$p" 2>/dev/null; local rc=$?
    printf '  %-8s %-22s %-10s %s\n' "$sig" "ended" "$rc" "$(wc -c < "$w/$sig.log" | tr -d ' ')"
  fi
}
for s in TERM INT HUP QUIT KILL; do one "$s" "$@"; done
# the control: a tool known to die properly, through the identical harness
one TERM sleep 30
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ signalprobe vvp "$PWD/logs/sim.vvp" +TESTNAME=hang +SEED=1
  SIGNAL   BEHAVIOUR              wait rc    log bytes
  TERM     ended                  0          105
  INT      IGNORED                -          151
  HUP      ended                  0          105
  QUIT     IGNORED                -          0
  KILL     ended                  137        0
  TERM     ended                  143        0

The last row is the control — sleep 30 through the identical harness, giving the textbook 143 — and it is part of the tool rather than a separate step, because a probe whose harness is broken produces a confident table of wrong numbers.

Which it did, the first time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  SIGNAL   BEHAVIOUR              wait rc    log bytes
  TERM     ended                  127        48
  INT      ended                  127        47
  HUP      ended                  127        48

Every row 127 — command not found. The function took the signal name as $1 and shifted after launching the subshell, so "$@" still contained it and the shell tried to execute TERM vvp …. One line in the wrong order:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
one() {
  local sig=$1
  shift                      # BEFORE launching: "$@" must not include the signal
  ( exec "$@" ) > "$w/$sig.log" 2>&1 & local p=$!
  sleep 0.8
  kill -"$sig" "$p" 2>/dev/null
  # … then poll, report, and fall back to KILL if it was ignored
}

And the tell was the control row, which should have read 143 and read 127 like everything else. A control that agrees with the thing it is controlling for is a control that has failed — the one check that would have caught this at a glance, which is the argument for building it into the probe rather than remembering to run it.

3. The Bound

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# bounded LIMIT_S OUTDIR -- run a tool under a time bound, and record whether
# the bound fired. The status cannot carry that: TERM makes this tool exit 0.
set -u
limit=${1:?need a limit in seconds}; d=${2:?need an output directory}; shift 2
root=$(cd "$(dirname "$0")/.." && pwd)
mkdir -p "$d" || exit 2

( cd "$d" && exec "$@" ) > "$d/run.log" 2>&1 & p=$!
timedout=0
n=0; lim=$(( limit * 10 ))
while kill -0 "$p" 2>/dev/null && [ "$n" -lt "$lim" ]; do n=$((n+1)); sleep 0.1; done

if kill -0 "$p" 2>/dev/null; then
  timedout=1
  # TERM first: it is the one that lets the tool flush its log.
  kill -TERM "$p" 2>/dev/null
  m=0; while kill -0 "$p" 2>/dev/null && [ "$m" -lt 20 ]; do m=$((m+1)); sleep 0.1; done
  # and KILL only if it refused, which costs the buffered output.
  if kill -0 "$p" 2>/dev/null; then
    printf 'bounded: TERM ignored, escalating to KILL (the log will be lost)\n' >&2
    kill -9 "$p" 2>/dev/null
  fi
fi
wait "$p" 2>/dev/null
rc=$?
printf '%s\n' "$rc"       > "$d/tool.rc"
printf '%s\n' "$timedout" > "$d/timedout"
printf '%s\n' "$limit"    > "$d/limit_s"
[ "$timedout" -eq 1 ] && printf 'bounded: the %ds limit fired\n' "$limit" >&2
exit 0

Five decisions.

exec in the subshell, so $! is the tool and not a shell. Chapter 31.5 measured what happens otherwise — an RSS sampler that read a 1 MB wrapper instead of a 9 MB simulator — and here it matters because kill has to reach the tool.

A polling loop rather than a blocking wait, because the shell must stay alive to enforce the bound. And sleep 0.1 rather than sleep 1, so a one-second limit is meaningful.

TERM before KILL, with a grace period. The measurement above is the justification: TERM keeps the log and KILL empties it, so the hard kill is a last resort and the wrapper says so when it reaches for it.

timedout recorded, and limit_s with it. A run that hit a four-second bound and a run that hit a two-hour bound are different events, and the limit is part of the record for the same reason Chapter 32.2 records the slot count.

And it exits 0 regardless. The wrapper's job is to bound and record, not to judge — Chapter 32.2's separation of dispatch from interpretation. The verdict comes from Chapter 32.3 reading timedout and the log together.

4. The Bound Is Approximate, and by How Much

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  LIMIT_S    actual     overshoot
  1s         1s         0s
  2s         2s         0s
  4s         5s         1s

A four-second limit produced a five-second run. Two reasons, both unavoidable in a shell.

sleep 0.1 is at least 0.1 seconds, and the loop spawns a process per iteration, so forty iterations take more than four seconds.

And the grace period adds up to two more. TERM, then up to 2 s of polling, then KILL — paid only when the bound fires, which is exactly the case being measured.

So the useful statement is: a shell time bound is accurate to within a few seconds and a small percentage, and both matter at different scales.

limitwhat the imprecision costs
1–5 sthe grace period dominates; the overshoot can be 25%
minutesnegligible
hoursnegligible in relative terms; the grace period is still ~2 s

Which gives the sizing rule: set the limit an order of magnitude above the expected runtime, never just above it. A limit chosen to be 10% above a two-hour run will fire on the first busy night and report a timeout for a run that was merely slow — and Chapter 33.8 measured why that is expensive, because a TIMEOUT that disappears on the next run is recorded as a fix.

5. Composing It With the Rest of the Module

A bound on its own answers "did it finish". The other chapters answer "did it work", and the order they compose in is fixed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# run_one -- one simulation, with every guard in this module around it.
set -u
root=$(cd "$(dirname "$0")/.." && pwd)
t=${1:?need a test}; s=${2:-1}; d=${3:?need an output directory}
mkdir -p "$d" || exit 2

# 34.5 -- bound what one run can write, so it cannot take the others with it
( ulimit -f "${MAX_RUN_BLOCKS:-5242880}"
  # 34.4 -- bound how long it may take, and record that the bound fired
  "$root/bin/bounded" "${LIMIT_S:-7200}" "$d" \
      vvp "$root/logs/sim.vvp" "+TESTNAME=$t" "+SEED=$s" "+CYCLES=${CYCLES:-200}" )

# 34.2 -- the log must exist, end properly, and state exactly one verdict
"$root/bin/logcheck" "$d/run.log" >/dev/null 2>&1
printf '%s\n' "$?" > "$d/logcheck.rc"

# 34.5 -- was it a space failure rather than a design one?
"$root/bin/spaceclass" "$d" > "$d/space.txt" 2>&1 || true

# 33.8 -- the machine facts that are gone in seconds, recorded non-fatally
"$root/bin/snapshot" "$d/machine" 2>/dev/null || echo "snapshot failed" >&2
exit 0

Four properties of that composition.

The caps are outermost and the checks are innermost. ulimit must be set before the process starts and cannot be applied afterwards; logcheck can only run once there is a log. That ordering is forced, not chosen.

Every guard writes a file and none of them decides. tool.rc, timedout, limit_s, logcheck.rc, space.txt, machine/ — six artifacts, each from the component that knows, and Chapter 32.4's collector turns them into columns. This is Chapter 32.2's separation of dispatch from interpretation, and it is why the wrapper ends in exit 0.

The || true appears exactly twice, on spaceclass and snapshot — both notes beside the result rather than inputs to it, which is Chapter 34.7's test. Everywhere else a failure must reach the caller.

And each limit comes from the environment with a default. ${LIMIT_S:-7200}, ${MAX_RUN_BLOCKS:-5242880} — so one long test can be given a longer bound without editing the wrapper, which is the difference between a wrapper that is reused and one that is copied.

6. Where the Bound Goes

On every tool invocation, not on the regression. A bound around the whole regression stops it mid-flight and loses every run still in progress — which Chapter 31.7 measured as NOSTATUS, four directories with zero-byte logs after the launcher was killed. A bound per invocation loses one run.

Including the compile. A compile can hang on a network filesystem or a licence wait, and an unbounded compile holds up everything behind it. Chapter 34.1's wrapper should be called through the bound, not instead of it.

And the limit comes from configuration, not from the script:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LIMIT_S=${LIMIT_S:-7200} bounded "$LIMIT_S" "$d" vvp "$image" "+TESTNAME=$t"

A limit hardcoded in a wrapper is a limit nobody can raise for one long test, which is how the wrapper gets copied and modified instead of reused — the failure mode Module 35 is about.

Practice

  1. Check whether timeout is installed on the machine your flow runs on, and on the machine you develop on.
  2. Run the registry's own hint and see what happens here.
  3. Send each of TERM, INT, HUP and KILL to a hung tool and record the status and the log size.
  4. Run the same harness against sleep as a control.
  5. Find which signal keeps the log and which keeps the status.
  6. Explain why no signal gives both.
  7. Record a timedout flag and say which process is the only one that can.
  8. Check it before the log in a verdict function, and construct the case that justifies the order.
  9. Write the bound with exec, a polling loop, and a TERM-then-KILL escalation.
  10. Explain why exec matters for the kill.
  11. Explain why a blocking wait cannot be used.
  12. Measure requested against actual for three limits.
  13. Account for the overshoot in two parts.
  14. Decide a sizing rule and say what a 10%-above limit does on a busy night.
  15. Record limit_s alongside the flag and justify it.
  16. Make the wrapper exit 0 regardless and say who judges.
  17. Add a watchdog inside the testbench and compare what it produces with what a kill produces.
  18. Say what the simulation-time bound cannot catch.
  19. Measure one heavy run alone and eight in parallel, then set a bound that survives both.
  20. Put the bound on an invocation rather than on the regression, and say what the other way loses.
  21. Bound the compile too.
  22. Take the limit from configuration and say what hardcoding it causes.

Check Your Understanding

  1. Why is a hang worse than a failure?
  2. Is timeout(1) available here? What about gtimeout?
  3. Why state that rather than silently substituting?
  4. Give the four signals and what each produced.
  5. What does the sleep control row establish?
  6. Why is TERM yielding 0 dangerous for a timeout wrapper?
  7. What does INT do, and what does that mean for an escalation?
  8. What does KILL cost?
  9. Why does no signal give you both the status and the evidence?
  10. What does the wrapper do instead, and who is the only one who can?
  11. Why must the flag be checked before the log?
  12. State the generalisation about recording outcomes.
  13. Why exec in the subshell?
  14. Why a polling loop rather than wait?
  15. Why sleep 0.1?
  16. Why TERM before KILL, and why announce the escalation?
  17. What were the three measured limits and overshoots?
  18. Account for the overshoot.
  19. At which scales does the imprecision matter?
  20. State the sizing rule and what violating it causes.
  21. What is a timeout for?
  22. Why is it not a performance check, and where does that question belong?
  23. What does a testbench watchdog produce that a kill does not?
  24. What can a simulation-time bound not catch?
  25. Why can a bound measured on an idle machine fire under load?
  26. Why bound each invocation rather than the regression?
  27. Why bound the compile?
  28. Why take the limit from configuration?

7. Remember

  • timeout(1) is not installed here, and nor is gtimeout. The registry's own hint does not run on this machine — say so rather than substituting quietly.
  • TERM makes the simulator exit 0 and keeps its log; KILL gives 137 and empties it; INT is ignored. No signal gives you both the status and the evidence.
  • So the wrapper records timedout itself, and the verdict function checks it first — a TERMed log can contain a stale TEST PASSED.
  • When an outcome cannot be read back, the actor records it. Same rule as the build manifest and the machine snapshot.
  • exec so $! is the tool; poll rather than wait; sleep 0.1 so a short limit means something.
  • TERM, a grace period, then KILL — and announce the escalation, because it explains the empty log.
  • A 4-second limit took 5 seconds. The overshoot is the loop plus up to 2 s of grace: 25% at seconds, negligible at hours.
  • Size the limit an order of magnitude above the expected runtime, and measure it at the parallelism the regression actually uses — contention alone gave a 4.5× spread.
  • A timeout is not a performance check. Put the runtime in the results file and let a trend read it.
  • Prefer a watchdog inside the testbench, which produces a log with a verdict; keep the shell bound as the backstop.
  • Bound every invocation, including the compile, never the regression — and take the limit from configuration.

8. What Is Next

A bound stops a run that will not finish. Chapter 34.5 handles the run that cannot finish — one that runs out of space halfway — and measures what lands in the log when that happens, which turns out to be a message from the wrong program entirely.

Where this fits

Part of the Linux & Bash curriculum.