Forking a running simulator so the child simulates on (eager replay): PDEATHSIG race, wait4 accounting, end-of-sim wait
Setup: a VPI/PLI system function, called from the testbench every N cycles, fork()s the simulator process. The child turns on a waveform dump and simulates the next N cycles, then $finishes. The parent goes on without dumping. Things that mattered, tested with Icarus Verilog plus a fake host in a container:
- Return value: make it a system FUNCTION (vpiSysFunc, vpiIntFunc) returning 1 in the child, 0 in the parent and -1 when refused. Set the value with vpi_put_value on the vpiSysTfCall handle. A stub that registers the function without the real library should return 0, so the testbench takes the parent path and runs unchanged. - Make the folder in the PARENT before fork (mkdir, refusing one that exists). The refusal is then reported by the process that can log it. The child chdir()s there, so files the simulator or dumper make by relative name (dump file, the dumper's own log) are per child. - Child set-up before returning: give each inherited writable fd its own open file description (reopen by path at the same offset). Redirect stdout/stderr to a per-child log. Put a pipe nobody writes on stdin, so the child never reads the parent's command channel. Call fflush(NULL) in the parent BEFORE fork, or buffered output is printed twice. - prctl(PR_SET_PDEATHSIG, SIGKILL), then check getppid() == the recorded parent pid. If the parent died between fork and prctl, the signal never comes, so _exit. A test that SIGKILLs the parent and checks /proc/<child> is gone catches a missing PDEATHSIG. - Collect children with wait4(pid, &st, WNOHANG, &ru) at every call. You get each child's own CPU (ru_utime/ru_stime) and peak RSS (ru_maxrss) for free. Note that /usr/bin/time over the parent also includes the children's CPU once they are reaped. - At end of simulation (cbEndOfSimulation), wait for running children, bounded, and keep applying each child's own timeout inside that wait loop. Our first version only checked timeouts at calls, so a hung child made the parent sit for the whole end-wait. - Cap concurrent children and REFUSE past the cap rather than block, so the parent never stalls. The refusal shows up as a missing chunk, which fails the row. - Refuse when the simulator has more than one thread (/proc/self/status Threads): fork copies only the calling thread.
Test-harness gotcha found on the way: ok() { if eval "$2"; ... } with checks like 'for i in ...; do ... || exit 1; done' makes a failing check EXIT THE WHOLE TEST SCRIPT, with no FAIL line and no summary. Use if (eval "$2") so the exit stays in a subshell. Mutation runs (delete prctl, delete chdir) are what exposed it.