-
Notifications
You must be signed in to change notification settings - Fork 747
Allowed Process Slice Buffered API #4023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense to me.
Two questions:
- What is the "ring"?
- If the length starts at 0, the xor of 0,0,0 is 0, and a blank (ie all 0) buffer will pass the check. Is that intentional?
kernel/src/processbuffer.rs
Outdated
pub fn append_to_ringbuffer(&self, data: &[u8]) -> Result<(), ErrorCode> { | ||
let len = self.ringbuffer_len()?; | ||
if data.len() + len <= self.slice.len() { | ||
self[len..len + data.len()].copy_from_slice(data); | ||
self.set_ringbuffer_len(len + data.len())?; | ||
Ok(()) | ||
} else { | ||
Err(ErrorCode::SIZE) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given where this code lives (core kernel), if we go with this approach I think this should be rewritten with .set()
and not with []
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I follow this.
Can you explain the reasoning behind the XOR? Is there a concern about concurrent access, and this tries to ensure that the kernel knows when the length value is in an inconsistent state? What does the kernel do if this is the case? This seems like it will lead to a need or DeferredCalls. Another approach is to use a u32 and require using atomic writes of the value (which is easier with a u32)? Being sure it is a correct ring buffer would seem better served by having a magic value? |
I think we should drop the XOR and use a 4 byte length. Applications really need to initialize this correctly. Maybe not on the first allow, but applications need to reset the length after reading from the buffer before the app re-allows the buffer. |
Talked about this on the core call; this should not be called "ring buffer". Really the benefits here are that this enables streaming of packets and/or raw data, and it implements a queue, and so the name should presumably reflect that. As an aside, I think that the implementation should probably be its own wrapper type around a |
capsules/extra/src/can.rs
Outdated
@@ -461,36 +460,20 @@ impl<'a, Can: can::Can> can::ReceiveClient<{ can::STANDARD_CAN_PACKET_SIZE }> | |||
buffer_ref | |||
.mut_enter(|user_buffer| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lschuermann Do we need a different API here then? How does a capsule get an object of the wrapper type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that our grant API yields a ReadOnlyProcessBuffer
reference, I was thinking that we can just wrap this type into $ThisPRsTypeName(ReadOnlyProcessBuffer)
that then exposes the APIs proposed here.
We'd rely on capsules correctly wrapping the process buffers prior to interacting with them, but that's similar to how we currently trust capsules to actually enforce their driver API contracts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so if you want to use the ProcessBufferSlice as a Buffer, wrap it first into a Buffer type?
|
Ok bear with me for a second: What if we make this functionality the default, and make applications/drivers opt-out (I'm not exactly sure how they opt out, but let's ignore that for now). Capsules then get allow buffers that "start" (at least as far as the API they have to use the allow buffer is concerned) wherever they last wrote to (or the beginning if the buffer was just allowed). Even if we don't make it the default, can we just have the process slice implementation do all the length tracking? That way we wouldn't need to the application to clear the length field, it would just happen on the allow syscall. Also, the capsule doesn't need to do anything, it just writes to the buffer like it always has (it would just happen to write to the middle of the buffer sometimes). Note: I'm concerned this idea is crossing over the "magic" threshold too far. It's just that capsules are already so hard to write, this would simplify something. |
Co-authored-by: Brad Campbell <bradjc5@gmail.com>
After reading through #4024, I think the main question is: do we make any changes to the kernel to support this? I lean towards no, with potentially one exception. The main functionality can be done in the capsule. The capsule can track where in the buffer it is, and only issue an upcall after the first write. Userspace is expected to do "atomic" buffer swaps (ie after an upcall call allow only once with the new buffer, rather than something like an "unallow" and then later doing an allow). Really then what this PR would accomplish is creating a standard way for handling this so individual capsules don't have to re-invent the process. The exception I'm thinking about is: it would be convenient to have the kernel reset the header metadata when a new buffer is allowed. The capsule cannot do this and must entirely rely on the application to reset this. It would be very easy for an app to forget to do this. It would also be trivial for the core kernel to do this. However, having the kernel do this would require some way to configure an allow as one where the kernel should do this. I'm not sure that overhead is worthwhile. |
Also after reading through #4024, it might be easiest to specify only a minimum header: say a 4 byte length. Anything extra (overflow bit, version, type of overflow, number of packets, etc.) could be capsule/driver specific. |
This is a little tricky. We need to make sure that the use case is the kernel sending data to the app and not vice-versa. If we have write-only buffers, this would be clear. |
I thought we talked about this on the calls and reached agreement that, while the app can forget to do this, this will not meaningfully impact the kernel. Capsules will always have to perform appropriate length checks, and just start writing at the specified offset -- and if the app forgets to change the header or (for any reason) deliberately does not reset the offset, the capsule will need to simply discard pending data. I don't think this is something that the kernel should concern itself with, and I think that it's an easy but obvious mistake to make: swapping buffers is as easy as making an allow system call, and the only thing an app to do is zero the first couple bytes of this buffer before it's allowed. It's a single operation in an app's source, and it will break more or less immediately after the first few buffer swaps. |
I generally agree, it's just a little odd for the kernel to defer something so trivial that would break the operation of the capsule. From the application's point of view, the kernel can only do the correct thing if the application zeroes the buffer. This is because of valid reasons within the kernel and how it's implemented, but I think it's still a fairly strange API that the capsule would rather break than just initialize the buffer on behalf of the application. |
I view it the other way around: the capsule works as intended. It does not permanently break, it just continues writing at the old offset; if there's no more space available it buffers or discards data. Userspace can, at any time, share a new buffer or reset the offset, and the capsule will continue writing data. This is not an edge case that can only occur when userspace doesn't reset the offset, it's a perfectly normal thing to happen during normal operation as well. I think this constitutes a significant part of the elegance and simplicity of this API: userspace tells capsule where to write, capsule writes and increments current offset. The freedom for userspace to select where the capsule starts writing can even be a feature, for instance for capsules that do internally buffer, where now userspace can "peek" into the buffer and re-allow it for the capsule to continue writing at the old offset. I think that's a more fringe use-case, but the simplicity of this API makes that possible. |
I think you make a good point, I just think userspace is better off not having to manage this complexity. It adds another step in the learning curve as some buffers you can just allow and now others you have to zero. Also, I don't really see why we need to support allowing userspace to ask the kernel to write to the middle of the buffer. |
Okay, I can see both arguments. My proposal for a way to move forward with this is to -- for now -- not reset offsets on a re-allow. We don't have a mechanism for this, and introducing one on the core-kernel level would raise similar concerns as with #3258. Then we can see how useful this feature would be and, given sufficient motivation, implement a solution in a follow-up PR. I think what this PR proposes right now is very simple at its core, shouldn't need to touch the core kernel at all, and has been extensively discussed. In other words, as soon as we have an implementation that is a wrapper around a process buffer struct I think this should be good to go any time. |
I agree. I would like to see documentation that says that userspace must reset the header by zeroing it. That gives us more flexibility going forward. |
08/30 update: this design is not fully implemented, that is the next step for this PR |
I think I made all the requested modifications. |
I don't know why those checks fail, but the required ones seem to pass. I think this is good to go. |
Netlify appears to be failing because of these warnings which are unrelated to this PR:
This PR is branched off of a relatively old commit which did not include these |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good.
// the offset where the buffer data starts | ||
// - skip the flags field (1 byte) | ||
// - skip the len field (size_of::<u32>() bytes) | ||
const BUFFER_OFFSET: usize = 1 + size_of::<u32>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this into the impl ala https://github.com/tock/tock/blob/master/kernel/src/process_standard.rs#L1300?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm working on a small refactor that includes this change.
@@ -886,6 +892,96 @@ pub struct WriteableProcessSlice { | |||
slice: [Cell<u8>], | |||
} | |||
|
|||
#[repr(transparent)] | |||
pub struct ProcessSliceBuffer<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexandruradovici I think this is great. The interface seems right to me, and the code looks good too. One minor change that I'd make is rename the "length" field to "offset", to distinguish it from the underlying buffer's length.
Also, I took the liberty to do some refactoring of this code with the goal of removing all* possibly panicing index operations from the code. Process buffers are an area where it's really easy to make mistakes, and Rust's combinators often provide clean and concise ways to express your goals while also reducing dynamic control flow and potential panic points. Here's the code -- have a look, and if it's good to you feel free to pull it in: lschuermann@430e3cd
Edit: @cherrypiejam reported some indexing bugs of my version (and a slightly incorrect ASCII figure), here's an updated version: lschuermann@344024b
Finally, I think that I'd like to see this moved out of the processbuffer.rs
file. That file is complex enough as it is, and littered with unsafe
s. Given that this infrastructure does not require either unsafe
, nor any changes to the core process buffer infrastructure (pretty cool!), I'd rather we move it to some other file like utilities/process_slice_buffer.rs
or something like that. Thoughts?
I support this. Also, what do we call this? Process slices are already buffers. |
|
Add `StreamingProcessSlice` helper, based on `ProcessSliceBuffer` design (#4023)
Handled in #4208 |
Patched from url: github.com/lschuermann/tock rev: 430e3cd
Pull Request Overview
This pull request adds a ringbuffer like API to the
WritableProcessSlice
and ports thecan
driver to use it.This is connected to #3252 and #3258.
The problem
As of Tock 2.1, drivers are not in charge of handling these system calls, but the kernel performs them in the driver's name. This makes drivers unaware of what happens with an application's upcalls and buffers. While most drivers do not need this information, some drivers might want to know when a an application swaps a buffer.
Applications are not allowed to access buffers while they are shared with a driver. In most cases, when an application swaps out a buffer (either unallows it, either provides a replacement for it) it means that the application is consuming the provided data. An example of such behaviour is the
touch
driver and thecan
driver.In both cases, the application could issue a supplementary command system call to inform the driver about the consumption of the buffer. This leads to a data race, as there capsule might execute in between the application's
allow
andcommand
.Solution
The implementation from this pull request provides a simple mechanism, that does not need any kernel changes and seems to keep code size the same as long as it is not used. It uses the first four bytes of the buffer to keep the current buffer head position and a checksum for the index. The checksum is necessary as applications might not initialise the buffer correctly or simply not initialise it at all, placing random data in the first four bytes.
The format of the buffer is described below:
0
Advantages
Disadvantages
buffer1
is shared, it contains either full 0 either random data, the drivers detects the failed checksum and resets the buffer positionbuffer2
is shared whilebuffer1
is swapped out, it contains either full 0 either random data, the drivers detects the failed checksum and resets the buffer positionbuffer1
si shared whilebuffer2
is swapped out, this is where the problem arises. If the application does not reset the buffer length, it will contain a valid checksum and a position different from 0. The driver will write to it starting from the previous position, instead of writing from the beginning.Testing Strategy
This pull request was not yet tested.
TODO or Help Wanted
Feedback
Documentation Updated
/docs
, or no updates are required.Formatting
make prepush
.