QEMU/AddressSpace / FlatView

用 RCU 发布内存映射视图并延后回收

QEMU 重建 FlatView 后发布新指针,已有读者继续使用旧视图,直到引用归还且 RCU 宽限期结束才销毁。

v9.2.0并发与一致性核验 2026-10-09

工程问题#

设备热插拔或内存映射变更会更新地址空间,而 vCPU 与 IOThread 还在读取映射。逐次保护 current_map 的互斥锁会让这些读者竞争;直接替换并释放又会留下悬空指针。

实际实现#

构造新的 FlatView 与 dispatch,更新者在 BQL 保护下执行区域差异回调并通过 qatomic_rcu_set 发布。读侧在 RCU 临界区中读取固定的视图指针,旧视图最后一次 unref 使用 call_rcu 延后销毁。

处理流程#

  1. 在旧视图之外构造新映射和 dispatch
  2. 更新者在 BQL 保护下执行区域差异回调并发布 current_map
  3. 读者在 RCU 临界区内使用所取得的视图
  4. 旧视图引用归零后,在 RCU 宽限期结束时销毁

源码片段#

固定版本 · 原文摘录
system/physmem.cv9.2.0 · L2942–L2955
查看原文件
2942MemTxResult address_space_read_full(AddressSpace *as, hwaddr addr,2943                                    MemTxAttrs attrs, void *buf, hwaddr len)2944{2945    MemTxResult result = MEMTX_OK;2946    FlatView *fv;29472948    if (len > 0) {2949        RCU_READ_LOCK_GUARD();2950        fv = address_space_to_flatview(as);2951        result = flatview_read(fv, addr, attrs, buf, len);2952    }29532954    return result;2955}
address_space_read_full

读侧先进入 RCU 临界区,再取得当前 FlatView,并在同一临界区内完成读取。保护的是映射对象生命周期;设备回调和 RAM 内容仍有各自同步规则。

system/memory.cv9.2.0 · L1099–L1119
查看原文件
1099    if (old_view) {1100        flatview_ref(old_view);1101    }11021103    flatview_ref(new_view);11041105    if (!QTAILQ_EMPTY(&as->listeners)) {1106        FlatView tmpview = { .nr = 0 }, *old_view2 = old_view;11071108        if (!old_view2) {1109            old_view2 = &tmpview;1110        }1111        address_space_update_topology_pass(as, old_view2, new_view, false);1112        address_space_update_topology_pass(as, old_view2, new_view, true);1113    }11141115    /* Writes are protected by the BQL.  */1116    qatomic_rcu_set(&as->current_map, new_view);1117    if (old_view) {1118        flatview_unref(old_view);1119    }
address_space_set_flatview

更新者持有 BQL,先持有新旧视图引用并调用 MemoryListener 的区域差异回调,再用 qatomic_rcu_set 发布新视图。事务级 commit 回调在所有 AddressSpace 更新后执行。旧视图的临时引用在完整函数末尾另行归还,不能把片段中的一次 unref 理解为全部生命周期处理。

system/memory.cv9.2.0 · L310–L317
查看原文件
310void flatview_unref(FlatView *view)311{312    if (qatomic_fetch_dec(&view->ref) == 1) {313        trace_flatview_destroy_rcu(view, view->root);314        assert(view->root);315        call_rcu(view, flatview_destroy, rcu);316    }317}
flatview_unref

最后一个引用归还后,call_rcu 登记延迟销毁回调;回调在登记时已经存在的 RCU 读侧临界区全部结束后执行。普通引用计数和 RCU 各自保护不同的使用方式。

system/memory.cv9.2.0 · L758–L777
查看原文件
758    view = flatview_new(mr);759760    if (mr) {761        render_memory_region(view, mr, int128_zero(),762                             addrrange_make(int128_zero(), int128_2_64()),763                             false, false, false);764    }765    flatview_simplify(view);766767    view->dispatch = address_space_dispatch_new(view);768    for (i = 0; i < view->nr; i++) {769        MemoryRegionSection mrs =770            section_from_flat_range(&view->ranges[i], view);771        flatview_add_to_dispatch(view, &mrs);772    }773    address_space_dispatch_compact(view->dispatch);774    g_hash_table_replace(flat_views, mr, view);775776    return view;777}
generate_memory_topology

先把 MemoryRegion 拓扑展开为互不重叠的地址区间,再合并可兼容的相邻区间并构造 dispatch。构造完成后才供 AddressSpace 发布,不在现有读者使用的视图上逐项修改。

读侧、发布、回收与视图构造分成连续短片段。发布函数还有引用归还逻辑,需结合完整函数阅读。

收益与取舍#

工程分析

带来的收益

  • 读者无需为取得 current_map 竞争历史实现中的 flat_view_mutex
  • 视图发布与旧对象销毁分离,避免读者拿到映射后对象立即被释放

付出的代价

  • 新旧视图可能同时占用内存,回收会被尚未退出的读侧临界区推迟
  • RCU 只覆盖相应映射对象访问;写侧仍需 BQL,设备寄存器和 RAM 数据仍需各自同步

可比较的选择

flat_view_mutexQEMU 历史实现曾用该互斥锁取得 current_map 引用;原作者改用 RCU 的理由是减少大系统、多 IOThread 场景的 futex 争用。

替换指针后立即释放旧视图虽然代码短,却不能保护已经读取旧指针的线程,不是可用的并发实现。

版本与适用范围#

固定于 QEMU v9.2.0 的 FlatView 生命周期。2015 年提交用于说明互斥锁对照方案与设计动机;未对该版本的性能进行实测。

参考来源#

源码、官方文档与原始论文
01
address_space_read_full

system/physmem.c · address_space_read_full

源码v9.2.0
02
address_space_set_flatview

system/memory.c · address_space_set_flatview

源码v9.2.0
03
flatview_unref

system/memory.c · flatview_unref

源码v9.2.0
04
generate_memory_topology

system/memory.c · generate_memory_topology

源码v9.2.0
05源码374f2981d1f10bc4307f250f24b2a7ddb9b14be0
06官方文档v9.2.0
07官方文档v9.2.0

相关实现与比较#

4 个相关案例
6 个匹配案例