-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
libtock: add util/streaming_process_slice
This adds userspace helpers implementing the "streaming process slice" contract as implemented in the Tock kernel in [1]. Instead of working with two separate buffers, these helpers present an interface similar to a regular read-write allow based on a single buffer. Internally, this buffer is then subdivided into an "application-owned" and "kernel-owned" part which are swapped atomically. The caller is responsible for keeping a `streaming_process_slice_state_t`, and is provided with ephemeral references to the kernel-written payloads on each swap operation. The full, original buffer can be reclaimed by deinitializing the `streaming_process_slice_state_t` struct. Currently, these helpers are quite minimal and do not expose the ability to set any optional flags (such as `halt`). They are tested to work against against the interface implemented in [1] (with the fix of [2] included) as part of the LwIP Ethernet tap-driver userspace app. [1]: tock/tock#4208 [2]: tock/tock#4343
- Loading branch information
1 parent
f00ad09
commit c6ed0ed
Showing
3 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
#include "streaming_process_slice.h" | ||
#include <string.h> | ||
|
||
static void streaming_process_slice_prepare_header(uint8_t* buf) { | ||
buf[0] = 0; // version[H] | ||
buf[1] = 0; // version[L] | ||
buf[2] = 0; // flags[H] | ||
buf[3] = 0; // flags[L] | ||
buf[4] = 0; // write offset | ||
buf[5] = 0; // write offset | ||
buf[6] = 0; // write offset | ||
buf[7] = 0; // write offset | ||
} | ||
|
||
returncode_t streaming_process_slice_init( | ||
streaming_process_slice_state_t* state, | ||
uint32_t driver, | ||
uint32_t allow, | ||
void* buffer, | ||
size_t size) { | ||
// Each slice's header is 8 bytes long, and we create two slices from this | ||
// buffer. Thus ensure that the provided buffer is at least 16 bytes long: | ||
if (size < 16) { | ||
return RETURNCODE_ESIZE; | ||
} | ||
|
||
state->driver = driver; | ||
state->allow = allow; | ||
|
||
// We split the buffer in half, an application and kernel side. These two | ||
// buffers are then atomically swapped with each other. | ||
// | ||
// Initially, the first half of this buffer is designated as the application | ||
// buffer. | ||
state->app_buffer_ptr = buffer; | ||
state->app_buffer_size = size / 2; | ||
|
||
// Write a streaming process slice header to the second half of this buffer, | ||
// and allow it to be the kernel buffer. We currently only support version | ||
// 0, and don't set the `halt` flag: | ||
uint8_t* kernel_buffer_ptr = state->app_buffer_ptr + state->app_buffer_size; | ||
size_t kernel_buffer_size = size - state->app_buffer_size; | ||
streaming_process_slice_prepare_header(kernel_buffer_ptr); | ||
|
||
allow_rw_return_t allow_res = | ||
allow_readwrite(driver, allow, kernel_buffer_ptr, kernel_buffer_size); | ||
if (!allow_res.success) { | ||
memset(state, 0, sizeof(streaming_process_slice_state_t)); | ||
} | ||
|
||
return tock_status_to_returncode(allow_res.status); | ||
} | ||
|
||
returncode_t streaming_process_slice_get_and_swap( | ||
streaming_process_slice_state_t* state, | ||
uint8_t** buffer, | ||
uint32_t* size, | ||
bool* exceeded) { | ||
uint8_t* ret_buffer; | ||
uint32_t ret_size; | ||
bool ret_exceeded; | ||
|
||
// Prepare the current app buffer to be shared with the kernel (writing a | ||
// zeroed-out header): | ||
streaming_process_slice_prepare_header(state->app_buffer_ptr); | ||
|
||
// Swap the current app buffer for the kernel buffer: | ||
allow_rw_return_t allow_res = | ||
allow_readwrite(state->driver, state->allow, state->app_buffer_ptr, | ||
state->app_buffer_size); | ||
|
||
if (allow_res.success) { | ||
// Record the new app buffer: | ||
state->app_buffer_ptr = allow_res.ptr; | ||
state->app_buffer_size = allow_res.size; | ||
|
||
// Return information about the received payload: | ||
ret_buffer = state->app_buffer_ptr + 8; | ||
memcpy(&ret_size, state->app_buffer_ptr + 4, sizeof(uint32_t)); | ||
ret_exceeded = (state->app_buffer_ptr[3] & 0x01) == 0x01; | ||
} else { | ||
// Allow was not successful, return safe dummy values instead: | ||
ret_buffer = NULL; | ||
ret_size = 0; | ||
ret_exceeded = false; | ||
} | ||
|
||
// Write return values if provided with non-NULL pointers: | ||
if (buffer != NULL) { | ||
*buffer = ret_buffer; | ||
} | ||
if (size != NULL) { | ||
*size = ret_size; | ||
} | ||
if (exceeded != NULL) { | ||
*exceeded = ret_exceeded; | ||
} | ||
|
||
return tock_status_to_returncode(allow_res.status); | ||
} | ||
|
||
returncode_t streaming_process_slice_deinit( | ||
streaming_process_slice_state_t* state, | ||
uint8_t** buffer, | ||
size_t* size) { | ||
uint8_t* ret_buffer; | ||
size_t ret_size; | ||
|
||
// Unallow the buffer currently allowed to the kernel: | ||
allow_rw_return_t unallow_res = | ||
allow_readwrite(state->driver, state->allow, NULL, 0); | ||
|
||
if (unallow_res.success) { | ||
// Unallow failed, don't modify the state struct. | ||
ret_buffer = NULL; | ||
ret_size = 0; | ||
} else { | ||
// The unallow worked, recreate the full, initial buffer from the app and | ||
// kernel halves: | ||
if ((void*)state->app_buffer_ptr < unallow_res.ptr) { | ||
// App buffer is left half, kernel buffer is right half: | ||
// `[ app_buffer ][ kernel_buffer ]` | ||
ret_buffer = state->app_buffer_ptr; | ||
ret_size = state->app_buffer_size + unallow_res.size; | ||
} else { | ||
// App buffer is right half, kernel buffer is left half: | ||
// `[ kernel_buffer ][ app_buffer ]` | ||
ret_buffer = unallow_res.ptr; | ||
ret_size = unallow_res.size + state->app_buffer_size; | ||
} | ||
|
||
// Wipe the state struct: | ||
memset(state, 0, sizeof(streaming_process_slice_state_t)); | ||
} | ||
|
||
if (buffer != NULL) { | ||
*buffer = ret_buffer; | ||
} | ||
if (size != NULL) { | ||
*size = ret_size; | ||
} | ||
|
||
return tock_status_to_returncode(unallow_res.status); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
#include "../tock.h" | ||
|
||
typedef struct { | ||
uint8_t* app_buffer_ptr; | ||
size_t app_buffer_size; | ||
uint32_t driver; | ||
uint32_t allow; | ||
} streaming_process_slice_state_t; | ||
|
||
// Initialize a "streaming process slice" read-write allow slot | ||
// | ||
// This method allows a userspace buffer into a "streaming process | ||
// slice" allow slot, implementing its atomic-swap semantics and | ||
// header layout. The streaming process slice abstraction allows a | ||
// userspace process to lossessly receive data from a kernel | ||
// capsule. This is done by maintaining two buffers, where at any time | ||
// one of which is owned by the kernel (for writing new, incoming data | ||
// into) and one by the application, to process received data. These | ||
// buffers are atomically swapped by the application, upon receipt of | ||
// a signal that some data has been inserted into the kernel-owned | ||
// buffer (such as an upcall). | ||
// | ||
// This method abstracts this interface by consuming one buffer and | ||
// splitting it into two halves, owned by the application and kernel | ||
// respectively. It tracks all necessary state in the | ||
// `streaming_process_slice_state_t` object. For this to work, the | ||
// passed buffer must be able to hold at least two streaming process | ||
// slice headers (8 byte each), i.e., it must be at least 16 bytes | ||
// long. | ||
// | ||
// In case of an error while allowing the kernel-owned buffer to the | ||
// specified driver and read-write allow slot, this function converts | ||
// this error-status into a returncode using | ||
// `tock_status_to_returncode` and returns it to the caller. When this | ||
// method returns `RETURNCODE_SUCCESS`, the passed buffer is assumed | ||
// to be owned by this `streaming_process_slice_state_t` and must not | ||
// be used until after a successful call to | ||
// `streaming_process_slice_deinit`. When the buffer is of | ||
// insufficient size, it returns `RETURNCODE_ESIZE`. | ||
returncode_t streaming_process_slice_init( | ||
streaming_process_slice_state_t* state, | ||
uint32_t driver, | ||
uint32_t allow, | ||
void* buffer, | ||
size_t size); | ||
|
||
// Swap kernel- for app-owned buffer and get received payload | ||
// | ||
// This method atomically swaps the kernel-owned and application-owned | ||
// halves of the streaming process slice. This function will reset the | ||
// application-owned buffers header, applying any flags set in the | ||
// `streaming_process_slice_state_t` and setting the write offset to | ||
// `0`. | ||
// | ||
// Following the swap operation, when returning `RETURNCODE_SUCCESS`, | ||
// it provides the buffer's payload and any kernel-set flags to the | ||
// caller through the `buffer`, `size`, and `exceeded` arguments | ||
// respectively. Callers must either provide pointers to variables for | ||
// these values, or set them to `NULL` in case they are not interested | ||
// in any given value. | ||
// | ||
// This function forwards any error from the underlying `allow_readwrite` | ||
// operation in its return value. In case of a return value other than | ||
// `RETURNCODE_SUCCESS`, any values returned in `buffer`, `size` and `exceeded` | ||
// must not be considered valid. | ||
returncode_t streaming_process_slice_get_and_swap( | ||
streaming_process_slice_state_t* state, | ||
uint8_t** buffer, | ||
uint32_t* size, | ||
bool* exceeded); | ||
|
||
// Deinitialize an initialized `streaming_process_slice_state_t` | ||
// | ||
// This function reconstructs the passed into `streaming_process_slice_init` and | ||
// returns it through the `buffer` and `size` arguments (if not set to `NULL` | ||
// respectively). | ||
// | ||
// This function forwards any error from the underlying `allow_readwrite` | ||
// operation in its return value. In case of a return value other than | ||
// `RETURNCODE_SUCCESS`, any values returned in `buffer`, `size` and `exceeded` | ||
// must not be considered valid. | ||
returncode_t streaming_process_slice_deinit( | ||
streaming_process_slice_state_t* state, | ||
uint8_t** buffer, | ||
size_t* size); |