
(also as an issue at https://github.com/tock/tock/issues/4370) Grants and allowed buffers provide a mechanism for capsules and other kernel components to use and/or access memory bound to the lifetime of ("owned" by) a process. Commonly allowed buffers are used to send/receive data on behalf of a process using lower-level drivers, sometimes using DMA or other asynchronous hardware, and grants are used to store metadata related to a process. Grants were originally design with three important goals in mind: 1. No dangling pointers: it is not possible for a capsule to access grant memory after the process has been reaped or an allowed buffer after it has been unallowed. 2. No zombies: It should be possible to deallocate process memory without waiting for outstanding hardware operations. I.e. the kernel should be able to reap a process's grants and allowed buffers as soon as it terminates the process. 3. Infallible deallocation: unallowing a buffer should always succeed. 4. Availability: the kernel should be always be able to access grant memory at least once per call-stack. Accomplishing all four simultaneously is challenging, and has lead to a design that either precludes or makes cumbersome and inefficient zero-copy operations on granted memory. This proposal outlines a design for two new grant access mechanisms. One that achieves all four goals while enables a limited form of zero-copy functionality. A second that enabled general-purpose zero-copy functionality (including through asynchronous DMA), but sacrifices infallible deallocation, availablity and the no-zombie guaratees and, in exchange, requires users to have elevated trust (via a capability). ## The case for zero-copying granted memory The current grant (including allow-ed memory) access mechanism only allows references with lifetimes bounded by the scope of a function. A consequence of this is many capsules adopt the following patterm: 1. Allocate a static buffer 2. Copy (some) grant data into the static buffer 3. Pass ownership of this buffer to a driver 4. Pass ownership back. Go to step 2. The downsides to this pattern are - Extra copying - Extra allocation - Extra logic Logic is both implementation and maintenance complexity, but also generated code size. There is no good solution to sizing allocations. Too small and the control logic will ruin performance. Too large consumes what is typically a precious resource. No size is large enough to accept arbitrary sized allow-ed buffers. Copies are a performance concern in some applications. Zero-copy could address these downsides. ## Different use-cases, different trade-offs The two main use cases for zero-copy we consider are those that do not require asynchronous hardware access to memory (e.g. DMA), and those that do. We can satisfy all four original requirements while allowing the former use case. However, concurrent and zero-copy DMA is fundementally incompatible with the last three of the original requirements. If a DMA is ongoing to user memory we cannot reap that proccesses memory, we cannot respect a user's wish to deallocate, and the kernel itself may not be able to concurrently access that memory while DMA is writing to it. #### Memory-Mapped FIFO UART This is a typical non-DMA capable UART (such as the ns16550). The CPU can read/write to the UART's FIFO by reading/writing to a memory-mapped register. The process provided buffer for sending/receiving data is an arbitrary length, and almost certainly bigger than the UART FIFO (typically just a few bytes), so a single send/recieve operation will need to be done in steps. The UART is slow compared to the CPU, and we do not want to wait for the FIFO to clear synchronously, instead relying on interrupts to resume transmission/reception asynchronously. As a result, it's also possible for a process to unallow or replace the allowed buffer while the UART is waiting for the FIFO to clear. The UART should discover this and react appropriately (e.g. short cut the operation), but it need not necessarily be able to complete the operation. We want to pass the allowed memory from, e.g., a console system call driver to the UART driver, and let the UART driver handle the logic necessary to track how much as been sent/received so far, and where to continue from, without having to allocate additional memory and without having to continually call back to the console driver. #### DMA Ethernet This is an Ethernet device that has one or more DMA channels for receiving. The CPU writes a base pointer and length to each memory-mapped registers for each channel's and the hardware asynchronously populates that memory with received MTU-sized frames. The process provides a large, variably sized buffer for receiving data potentially larger than the MTU size. Because receiving one or more frames may take an abitrary amount of time, the CPU should do other things while it's waiting rather than block. As a result, to effectively use the device, the driver should chop up the process buffer into MTU-sized slices and receive on as many slices as there are channels, then rely on interrupts to signal completion of DMA operations. We want to pass the allowed memory from, e.g., a UDP system call driver to the ethernet driver, and allow the ethernet driver to reference this memory directly in the DMA, rather than copying it to a potentially large buffer. We _must_ retain goal one---a dangling pointer that's reused for something else might result, for example, in leaking some secret unintentionally over the network---but the ethernet driver is trusted enough (by the board) to release memory for reclamation to relax goals 2-4. ## Two New Grant Mechanisms Here are described two new mechanisms that intend to solve the two different zero-copy patterns. ### `ARef` (Allow-Lifetime Reference) & `PRef` (Process-Lifetime Reference) `ARef` & `PRef` are similar types that differ only in that the first is bounded by the lifetime of an allow, and the second by the lifetime of a process. They are both a sort of reference (generic in what they point to), and they have `'static` lifetime. Neither are directly dereferenceable. Instead, capsules and drivers must first convert them to a live version (`LiveARef<'a>` and `LivePRef<'a>`). The difference between the live and non-live versions is that the live versions _can_ be dereferenced (they are smart pointers), and have a corresponding lifetime that ensures they do not outlive a scope narrower than any process could be de-allocated, or buffer unallowed. The conversion to live is cheap (a load / compare), and fallible (it returns an `Option`). Live references have the same caveat as legacy entry in that they only work within a given function. However, a live reference can be frozen again (which is zero-cost and cannot fail), and so stored globally. Notably, while live, these reference can be modified and, e.g., broken apart into packets or the portions of a slice that still needs to be transmitted. ARef/PRef work by storing with each process / allow a generation counter. ARef / PRef themselves have both the counter of when they were created, and a reference to the counter to compare to. All ARef/PRef can be immediatly invalidated by incrementing the counter. This is done in a scope where no LiveARef/LivePRef are allowed to exist. These types are applicable in the non-DMA case. For instance, we can pass a LiveARef to a uart driver. It can write as many bytes as it likes, then freeze the remainging sub-slice and store it. Later, on an interrupt path, it can try covert to live and continue. If the conversion fails, the buffer must have been un-allowed and it can report this outcome. ARef/PRef also support an iterator pattern for use cases like this. ARef/PRef do not suffer from availability problems as they are shared references. They can be requested as many times as required on a call stack. They can also all be immediately revoked, causing any future attempts to convert to live reference to fail. They cannot dangle as the live versions have lifetimes that bound them sufficiently. ARef/PRef are not appropriate for DMA wherein the system is doing other things while DMA takes place. Hardware will not perform the checks that the live conversions do, and also will outlive the lifetimes that guard the live references. ### `Ref` / `RefMut` For DMA, we need to ensure that memory does not have its lifetime end while DMA is ungoing. DMA is unbounded. For this reason, we suggest reference counting by the kernel, which can also block de-allocaiton and fail subsequent allows if it finds non-zero reference counts. `RefCell`, and its referrence types`Ref` and `RefMut` are the types from the core library which implement reference counting. (Note the lifetime we provide for these references is `'static`). These provide a sufficient implementation of reference counting for our purposes. We allow both `Ref` and `RefMut` to the grant data, and `Ref` to allow-ed data. Misbehaving capsules and drivers cannot break safety, but if they leak the `Ref` or `RefMut` will create zombie processes. Nothing short of a system reset is likely to save a suffciently broken driver. Because accesssing these types breaks three of our original design goals, they are both locked off behind a capability, and can be turned off entirely at a configuration level. The intent is that these be used solely when other mechanisms do not work. ### Legacy mechanism `ARef` / `PRef` are proposed as general replacement for the legacy mechanism. `Ref` / `RefMut` are not as they have drawbacks. However, to to support the existing codebase the legacy mechanism is still supported in parralel on a case-by-case basis. There are conditions on using them in a mix-and-match way. For any given grant: Using `ARef`/`PRef` at all, or having an active `Ref`/`RefMut`, blocks use of the legacy mechanism. Being inside a grant via the legacy mechanism blocks both of the two new mechanisms. ARef/PRef cannot be used in conjunction with `RefMut` (they can be used with `Ref`) ## Initial results / takeaways ### Code size savings A simple "hello world" board was created with a console, uart mux, and uart. It was compiled with two versions of the console/uart: the existing version and a new zero-copy version. The new version saved 1K of text. ### Impementation simplicity Insert image of side by side code here ## Nice externalities Other things that this design solved at the same time (but are somewhat orthogonal): - Reentrancy Previously, if a grant was entered it could not be entered again on the same call stack. ARef/PRef/Ref are all inherently shared references and solve this problem by just allowing accessing grants multiple times. - Variable number of allows Scatter/gather lists with arbitrary number of ranges now works with a fixed (only 1!) allow number. Because changing the generation counter is somewhat orthogonal to changing the pointer, we can allow a new system call to change the pointer but not the counter. - Drivers/Drivers can notice buffers being ripped out underneath them User no longer has the ability to change a slice address / length underneath a capsule. Only disallow entirely.
participants (1)
-
Amit Levy