Skip to content

Commit

Permalink
Merge pull request tock#3577 from tock/yeild-for
Browse files Browse the repository at this point in the history
[RFC] Yield-WaitFor syscall
  • Loading branch information
alevy authored Jun 16, 2024
2 parents f2466b9 + 07bd118 commit db6b982
Show file tree
Hide file tree
Showing 8 changed files with 402 additions and 120 deletions.
175 changes: 127 additions & 48 deletions doc/reference/trd104-syscalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ in other documents.
Three design considerations guide the design of Tock's system call API and
ABI.

1. Tock is currently supported on the ARM CortexM and RISCV architectures.
1. Tock is currently supported on the ARM CortexM and RISC-V architectures.
It may support others in the future. Its ABI must support both architectures
and be flexible enough to support future ones.
2. Tock userspace applications can be written in any language. The system
Expand Down Expand Up @@ -234,6 +234,37 @@ Values greater than 1023 are reserved for userspace library use. Value 1024
(BADRVAL) is for when a system call returns a different failure or success
variant than the userspace library expects.

3.4 Returning To Userspace
---------------------------------

When the kernel returns to userspace, it only gets to set registers for
one stack frame. In practice, we have two cases:

### Direct Resume

Userspace resumes execution directly after the `svc` invocation, so the
assembly that follows the `svc` command can use the values in r0-r3
as-set by the kernel.

### Pushed Callback

Userspace resumes execution at the start of the callback function.

The values in r0-r3 are consumed by the callback. When the callback
finishes, it will `pop {lr}` (or similar), where the link register in
the callback stack frame has been set by the kernel to the instruction
after the `svc` that relinquished control to the kernel.

The assembly that invoked the syscall now gets to run. At this point
r0-r3 are unknown as those are caller-save registers (which means the
Upcall callback can clobber them freely). The assembly that invoked the
`svc` cannot make any assumptions about the values in r0-r3, nor can the
kernel use them to pass things "to" the calling assembly. Thus, the
`PushedCallback` case has to use a pointer-based approach for the kernel
to communicate with the assembly that invokes the `svc` (e.g.
`yield-param-A` in `Yield-NoWait`).


4 System Call API
=================================

Expand Down Expand Up @@ -293,71 +324,115 @@ result with an error of `NOSUPPORT`.
The Yield system call class is how a userspace process handles
upcalls, relinquishes the processor to other processes, or waits for
one of its long-running calls to complete. The Yield system call
class implements the only blocking system call in Tock that returns,
`yield-wait`.

When a process calls a Yield system call, the kernel schedules one
pending upcall (if any) to execute on the userspace stack. If there
are multiple pending upcalls, each one requires a separate Yield
call to invoke. The kernel invokes upcalls only in response to Yield
system calls. This form of very limited preemption allows userspace
to manage concurrent access to its variables.

There are two Yield system calls:
- `yield-wait`
- `yield-no-wait`

The first call, `yield-wait`, blocks until an upcall executes. It is
commonly used to provide a blocking I/O interface to userspace or to
indicate to the kernel that the process has no work to do so the system
can be put into a sleep state. A userspace library starts a long-running
operation that has an upcall, then calls `yield-wait` to wait for an upcall.
When the `yield-wait` returns, the process checks if the resuming upcall was
the one it was expecting, and if not calls `yield-wait` again.

The second call, `yield-no-wait`, executes a single upcall if any is pending.
If no upcalls are pending it returns immediately.
class implements the only blocking system calls in Tock that return:
`Yield-Wait` and `Yield-WaitFor`.
The kernel invokes upcalls only in response to Yield
system calls.

There are three Yield system call variants:
- Yield-Wait
- Yield-NoWait
- Yield-WaitFor

The register arguments for Yield system calls are as follows. The registers
r0-r3 correspond to r0-r3 on CortexM and a0-a3 on RISC-V.

| Argument | Register |
|------------------------|----------|
| Yield number | r0 |
| No wait field | r1 |
| unused | r2 |
| unused | r3 |

| yield-param-A | r1 |
| yield-param-B | r2 |
| yield-param-C | r3 |

The yield number specifies which call is invoked.
The Yield number (in r0) specifies which call is invoked:

| System call | Yield number value |
|-----------------|--------------------|
| yield-no-wait | 0 |
| yield-wait | 1 |
| yield-wait-for | 2 |

All other yield number values are reserved. If an invalid yield number
is passed the kernel MUST return immediately and MUST NOT
use `yield-param-A`, `yield-param-B`, or `yield-param-C`.

The meaning of `yield-param-X` is specific to the yield type.


### 4.1.1 Yield-NoWait

Yield number 0, Yield-NoWait, executes a single upcall if any is
pending. If no upcalls are pending it returns immediately.
There are no return values from Yield-NoWait. This is because if an upcall was
invoked, the kernel pushes that function call onto the stack, such that the
return value may be the return value of the upcall.

All other yield number values are reserved. If an invalid
yield number is passed the kernel MUST return immediately.
Yield-NoWait will use
`yield-param-A` as the memory address of an 8-bit byte to
write to indicate whether an upcall was invoked. If invoking Yield-NoWait
resulted in an upcall executing, Yield-NoWait writes 1 to the field address. If
invoking Yield-NoWait resulted in no upcall executing, Yield-NoWait writes 0 to the
field address. Userspace SHOULD ensure that `yield-param-A` points to a valid address
in the current process. If userspace does not wish to receive the Yield-NoWait
result, it SHOULD set `yield-param-A` to `0x0`. The kernel SHALL write the Yield-NoWait result
if `yield-param-A` points to any valid process memory and SHALL NOT write the Yield-NoWait
result if it points to an address not in the memory allocated to the
calling process.

The no wait field is only used by `yield-no-wait`. It contains the
memory address of an 8-bit byte that `yield-no-wait` writes to
indicate whether an upcall was invoked. If invoking `yield-no-wait`
resulted in an upcall executing, `yield-no-wait` writes 1 to the
field address. If invoking `yield-no-wait` resulted in no upcall
executing, `yield-no-wait` writes 0 to the field address. This field
allows userspace loops that want to flush the upcall queue to
execute `yield-no-wait` until the queue is empty.
Yield-NoWait can use the Yield-NoWait result to allow userspace loops that want
to flush the upcall queue to execute Yield-NoWait until the queue is
empty.

The Yield system call class has no return value. This is because
`yield-param-B` and `yield-param-C` are unused and reserved.


### 4.1.2 Yield-Wait

Yield number 1, Yield-Wait, blocks until an upcall executes. It is
commonly used when applications have no other work to do and are waiting
for an event (upcall) to occur to do more work.

This call will deliver events to the userspace application in the order
they occurred in time in the kernel. If an application has multiple
subscriptions, the userspace upcall handler is responsible for in some way
noting which callback occurred if necessary.

_Note:_ This will _only_ return after an upcall _executes_. If an event
occurs which would normally generate an upcall, but that upcall is
currently assigned to the Null Upcall, no upcall executes and thus this
syscall will not return.

Yield-Wait has no return value. This is because
invoking an upcall pushes that function call onto the stack, such
that the return value of a call to yield system call may be the
return value of the upcall. This is why the no wait field exists,
so that `yield-no-wait` can return a result to the caller. Allowing
the kernel to pass a return value in register back to userspace
would require either re-entering the kernel or expensive
execution architectures (e.g., additional stacks or additional
stack frames) for upcalls.
return value of the upcall.

`yield-param-A`, `yield-param-B`, and `yield-param-C` are unused and reserved.


### 4.1.3 Yield-WaitFor

The third call, Yield-WaitFor, blocks until one
specific upcall is ready to execute. If
other events arrive that would invoke an upcall on this process, they
are queued by the kernel, and will be delivered
in response to subsequent Yield calls.
Event order in this queue is maintained.

The specific upcall is identified by a Driver number and a Subscribe number
(which together form an UpcalId).

- Driver number: `yield-param-A`
- Subscribe number: `yield-param-B`

This process will resume execution when an event in the kernel generates
an upcall that matches the specified upcall. No userspace callback
function will invoked by the kernel. Instead, the contents of r0-r2 will
be set to the Upcall Arguments provided by the driver when the upcall is
scheduled.

`yield-param-C` is unused and reserved.


4.2 Subscribe (Class ID: 1)
--------------------------------
Expand All @@ -384,6 +459,9 @@ the upcall function. The `application data` argument is a parameter
that an application passes in and the kernel passes back in upcalls
unmodified.

The `upcall pointer` SHOULD be a valid upcall, i.e., either a
`SubscribeUpcall` or the Null Upcall, as defined in the next section.

If the passed upcall is not valid (is outside process executable
memory and is not the Null Upcall described below), the kernel MUST
NOT invoke the requested driver and MUST immediately return a failure
Expand Down Expand Up @@ -797,11 +875,12 @@ If an exit syscall is successful, it does not return. Therefore, the return
value of an exit syscall is always `Failure`. `exit-restart` and
`exit-terminate` MUST always succeed and so never return.


5 libtock-c Userspace Library Methods
=================================

This section describes the method signatures for system calls and upcalls in C, as an example
of how they appear to application/userspace code..
of how they appear to application/userspace code.

Because C allows a single return value but Tock system calls can return multiple values,
they do not easily map to idiomatic C. These low-level APIs are translated into standard C
Expand Down
5 changes: 5 additions & 0 deletions kernel/src/collections/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ pub trait Queue<T> {
/// Remove the element from the front of the queue.
fn dequeue(&mut self) -> Option<T>;

/// Remove and return one (the first) element that matches the predicate.
fn remove_first<F>(&mut self, f: F) -> Option<T>
where
F: Fn(&T) -> bool;

/// Remove all elements from the ring buffer.
fn empty(&mut self);

Expand Down
26 changes: 26 additions & 0 deletions kernel/src/collections/ring_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ impl<T: Copy> queue::Queue<T> for RingBuffer<'_, T> {
}
}

fn remove_first<F>(&mut self, f: F) -> Option<T>
where
F: Fn(&T) -> bool,
{
let len = self.ring.len();
let mut slot = self.head;
while slot != self.tail {
if f(&self.ring[slot]) {
// This is the desired element, remove it and return it
let val = self.ring[slot];

let mut next_slot = (slot + 1) % len;
// Move everything past this element forward in the ring
while next_slot != self.tail {
self.ring[slot] = self.ring[next_slot];
slot = next_slot;
next_slot = (next_slot + 1) % len;
}
self.tail = slot;
return Some(val);
}
slot = (slot + 1) % len;
}
None
}

fn empty(&mut self) {
self.head = 0;
self.tail = 0;
Expand Down
Loading

0 comments on commit db6b982

Please sign in to comment.