返回

文章详情

安全的无锁原语与iceoryx2的ByteAtomic

Hacker News2026年8月4日 10:35

Marika Lehmann - 2026年7月28日 数据竞争与序列锁 在多线程编程中,一个常见场景是多个线程同时读取和修改共享数据。如果这些读写操作不是原子的,就会发生数据竞争。在像Rust和C++这样的语言中,它们几乎有相同的内存模型,这会导致未定义行为。为了防止这种情况,可以使用锁来保护数据在读取时不被修改。然而,传统的锁机制存在死锁的风险,这在安全关键和高可靠性系统中特别不可接受。 改善上述数据竞争而不使用阻塞锁的一种常见方法是利用序列锁。序列锁包含共享数据和一个原子计数器,在数据更新时计数器的值为奇数:使用序列锁,写线程将序列计数器递增至奇数值,更新数据,然后将计数器递增至偶数值。读线程在复制共享数据的前后读取序列计数器。如果计数器已改变或当前是奇数,则表示数据被并发修改。然后,读线程丢弃损坏的副本并重试。 问题:即使读取者检测到数据被修改并在使用前丢弃副本,复制非原子数据的行为本身仍然会触发未定义行为。虽然序列锁可以检测数据竞争的发生,但并不能防止它。因此,目前在Rust或C++中实现一个正确的序列锁是不可能的,除非将数据分解成更小、各自原子的部分。这是一个已知问题,虽然有正在进行的提案以将“原子memcpy”引入Rust和C++标准库,但我们暂时不能依赖该特性。 针对安全关键和高可靠性系统,iceoryx2提供了一种基于类似于序列锁的机制的无锁构造库。为了使这些构造安全且正确,我们需要一种方法,以进行字节级别的原子内存复制,确保不会发生数据竞争。这就是我们实现字节级原子包装器ByteAtomic的原因,我们将在接下来的部分进行描述。虽然其概念很简单,但要实现真正的安全性需要克服未初始化内存的微妙但关键问题。 解决方案:字节级原子包装器 为了防止上述数据竞争以及未定义行为,iceoryx2中的ByteAtomic提供了对其内部类型的字节级原子读写操作。此包装器仅保证每个字节以原子方式更新/读取;它不提供更高级别的线程安全保证。用户仍然必须强制执行正确的同步(例如序列锁)以防止撕裂读取或写入。该包装器仅确保内存复制不是未定义行为,但它本身并不保证数据完整性。 实现 该包装器的实现经过一些优化,因为我们解决了内存安全的复杂性。我们最初版本的ByteAtomic包装器如下:它被命名为FixedSizeByteAtomic,因为数组大小必须在编译时提供,Rust尚不允许在结构定义中直接使用core::mem::size_of::<T>()。一旦这变得可能,我们计划删除SIZE通用参数,删除运行时固定大小版本RelocatableByteAtomic,并将结构重命名为ByteAtomic。 填充字节 为了理解为什么实现必须演变,让我们看看new()的初始天真实现:该版本的new()接受一个可复制的值,执行transmute_copy到一个字节数组,并将每个字节作为AtomicU8存储到ByteAtomic的数据字段中。这工作得很好——除非T包含未初始化的内存,例如MaybeUninit或填充字节:transmute_copy假定被复制的值是目标类型的有效表示,对于我们来说是有效的u8。这个假设对于填充字节失败,因为填充字节是未初始化的内存;读取它们会导致未定义行为。因此,我们必须确保只复制传入值的字段(即初始化的字节)。这导致了当前正确的new()实现:我们现在要求内在类型T实现iceoryx2中的AtomicCopy特质,以便可以进行原子复制。它提供了for_each_field(),这是一个用于字节级复制的字段访问器。该方法将提供的回调应用于T的每个字段的偏移量-大小对。通过这个,new()仅将值的初始化字节复制到数据字段,有效地跳过潜在的填充字节。当然,AtomicCopy特质的实现必须确保每个字段的偏移量和大小被正确计算;否则,仍然可能发生未定义行为。 请注意,read()的返回类型也经历了演变。它的在

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡