iverilog fork/join 并发:join_any、join_none 与 disable fork 的等待差异
更新时间:2026-10-31。
fork创建并行进程并不难,真正容易误判的是收尾关键字:父进程在等什么、其余子进程是否还活着、父initial结束后 vvp 会不会立即退出。本篇用同一组延时子进程实测 iverilog fork/join 并发,避免只凭语句名称猜调度结果。
本文要回答的问题
join、join_any、join_none分别让父进程在什么时刻继续?join_any恢复后,尚未结束的子进程会被自动取消吗?join_none后面的语句与零延时子进程,哪一个先执行?disable fork能终止哪些进程,父进程结束后仿真是否自动退出?
先说答案:Icarus Verilog 13.0 本次实测支持这四种写法,编译和运行均未出现 warning 或 error。join 等全部子进程,join_any 只等第一个且不取消其余进程,join_none 不等待;分离子进程仍有未完成的延时事件时,即使父 initial 已结束且没有调用 $finish,仿真也会继续到事件耗尽。
一、iverilog fork/join 并发先看父进程在哪里停住
fork 块中的各条分支并行执行,块尾关键字决定父进程的汇合策略。这里的父进程指执行该 fork 的进程,不是操作系统进程;这些分支都由同一个 vvp 仿真器调度,共享仿真时间与变量。
读图时把「分支是否并行」与「父进程是否等待」分开:三种写法的分支创建方式相同,差别只在橙色或紫色的汇合点。join_none 还有一个容易漏看的细节——父进程继续执行时,新子进程尚未开始,要等各分支首次真正运行后,其中的语句才会产生效果。
| 收尾写法 | 父进程恢复条件 | 其余子进程去向 | 典型用途 |
|---|---|---|---|
join | 所有直接子进程结束 | 已全部结束 | 等一组激励或检查全部完成 |
join_any | 任意一个直接子进程结束 | 未结束的继续运行 | 等首个响应,但需自行处理其余分支 |
join_none | 不等待,立即继续 | 全部继续,等待调度运行 | 启动后台监测或独立激励 |
二、最小实验:相同延时只替换汇合方式
测试平台给短分支 10 ns、长分支 20 ns,并用 $timeformat 把 $time 显示为纳秒。每组之间留出足够间隔,避免前一组后台分支混入后一组日志。核心结构如下,完整文件还包含零延时分支、终止分支和自然退出用例:
fork
begin
#10;
$display("%0t short child done", $time);
end
begin
#20;
$display("%0t long child done", $time);
end
join_any
$display("%0t parent resumed", $time);下面是一次完整执行。前三行记录命令、日期和工具链,随后按用例输出;原始日志没有补写或调整顺序。
第一组在 0 ns 分叉,短、长子进程分别于 10 ns 和 20 ns 完成,父进程到 20 ns 才打印 after join。第二组从 50 ns 开始,父进程在短分支结束的 60 ns 随即恢复,但长分支仍在 70 ns 打印完成。可见 join_any 不是「留下赢家、杀掉输家」,它只是解除父进程的等待。
CASE fork-join_any
50 ns parent before fork
60 ns any short child done
60 ns parent after join_any
70 ns any long child done
CASE fork-join_none
90 ns parent before fork
90 ns parent after join_none
90 ns none immediate child started
100 ns none delayed child done
CASE disable-fork
110 ns parent before fork
120 ns disable short child done
120 ns parent after join_any
120 ns disable fork executed
160 ns parent after quiet wait
CASE natural-exit
170 ns parent initial block ends without finish
190 ns detached child done这段原始输出把后三组的关键顺序放在一起:join_any 后仍有 70 ns 的长分支记录,join_none 的父进程记录先于同刻子进程,disable fork 后则没有长分支的失败文本。最后的 190 ns 记录也证明父块结束后还有子进程事件在执行。
三、join_none 的同时刻顺序与仿真自然退出
第三组从 90 ns 开始,日志顺序是父进程 after join_none,然后才是零延时的 immediate child started,两者时间戳都为 90 ns。时间相同不代表执行顺序无法区分:子进程虽然已被创建,但在父进程阻塞或结束之前尚未开始,所以父进程中紧跟 join_none 的语句先执行。
这个语义对变量初始化很重要。若父进程在 join_none 后马上修改共享变量,不要假设后台分支已经读到旧值;反过来,也不能把本次打印顺序扩张成任意的跨模拟器同步协议。需要确定先后关系时,应使用事件、旗标或显式延时建立同步,而不是依赖日志碰巧整齐。
最后一个用例在 170 ns 执行 join_none,随后父 initial 打印结束信息。源码没有 $finish,但 vvp 没有在这一刻退出,而是在分离子进程于 190 ns 完成后自然结束。原因是仿真器仍有一个未来事件要处理;父块结束只结束这一条执行线程,不等于清空整个事件队列。
| 观察点 | 本次时间 | 能得出的结论 |
|---|---|---|
父块结束,未调用 $finish | 170 ns | 父线程不再执行后续语句 |
| 分离子进程完成 | 190 ns | 子进程和延时事件在父块结束后仍有效 |
| vvp 返回 | 190 ns 之后 | 没有待处理事件,仿真自然结束 |
因此,测试平台看似「主代码已经走完」却仍不退出时,应检查 join_none 分支、永久循环和仍在等待的事件。若需要规定测试总时长,使用显式超时和清晰的 $finish / $fatal 策略,不要把父块结束当作停止条件。
四、disable fork 终止的是调用者的后代进程
第四组使用 join_any 等短分支在 120 ns 完成,随后父进程执行 disable fork。长分支原本应在 140 ns 打印 ERROR long child survived;父进程一直等到 160 ns,这行仍没有出现,说明该长尾分支已被终止。
fork
begin
#10;
$display("short child done");
end
begin
#30;
$display("long child done");
end
join_any
disable fork;disable fork 终止调用进程的所有后代进程,不只是当前 fork 文本块里的某一个分支。它适合配合 join_any 实现「首个结果到达,取消其余尝试」,但作用域较宽:若调用者还有其他仍需保留的后代任务,同一条语句也可能把它们一并停掉。精确取消时应把相关分支组织在明确的层级中,并检查共享变量是否停在中间状态。
还要区分终止与等待。disable fork 不表示「等它们结束」,而是使目标进程不再继续;它也不是全局停止仿真,其他无关 initial / always 进程仍可运行。需要停掉整个仿真时应使用结束系统任务,需要只停一个命名块时则应评估更窄的 disable 目标。
五、把并发测试写得更可判断
并发日志至少应带用例名、角色和仿真时间。只打印若干 done,一旦分支交错就无法判断是谁完成,也无法区分「被终止」与「尚未到时间」。本文给被终止分支安排了失败文本,并在其预定完成时刻之后再等待一段时间,让缺失的输出成为可解释证据。
实际测试平台还要避免两类隐患。其一,join_any 后忘记处理剩余分支,旧激励可能进入下一个用例并污染共享信号;其二,滥用 disable fork,把本应存活的监视器一起终止。可靠做法是为每个用例隔离状态和生命周期,结束时确认后台进程数量与共享旗标都符合预期。
如果关心的是强制覆盖与释放信号,而不是并发进程生命周期,可对照 iverilog force/release 行为。两者都可能让后续输出与直觉不同,但一个作用于进程调度,另一个作用于变量或网络的驱动状态。
实验环境
- 机器/系统:Mac / macOS,Darwin 25.5.0
- 工具版本:Icarus Verilog 13.0 stable(v13_0)
- 日期:2026-10-31
- 完整命令:
bash run.sh;脚本执行iverilog -g2012 -Wall -s tb -o fork_join_behavior.vvp fork_join_behavior.sv && vvp fork_join_behavior.vvp
代码获取
最小测试平台与一键复跑脚本位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-iverilog-fork-join-behavior。安装 iverilog 后进入该目录执行 bash run.sh,即可复现全部时间戳;源码故意不调用 $finish,以观察最后分离子进程完成后的自然退出。
这一篇在知识体系里的位置
本篇属于 EDA 排错目录中的 SystemVerilog 进程调度节点,重点处理 iverilog fork/join 并发的等待、后台生命周期与终止边界。数组或变量本身的异常读取不是同一问题;若现象是内存索引越界,应转看 iverilog 数组下标越界。
总结
iverilog fork/join 并发的关键不是 fork 如何分支,而是父进程如何收尾:Icarus Verilog 13.0 实测中,join 等全部子进程,join_any 等首个且保留其余分支,join_none 让父进程先于新子进程继续。disable fork 可清除调用者的剩余后代进程;不使用它时,分离子进程的未来事件会让 vvp 在父 initial 结束后继续运行,直到事件耗尽才自然退出。