|
|
Log in / Subscribe / Register

The kernel "closure" API

By Jonathan Corbet
January 11, 2024
The data structure known as a "closure" first found its way into the mainline kernel with the addition of bcache in the 3.10 development cycle. With the advent of bcachefs in 6.7, though, it acquired a second user and was moved to the kernel's lib directory, making it available to other kernel users as well. The documentation of closures in the source is better than that of many things in the kernel, but there is still room for a gentler introduction.

As include/linux/closure.h notes: "Closure is perhaps the most overused and abused term in computer science, but since I've been unable to come up with anything better you're stuck with it again". In the kernel sense, a closure can be thought of as a reference count tracking some number of things that need to happen, along with some synchronization features and a hierarchical organization.

To start working with closures, one should allocate a structure of type struct closure and initialize it with:

    #include <linux/closure.h>

    closure_init(struct closure *cl, struct closure *parent);

Where cl is the closure to be initialized, and parent is used to create a parent relationship, which will be described below. On return from this call, the caller owns a reference to the closure that must eventually be given back.

A closure's reference count can be manipulated with:

    void closure_get(struct closure *cl);
    void closure_put(struct closure *cl);

Closures have a few mildly quirky rules, one of which is that only references obtained with closure_get() can be released with closure_put(); the initial reference obtained from closure_init() is special and must be handled differently.

There are a couple of ways of managing that initial reference; to understand them, it's worth keeping in mind what closures are for. Essentially, they allow a thread running in the kernel to place one or more operations (a set of I/O requests, for example) in motion and then wait for all of those operations to complete. To do so, that thread will initialize its closure, then start those other operations, each of which will involve calling closure_get() to obtain a reference to the closure. As each operation completes, a closure_put() call is made. When the closure's reference count drops to one, all of those operations are complete and the next step, whatever it is, can be taken.

It is up to the creator of the closure to arrange for that next step once the closure has dropped back to just the initial reference. One option for doing that is for the initiating thread to simply wait until the reference count drops by calling both of:

    bool closure_wait(struct closure_waitlist *list, struct closure *cl);
    void closure_sync(struct closure *cl);

The caller should allocate list separately. Another rule of closures is that they can only be on one wait list at a time; if the given cl is already on a list, closure_wait() will immediately return false. Otherwise it will place the closure on the given list. A call to closure_sync() will then block the current thread until the reference count drops to one.

If the initiating thread does not want to wait synchronously for the closure to complete, the alternative is to arrange for a sort of callback when the reference count drops to one:

    typedef void (closure_fn) (struct closure *);
    void continue_at(struct closure *cl, closure_fn *callback,
    		     struct workqueue *wq);

This call will arrange for callback() to be called when the last closure_put() call is made — the point where only the initial reference to the closure remains. If wq is non-NULL, it specifies the workqueue to be used to make this call; otherwise the call will made directly from closure_put(). The call to continue_at() drops the caller's reference to cl (which, remember, is the initial reference created when the closure was set up), so the caller should not touch it further; indeed, the rules for closures say that the caller should return immediately after the call.

The way to destroy a closure is to call continue_at() with a NULL callback() pointer; that is the signal that the closure is done. The macro closure_return() is defined as a shorthand for this call:

    #define closure_return(_cl)	continue_at((_cl), NULL, NULL)

There is also a variant, closure_return_with_destructor(), that takes a second closure_fn() pointer indicating a function to call when all references have been dropped and the closure is finished.

As noted above, closures can be initialized with a parent pointer; this allows the caller to set up a hierarchy of dependent events. When a closure is initialized, it will take a reference (with closure_get()) on the parent if one is specified; as a result, the parent will continue to exist for a long as the new closure does. When a closure is finished with the special continue_at() call, the reference to the parent will be dropped with closure_put(). This mechanism ensures that the parent closure will not complete until all of its child closures have finished.

Needless to say, there are other complications in the closure API as well, but the above covers the core of it. As of this writing, only bcache and bcachefs use closures. In the past, there have been occasional vague objections to the closure abstraction, but those have not prevented its use so far. Whether its usage will grow will depend entirely on whether other developers find it useful.

Index entries for this article
KernelClosures


to post comments

The kernel "closure" API

Posted Jan 12, 2024 5:31 UTC (Fri) by alison (subscriber, #63752) [Link] (1 responses)

The present article is another exemplary exposition by the LWN team. I can only imagine how long it would take someone to glean all this information otherwise by reading mailing lists and studying the headers.

The kernel "closure" API

Posted Jan 12, 2024 7:15 UTC (Fri) by itsmycpu (guest, #139639) [Link]

+1. Good Info.

The kernel "closure" API

Posted Jan 12, 2024 15:08 UTC (Fri) by ms (subscriber, #41272) [Link] (4 responses)

To me, this is a wait-group, not a closure. But naming is nearly always the hardest thing.

The kernel "closure" API

Posted Jan 12, 2024 17:48 UTC (Fri) by koverstreet (✭ supporter ✭, #4296) [Link] (2 responses)

I called them closures because when used asynchronously, you embed them in objects that function like stack frames and with the parent and closure_return functionality you can create spaghetti stacks.

But since I wrote that code I've probably used them more for the waiting functionality, since you can do some things with them that you can't do with standard kernel waitlists.

For example, you can pass a closure down to some code that might have to block, then if it has to block the inner function can add the closure to a waitlist and return -EAGAIN, unwinding to the level of the stack where the closure was declared and it's safe to block.

This wouldn't be as safe to do with standard kernel waitlists because prepare_to_wait() also changes the task state, and once that's been done and you're no longer in TASK_RUNNING you have to be careful about what code you run.

For the same reason I'll sometimes use closure_wait() instead of wait_event() - if the wait expression has to take a mutex you can't do that with wait_event(), but you can with closure_wait().

The kernel "closure" API

Posted Jan 13, 2024 0:15 UTC (Sat) by itsmycpu (guest, #139639) [Link] (1 responses)

Why is the function calling continue_at "expected to immediately return after using this macro" ?

The kernel "closure" API

Posted Jan 13, 2024 2:11 UTC (Sat) by koverstreet (✭ supporter ✭, #4296) [Link]

Because continue_at() drops the ref for the thread running the closure - after continue_at(), you can't touch that stuff anymore.

Just immediately returning is the simplest and sanest way of avoiding those sorts of use-after-free races.

continue_at() originally _did_ do a return within the macro, but Jens changed that without CCing or posting for review, and honestly I should have just reverted that change.

The kernel "closure" API

Posted Jan 12, 2024 22:21 UTC (Fri) by itsmycpu (guest, #139639) [Link]

Talking about naming, maybe continue_at and continue_at_nobarrier should be prefixed with "closure_" like all other functions.

So: closure_continue_at
And maybe closure_set_fn instead of set_closure_fn

Not that it would be important to me :)

The kernel "closure" API

Posted Jan 18, 2024 19:41 UTC (Thu) by alkbyby (subscriber, #61687) [Link]

Hi all. So, "Closure is perhaps the most overused" triggered my "someone is wrong on the Internet" reaction.

I am aware of only one computer-related meaning of closure. Which matches Wikipedia's: https://en.wikipedia.org/wiki/Closure_(computer_programming)

So I am curious what are the specific examples of "most overused" that quote is refererring to. Any pointers?

The kernel "closure" API

Posted Jan 25, 2024 16:25 UTC (Thu) by Vorpal (guest, #136011) [Link]

This to me sounds more like a "promise" + "future" as found in some languages (not C). Usually those are 1-to-1 rather than n-to-1 (number of actions we are waiting for to number of waiting things). So not quite the same. Multi-future perhaps?

Still, it doesn't seem like a closure in any language I know.

A few languages or libraries that I know of that have futures: Rust, JavaScript, Python, C++ (via boost, probably in the standard by now, haven't kept up).


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