读一次,写一次,但不是为 Rust 设计的。
Read_once(), Write_once(), but Not for Rust

原始链接: https://lwn.net/SubscriberLink/1053142/8ec93e58d5d3cc06/

## Rust 与 C 在 Linux 内核中的并发性 Linux 内核开发社区最近讨论了 C 和 Rust 代码之间并发数据访问的不同方法。内核大量使用 `READ_ONCE()` 和 `WRITE_ONCE()` 宏来实现无锁算法和设备内存访问,但这些宏缺乏全面的文档。一项旨在实现 Rust 等效功能的补丁遭到了反对。 Rust 开发者认为,不应直接镜像这些宏,而是倾向于使用更明确和精确的 `Atomic` crate 来定义并发保证。他们认为 `READ_ONCE()`/`WRITE_ONCE()` 是“权宜之计”,掩盖了意图,更喜欢在指定原子操作时保持清晰。 虽然 Rust 补丁已被撤回,但这场争论促使现有的 C 代码得到了有价值的改进。它揭示了需要 `WRITE_ONCE()` 的实例以及缺失的 `READ_ONCE()` 调用,从而引发了更新。这种差异可能导致 Rust 和 C 在访问相同数据时采用截然不同的并发处理方式,从而可能使开发复杂化。这场讨论引发了一个问题:Rust 的优越设计是否应该影响 C 端的改进,尽管这将是一项巨大的工程。

最近Hacker News上的一场讨论集中在Rust处理并发数据访问的方式上,特别是Linux内核中使用的`read_once`操作。核心问题是Rust不同的方法是否会在Linux生态系统中创造出两层开发体验。 一位评论员指出内核对`read_once`的“consume”语义是明确的,暗示Rust可能将其降级为“relaxed”操作是不正确的。这引发了一个关于Rust如何实现`std::memory_order_consume`的疑问,该特性被认为无法在C++中实现。 另一位评论员澄清说Rust使用了C++20内存排序,暗示这是一种不同的,但可能兼容的解决方案。这场对话突显了关于内存模型和并发的细微争论,尤其是在内核开发以及Rust集成到Linux内核的背景下。
相关文章

原文

Welcome to LWN.net

The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!

By Jonathan Corbet
January 9, 2026

The READ_ONCE() and WRITE_ONCE() macros are heavily used within the kernel; there are nearly 8,000 call sites for READ_ONCE(). They are key to the implementation of many lockless algorithms and can be necessary for some types of device-memory access. So one might think that, as the amount of Rust code in the kernel increases, there would be a place for Rust versions of these macros as well. The truth of the matter, though, is that the Rust community seems to want to take a different approach to concurrent data access.

An understanding of READ_ONCE() and WRITE_ONCE() is important for kernel developers who will be dealing with any sort of concurrent access to data. So, naturally, they are almost entirely absent from the kernel's documentation. A description of sorts can be found at the top of include/asm-generic/rwonce.h:

Prevent the compiler from merging or refetching reads or writes. The compiler is also forbidden from reordering successive instances of READ_ONCE and WRITE_ONCE, but only when the compiler is aware of some particular ordering. One way to make the compiler aware of ordering is to put the two invocations of READ_ONCE or WRITE_ONCE in different C statements.

In other words, a READ_ONCE() call will force the compiler to read from the indicated location exactly one time, with no optimization tricks that would cause the read to be either elided or repeated; WRITE_ONCE() will force a write under those terms. They will also ensure that the access is atomic; if one task reads a location with READ_ONCE() while another is writing that location, the read will return the value as it existed either before or after the write, but not some random combination of the two. These macros, other than as described above, impose no ordering constraints on the compiler or the CPU, making them different from macros like smp_load_acquire(), which have stronger ordering requirements.

The READ_ONCE() and WRITE_ONCE() macros were added for the 3.18 release in 2014. WRITE_ONCE() was initially called ASSIGN_ONCE(), but that name was changed during the 3.19 development cycle.

On the last day of 2025, Alice Ryhl posted a patch series adding implementations of READ_ONCE() and WRITE_ONCE() for Rust. There are places in the code, she said, where volatile reads could be replaced with these calls, once they were available; among other changes, the series changed access to the struct file f_flags field to use READ_ONCE(). The implementation of these macros involves a bunch of Rust macro magic, but in the end they come down to calls to the Rust read_volatile() and write_volatile() functions.

Some of the other kernel Rust developers objected to this change, though. Gary Guo said that he would rather not expose READ_ONCE() and WRITE_ONCE() and suggested using relaxed operations from the Rust Atomic crate the kernel's Atomic module instead. Boqun Feng expanded on the objection:

The problem of READ_ONCE() and WRITE_ONCE() is that the semantics is complicated. Sometimes they are used for atomicity, sometimes they are used for preventing data race. So yes, we are using LKMM [the Linux kernel memory model] in Rust as well, but whenever possible, we need to clarify the intention of the API, using Atomic::from_ptr().load(Relaxed) helps on that front.

IMO, READ_ONCE()/WRITE_ONCE() is like a "band aid" solution to a few problems, having it would prevent us from developing a more clear view for concurrent programming.

In other words, using the Atomic crate allows developers to specify more precisely which guarantees an operation needs, making the expectations (and requirements) of the code more clear. This point of view would appear to have won out, and Ryhl has stopped pushing for this addition to the kernel's Rust code — for now, at least.

There are a couple of interesting implications from this outcome, should it hold. The first of those is that, as Rust code reaches more deeply into the core kernel, its code for concurrent access to shared data will look significantly different from the equivalent C code, even though the code on both sides may be working with the same data. Understanding lockless data access is challenging enough when dealing with one API; developers may now have to understand two APIs, which will not make the task easier.

Meanwhile, this discussion is drawing some attention to code on the C side as well. As Feng pointed out, there is still C code in the kernel that assumes a plain write will be atomic in many situations, even though the C standard explicitly says otherwise. Peter Zijlstra answered that all such code should be updated to use WRITE_ONCE() properly. Simply finding that code may be a challenge (though KCSAN can help); updating it all may take a while. The conversation also identified a place in the (C) high-resolution-timer code that is missing a needed READ_ONCE() call. This is another example of the Rust work leading to improvements in the C code.

In past discussions on the design of Rust abstractions, there has been resistance to the creation of Rust interfaces that look substantially different from their C counterparts; see this 2024 article, for example. If the Rust developers come up with a better design for an interface, the thinking went, the C side should be improved to match this new design. If one accepts the idea that the Rust approach to READ_ONCE() and WRITE_ONCE() is better than the original, then one might conclude that a similar process should be followed here. Changing thousands of low-level concurrency primitives to specify more precise semantics would not be a task for the faint of heart, though. This may end up being a case where code in the two languages just does things differently.




联系我们 contact @ memedata.com