|
|
Subscribe / Log in / New account

A memory model for Rust code in the kernel

By Jonathan Corbet
April 3, 2024
The Rust programming language differs from C in many ways; those differences tend to be what users admire in the language. But those differences can also lead to an impedance mismatch when Rust code is integrated into a C-dominated system, and it can be even worse in the kernel, which is not a typical C program. Memory models are a case in point. A programming language's view of memory is sufficiently fundamental and arcane that many developers never have to learn much about it. It is hard to maintain that sort of blissful ignorance while working in the kernel, though, so a recent discussion of how to choose a memory model for kernel code in Rust is of interest.

Memory models

It is convenient to view a system's memory as a simple array of bytes that can be accessed from any CPU. The reality, though, is more complicated. Memory accesses are slow, so a lot of effort goes into minimizing them; modern systems are built with multiple levels of caching for that purpose. Without that caching, performance would slow to a crawl, severely impacting the production, delivery, and consumption of cat videos, phishing spam, and cryptocurrency scams. This prospect is seen as a bad thing.

Multi-level caching speeds computation, but it brings a problem of its own: it is no longer true that every CPU in the system sees the same memory contents. If one CPU has modified some data in a local cache, another CPU reading that data may not see the changes. Operations performed in a carefully arranged order may appear in a different order elsewhere in the system. As is the case in relativistic situations, the ordering of events depends on who is observing them. Needless to say, this kind of uncertainty also has the potential to create disorder within an operating-system kernel.

CPUs have special operations that can ensure that a given memory store is simultaneously visible across the system. These operations, though, are slow and need to be used with care. Modern CPUs provide a range of "barrier" operations that can be used to properly sequence access to data with less overhead; see this article for an overview of some of them. Use of these barriers can be somewhat complex (and architecture-specific), so a few generic interfaces have been created to simplify things (to the extent that they can be simplified). A memory model combines a specification of how barriers should be used and any interfaces that ease that use, describing how to safely access data in concurrent settings.

"The majority of the _good_ programmers I know run away screaming from this stuff. As was said many, many years ago - understanding memory-barriers.txt is an -extremely high bar- to set as a basic requirement for being a kernel developer."
Dave Chinner, 2020
The C11 standard, for example, defines some atomic types and atomic operations. C++ offers an atomic type of its own. While the kernel community has occasionally discussed using the C atomic types, that has never happened for a number of reasons, some of which will become apparent below. Instead, the kernel defines its own memory model, described in the infamous memory-barriers.txt file (and sometimes referred to as "LKMM"). Few kernel developers understand this model in detail (and many find it too subtle to understand deeply), but it governs how memory access works at the lowest levels. (See also this article series for more on the kernel's memory model).

One of the early concerns about incorporating Rust into the kernel is that the Rust language lacked a memory model of its own. That gap has since been filled; the Rust memory model looks a lot like the C++ model. Boqun Feng, who helps maintain the kernel's memory model, thought that it would be good to formalize a model for Rust code to use in the kernel. Should the Rust model be used, the kernel's model, or some combination of the two? He posted his conclusion to the linux-kernel mailing list: Rust code should adhere to the kernel's memory model. He included an initial patch set showing what that would look like.

Using the kernel's model

The reasoning behind this conclusion is simple enough: "Because kernel developers are more familiar with LKMM and when Rust code interacts with C code, it has to use the model that C code uses". Learning one memory model is hard enough; requiring developers to learn two models to work with the kernel would not lead to good results. Even worse results are likely when Rust and C code interact, each depending on its respective memory model to ensure proper data ordering. So, as long as the kernel has its own memory model, that is what Rust code will have to use.

Kent Overstreet pointed out a disadvantage of this approach: the kernel will remain incompatible with other Rust code and will not be able to incorporate it easily. He suggested that, perhaps, the kernel's memory model could be rebuilt on top of C or C++ atomic operations, at which point supporting the Rust model would be easier. That seems unlikely to happen, though, given the strong opposition from Linus Torvalds to any such change.

One of Torvalds's arguments was that language-based memory models are insufficiently reliable for use in the kernel, and that is not a problem that can be addressed quickly. "The C++ memory model may be reliable in another decade. And then a decade after *that*, we can drop support for the pre-reliable compilers." Thus, he said, "I do not understand why people think that we wouldn't want to roll our own". Feng added that the kernel's memory model encompasses a number of use cases, such as mixed-size operations on the same variable, that are important to the kernel. Those uses are not addressed (or allowed) in the Rust model. He did suggest that, perhaps, a subset of the Rust model could be implemented on top of the kernel's operations.

Another reason for the kernel project to implement its own memory model, Torvalds said, is that kernel developers need to be deeply familiar with the architectures they are supporting anyway. There is no way to create a kernel without a lot of architecture-specific code. Given that, "having the architecture also define things like atomics is just a pretty small (and relatively straightforward) detail".

Torvalds has another reason for sticking with the kernel's memory model, though: he thinks that the C++ model is fundamentally misdesigned. A key aspect of that model is that data exposed to concurrent access is given a special atomic type, and the compiler automatically inserts the right barriers when that data is accessed. Such a model can ensure that a developer never forgets to use the proper atomic operations, which is an appealing feature. That is, however, not how the kernel's model works.

In the kernel's memory model, it is not the type of the data that determines how it must be accessed, but the context in which that access happens. A simple example is data that is protected by a lock; while the lock is held, that data is not truly shared since the lock holder has exclusive access. So there is no need for expensive atomic operations; instead, a simple barrier when the lock is released is sufficient. In other settings, where a lock is not held, atomic operations may be needed to access the same data.

Torvalds argued that the kernel's approach to shared data makes more sense:

In fact, I personally will argue that it is fundamentally wrong to think that the underlying data has to be volatile. A variable may be entirely stable in some cases (ie locks held), but not in others.

So it's not the *variable* (aka "object") that is 'volatile', it's the *context* that makes a particular access volatile.

That explains why the kernel has basically zero actual volatile objects, and 99% of all volatile accesses are done through accessor functions that use a cast to mark a particular access volatile.

This approach has been taken in the kernel for a long time; it is described in the volatile-considered-harmful document that was first added to the kernel for the 2.6.22 release in 2007.

The outcome of this discussion is clear enough: Rust code in the kernel will have to use the kernel's memory model for the foreseeable future. The Rust language brings with it a number of new ways of doing things, many of which have significant advantages over C. But bringing a new language into a code base that is old, large, and subject to special requirements is always going to require some compromises on the new-language side. Using the kernel's memory model may not actually be a compromise, but it is different from what other Rust code will do; it will be one of the many things Rust developers will have to learn to work in the kernel project.

Index entries for this article
KernelDevelopment tools/Rust
KernelMemory model


to post comments

A memory model for Rust code in the kernel

Posted Apr 3, 2024 22:38 UTC (Wed) by ejr (subscriber, #51652) [Link]

Vehemently agreeing for variables (esp. arrays) marked as "atomic." Ok, you use atomic int-fetch-and-add (ifa) to build them, but then they're effectively constant.

Consider transposing a sparse matrix in CSR/CSC (compressed sparse row/column) representation. With ifa, you can build the transpose in parallel using only the output space. But if the language requires you to copy it... And doesn't allow some method of *scoping* the "atomicity" as with function arguments that suddenly don't match, well, feh.

A memory model for Rust code in the kernel

Posted Apr 3, 2024 22:46 UTC (Wed) by atnot (subscriber, #124910) [Link] (2 responses)

Another disadvantage of going with a custom memory model is not immediately being able to use a bunch of nice tools like loom (https://github.com/tokio-rs/loom) which allow you to exhaustively test your code with the worst orderings the C11 memory model allows, which is very neat.

That said, the Kernel is probably big enough of a project for someone to be willing to add support for the LKMM to tools like this.

A memory model for Rust code in the kernel

Posted Apr 3, 2024 23:20 UTC (Wed) by koverstreet (✭ supporter ✭, #4296) [Link]

The C memory model doesn't support everything we need yet - we need to poke the appropriate people until it does.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 10:58 UTC (Thu) by foom (subscriber, #14868) [Link]

Doing anything like that would require the lkmm to be well-specified and correctly-implemented enough to be correctly formally modeled.

Given how many research papers and iteratations the C/C++ mm has taken to address all sorts of crazy edge cases, and how little of that sort of work seems to have been done on the lkmm so far (as far as I can tell, at least), I'm skeptical that formal modeling could be successful yet...

But maybe I'm wrong. I mean, I'm not an expert, I can only even barely understand the implications of the issue with sequentially-consistent ordering raised in http://plv.mpi-sws.org/scfix/paper.pdf and subsequently fixed in the first part of https://wg21.link/P0668 for C++20...

A memory model for Rust code in the kernel

Posted Apr 4, 2024 0:16 UTC (Thu) by roc (subscriber, #30627) [Link] (1 responses)

Rust atomics do let you use non-atomic access to atomic values when you have obtained exclusive access, e.g. by taking a mutex.

A memory model for Rust code in the kernel

Posted Apr 5, 2024 7:56 UTC (Fri) by phg (subscriber, #96794) [Link]

Indeed, from the article it sounds like LKMM behavior could be modeled in Rust’s typesystem, using the typestate pattern for e. g.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 0:18 UTC (Thu) by roc (subscriber, #30627) [Link]

Maybe the Rust standard library could have a feature that implements its atomics on top of the LKMM atomics.

What if another add-on language uses yet another memory model?

Posted Apr 4, 2024 2:59 UTC (Thu) by felixfix (subscriber, #242) [Link] (4 responses)

Rust is just a tiny minority compared to the kernel's C code. It ought to be obvious that it's better to convert the tiny minority than the huge majority.

If yet another language is brought into kernel space (Java, C++, Algol) with yet another memory model, do you switch both Rust and kernel code to use the new tiny minority language?

It's easier to keep the working memory model and make tiny newcomers adjust to it.

Then again, I haven't mucked about in kernel code for 25 years. I may be completely wrong.

What if another add-on language uses yet another memory model?

Posted Apr 4, 2024 7:04 UTC (Thu) by Wol (subscriber, #4433) [Link] (3 responses)

> It's easier to keep the working memory model and make tiny newcomers adjust to it.

> Then again, I haven't mucked about in kernel code for 25 years. I may be completely wrong.

You are wrong. It may be EASIER, true, but if the working model is flawed then it's technical debt. In which case, yes maybe evolve it rather than ditch it, but it's SAFER (and better in the long run) to make the working model adjust to the newcomers.

They found a lot of long-standing bugs in LLVM because Rust used a different model ...

Cheers,
Wol

What if another add-on language uses yet another memory model?

Posted Apr 8, 2024 20:58 UTC (Mon) by pjdesno (guest, #167375) [Link]

The standard practice in the Linux kernel world seems to be that if you break something, you fix it - i.e. if you change an interface, you have to submit a patch that not only does what you're intending to do, but fixes that interface everywhere it's used, in all zillion lines of the kernel.

In theory the Rust folks could submit a patch that included changes across the entire kernel to update it to use a new memory model. That's 25 million lines of code, across multiple architectures including some they may not have access to. Some fraction of which are being actively developed while they're doing that work, so they'll have to go back and update those, etc etc. I expect the heat death of the universe to occur before they finished...

Why languge specific?

Posted Apr 17, 2024 10:31 UTC (Wed) by neva_krien (guest, #169188) [Link] (1 responses)

Maybe it would be a good idea but I am doubtful the model should be made based on any 1 languge.

And if I had to choose the 1 languge defiantly c. Just because everything has c interop and will have it for the foreseeable future.

Why languge specific?

Posted Apr 17, 2024 10:46 UTC (Wed) by farnz (subscriber, #17727) [Link]

Which C memory model?

The whole problem here is that the kernel (written in C) has a memory model based on C90's memory model with extensions, and this memory model is not compatible with C11's memory model.

In turn, the C11 memory model is underspecified (contains defects, to use the C committee jargon), and you need to consider the fixes to it in C17 and C23 to have a complete memory model - it's also quite likely that a future C standard will fix more defects in the C11 memory model.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 16:08 UTC (Thu) by mbp (subscriber, #2737) [Link] (2 responses)

Thanks for writing about this, but I do wish this article had given some examples of Rust code that is legal in normal userspace Rust but not in the Linux kernel, so people not deeply steeped in these models could understand what it really means.

From a brief look at the patches you linked, it seems like the impact is that Rust kernel code won't be able to use the normal `std::sync` primitives, like for atomic ints, but rather some different kernel-specific primitives. However, most Rust code would look the same as you'd normally expect, except for using these different underlying types?

A memory model for Rust code in the kernel

Posted Apr 4, 2024 16:44 UTC (Thu) by kpfleming (subscriber, #23250) [Link] (1 responses)

That's probably the case, but given the context (drivers and filesystems and other kernel components) the usage of synchronized access (locking), atomics, etc. will likely be orders of magnitude more frequent than 'normal Rust code'. If the replacement primitives don't provide the same semantics as the ones in the Rust standard library, then the code is fundamentally different even if it appears to be similar.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 16:51 UTC (Thu) by mbp (subscriber, #2737) [Link]

Yep, that's the kind of thing I would like to read about, as a person who's written a decent amount of Rust and a little bit of kernel C code.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 17:47 UTC (Thu) by dvdeug (guest, #10998) [Link] (4 responses)

> So it's not the *variable* (aka "object") that is 'volatile', it's the *context* that makes a particular access volatile.

That reduces my (already low) plans of working on the kernel. I can see the arguments against how C uses volatile, but the memory model is already confusing enough, and trying to use volatile in ways it's not intended just adds confusion. Why not copy from a volatile variable, work on it, and then copy it back to the volatile variable when you're done? Similar semantics, and clearly working within the standard.

A memory model for Rust code in the kernel

Posted Apr 4, 2024 17:52 UTC (Thu) by mb (subscriber, #50428) [Link] (2 responses)

>Why not copy from a volatile variable, work on it, and then copy it back to the volatile variable when you're done?

Because it doesn't give you the option to do a non-volatile access. Which is the common case in the kernel. We want optimization, except for in a few cases (a.k.a. context).

A memory model for Rust code in the kernel

Posted Apr 8, 2024 1:11 UTC (Mon) by marcH (subscriber, #57642) [Link] (1 responses)

So "you" want the _same_ memory to behave sometimes as "shared" and sometimes not. This is definitely looks like very sharpest knives.

Even _C++_ chose a "slower" and safer to use model? Wow, no surprise everyone is running away from memory-barriers.txt.

I guess it's OK as long as the kernel can provide "some" safe abstractions/routines that allow most kernel developers not to think about that? Which seems to be the case since very few understand memory-barriers.txt and Linux has been relatively... successful :-)

At that point I should really go and read the email thread but I don't have the time (that's why we have LWN) so I'll stop...

A memory model for Rust code in the kernel

Posted Apr 8, 2024 14:41 UTC (Mon) by foom (subscriber, #14868) [Link]

Note that C++20 did add support for atomic access to memory without using an atomic typed variable, https://en.cppreference.com/w/cpp/atomic/atomic_ref

But also, gcc and clang have supported it as a compiler built-in function since forever. The kernel has never been shy about using compiler extensions elsewhere, so I'm not sure why the inability to do atomic access on non-atomic-typed memory in standard C is even a point of discussion.

Such functionality is certainly useful for some rare cases, but does mean the user has to _manually_ deal with ensuring the alignment is as high as required, and ensure that the memory isn't accessed non-atomically at the same time as atomically (or else UB).

It's good for the functionality to exist, but it's also good for it not to be the default, because atomics already have enough sharp edges without. I'd note also that in most cases, it is entirely sufficient to drop down to a memory_order_relaxed access, instead of dropping to a non-atomic access. The cost of a relaxed atomic access is minimal, as are the guarantees (more sharp edges!).

A memory model for Rust code in the kernel

Posted May 16, 2024 2:19 UTC (Thu) by mrugiero (guest, #153040) [Link]

You would need to provide context either way because `volatile` only tells the compiler not to remove or reorder across sequence points (calls to functions and other `volatile` accesses), but does nothing wrt the CPU that will reorder whatever it wants, nor across non-volatile accesses. You need the explicit barriers for any kind of synchronization, there's no way around that. So, just use the `WRITE_ONCE` and `READ_ONCE` macros that will do the `volatile` access while ensuring the right behavior wrt the CPU as well. And it the context allows, simply read and write from the normal variable without penalty.


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds