Difficulty: Medium · Puzzle 3 of 6 · Topic: Clocking Block Puzzles
Predict every line printed and its time. Does the background thread survive the disable fork? What would change if the inner fork were not wrapped in fork begin ... end join?
`timescale 1ns/1ns
module tb;
initial begin
fork
begin #30; $display("[%0t] background done", $time); end
join_none
fork
begin // isolating wrapper
fork
begin #10; $display("[%0t] fast done", $time); end
begin #20; $display("[%0t] slow done", $time); end
join_any
disable fork;
end
join
$display("[%0t] after join_any", $time);
#40 $display("[%0t] end", $time);
$finish;
end
endmoduledisable fork kills every active child (and descendant) of the process that executes it, not just the children of the last fork.
1) The first fork/join_none starts a background thread (t=30) as a child of the initial process and does not wait for it.
2) The wrapper fork begin ... end join runs one child process. The inner fork is created inside that child, so fast and slow are descendants of the wrapper child only.
3) join_any returns at t=10, right after fast prints. disable fork then executes inside the wrapper child and kills only its own descendants: slow (due at t=20) is killed and never prints.
4) The wrapper child ends, the outer join returns, and [10] after join_any prints. The background thread is a child of the initial process, not of the wrapper, so it is untouched and prints [30] background done.
5) Output: [10] fast done, [10] after join_any, [30] background done, [50] end. The first two lines have a defined order (fast prints before its process ends).
6) Without the wrapper, disable fork would run in the initial process itself, whose descendants include the background thread, so [30] background done would be silently lost. Verified in Verilator 5.048: wrapper keeps it, no wrapper loses it (output is [10] fast done, [10] after join_any, [50] end).
7) Tool note: Icarus kills the background thread in both versions (it does not honour the wrapper isolation), so use Verilator or the LRM (9.6.3) as the reference.Answer: [10] fast done, [10] after join_any, [30] background done, [50] end. slow never prints (killed at t=10). The background thread survives only because the join_any/disable fork pair is isolated in a fork begin ... end join wrapper; without it disable fork would also kill the background thread and [30] background done would disappear.
Checked by running the code in Verilator 5; the printed output supports the answer above. The wrapper keeps disable fork from killing the background thread; slow is killed. Without the wrapper, disable fork also kills the background thread.
disable fork is not scoped to the fork statement right before it: it disables all active descendants of the calling process. Putting it in a bare initial or task body can kill threads started long before (a monitor, a watchdog, a driver loop). Always isolate the join_any plus disable fork pair.
A task uses join_any plus disable fork and is called from a UVM run_phase that also has forked background monitors. What do you do? Model answer: wrap the join_any/disable fork pair in fork begin ... end join (or call it from a helper task spawned inside such a wrapper) so only the wrapper's own children are killed; alternatively keep handles of the specific processes with process::self() and kill those individually.
The background thread survives, and slow is killed. Disable fork kills all active descendants of the process that calls it, and the wrapper makes the calling process a child that owns only fast and slow. So the output is fast done and after join_any at ten, then background done at thirty, then end at fifty. The trap is thinking disable fork is limited to the fork just before it; without the wrapper it would kill the background thread too. Icarus does not honour that isolation, so I would use the LRM or Verilator as the reference.