Part 2 · OOP for Verification · Intermediate
disable fork, wait fork & process
disable fork scoping dangers, isolation with nested fork begin..end, wait fork, and std::process control.
disable fork: a chainsaw, not a scalpel
disable fork kills every child process spawned by the current process that is still running — not just the children of the most recent fork statement. If your task spawned a background monitor with join_none earlier and later uses join_any plus disable fork for a timeout race, the disable kills the monitor too. This sibling-killing behavior is the single most common source of "my monitor silently stopped mid-test" bugs.
DISABLE FORK BLAST RADIUS
task run(); With ISOLATION wrapper:
fork mon.run(); join_none ─ M fork begin ┐
... fork │ inner
fork main_seq(); ─ S │ parent
main_seq(); ─ S #1ms timeout ─ T │ P'
#1ms; ─ T join_any │
join_any disable fork; ← kills │
disable fork; ← kills S or T only S/T (children of P')│
AND M (sibling!) end join ┘
M survives: it is a child of
M, S, T are ALL children of run() run(), not of the inner P'.
→ all in the blast radius.The isolation idiom: fork begin ... end join
task run();
fork mon.run(); join_none // background daemon — must survive
// Isolation wrapper: ONE child (the begin..end), which becomes the
// parent of the racing pair. disable fork inside it cannot reach mon.
fork
begin
fork
main_sequence(); // the work
#1ms; // the timeout
join_any
disable fork; // kills only main_sequence / timeout
end
join // wait for the wrapper itself
endtaskwait fork and std::process
wait fork blocks the current process until ALL of its spawned children complete — the cleanup companion to join_none. A test that spawned daemons and short-lived workers calls wait fork before reporting, so no result is read while a checker is still mid-flight. Like disable fork, it applies to all children of the current process, so structure your forks accordingly.
For per-thread control, std::process gives each spawned thread a handle: process::self() captured inside the child can be stored, then the parent can kill() exactly one thread, await() its completion, suspend() / resume() it, or query status(). This is the scalpel that disable fork is not — UVM's phase machinery and sequence kill logic are built on it.
class worker_pool;
process workers[3]; // handles, one per thread
task start();
foreach (workers[i]) begin
automatic int idx = i;
fork
begin
workers[idx] = process::self(); // FIRST statement: register
forever begin
#(10 * (idx + 1));
$display("[%0t] worker %0d tick", $time, idx);
end
end
join_none
end
endtask
task stop_one(int idx);
if (workers[idx] != null && workers[idx].status() != process::FINISHED)
workers[idx].kill(); // surgical: siblings unaffected
endtask
task drain();
foreach (workers[i])
if (workers[i] != null) workers[i].await(); // wait for THIS one
endtask
endclassThe timeout watchdog pattern
Every robust test wraps its main activity in a watchdog so a stuck handshake fails loudly at a bounded time instead of running until the LSF job is killed. The pattern combines everything from this lesson: isolation wrapper, join_any race, and disable fork.
task automatic run_guarded(time limit);
bit timed_out;
fork
begin // isolation wrapper
fork
begin
stimulus_and_checks(); // the real test body
end
begin
#limit;
timed_out = 1;
end
join_any
disable fork; // safe: only the pair above
end
join
if (timed_out)
$fatal(1, "[%0t] WATCHDOG: test exceeded %0t", $time, limit);
else
$display("[%0t] test finished within budget", $time);
endtaskInterview angle
"What exactly does disable fork kill?" — all live children of the calling process, including earlier join_none spawns.
"How do you kill one thread without touching others?" — store process::self() handles and call kill() on the target.
"Why wrap join_any/disable fork in fork begin..end join?" — to shrink the blast radius to the intended pair.
Key takeaways
disable fork kills ALL live children of the current process — wrap races in fork begin..end join to isolate.
wait fork is the cleanup barrier for join_none daemons before final checks and reports.
std::process handles (self/kill/await/suspend/status) give surgical per-thread control.
The watchdog pattern — isolation wrapper + join_any + disable fork — belongs in every top-level test.
Common pitfalls
Bare disable fork after a timeout race — silently kills background monitors spawned earlier in the same task.
Registering process::self() after a blocking statement — the parent may act on a still-null handle.
Calling kill() on a thread holding a semaphore key — the key is never put back; later gets hang.
Using disable LABEL on a task enabled from multiple threads — disables every active instance, not just yours.