The core of the operating system is the Xous Kernel. This kernel aims to be tiny, and contains just two drivers: A serial port and a random-number generator, neither of which are exposed to programs running on the system. All other services are provided in userspace.

The kernel supports six major features:

  • Processes
  • Threads
  • Interrupts
  • Memory
  • Servers
  • Messages

All other features flow from these primitives.

Processes

Each process has its own memory space. For this reason, Xous requires an MMU. If configured, processes may be spawned at runtime, however the normal case is that the bootloader spawns all processes before the kernel begins.

Each process begins with one main thread. With the current libstd implementation the program will terminate when this thread exits, however this is not strictly required.

Processes may create threads, pass messages, claim interrupts, and allocate memory. There is no concept of an “elevated” or “privileged” process – all processes are equal.

Threads

A process may create a limited number of threads. Currently the limit is around 30 threads. Threads are preemptively time-sliced.

Threads within a process share a memory space. The Rust standard library places guard pages between threads, and a memory violation in one thread causes the entire process to terminate.

Threads may send Messages to Servers. If a Message is blocking, then that thread will stop execution until the Server replies to the message. A stopped thread does not prevent other threads in a process from running.

Interrupts

Processes can claim interrupts which will run in their own thread. There is no syscall to lock the system, so claiming an interrupt is the only way to ensure code can run with interrupts disabled.

Interrupts are available on a first-come, first-served basis, thus it is important to claim interrupts as soon as possible.

Most syscalls are unavailable during an interrupt. The only syscalls that are available are nonblocking message sending or receiving, returning messages to their sender, or forcing a context switch.

As an example, kernel preemption is implemented in userspace by having the tick-timer claim the interrupt for the system timer, and then having it call ReturnToParent in order to force a context switch to the next process.

Memory

Like interrupts, memory is available on a first-come, first-served basis. Memory is allocated with the MapMemory call.

A process may request a physical address, in which case the kernel will grant that address if it is valid. Ony valid memory may be granted. Valid memory is determined at compile time by reading the device’s SVD file.

Each page of memory may only be allocated to one process. A process may re-lend that memory, but the actual memory owner does not change. If two processes try to map the same physical area of memory, the second process will encounter an error.

When allocating memory from RAM, the memory will be cleared when it is returned. When allocating memory from other areas, the page will not be cleared. This enables processes to allocate system registers and write drivers in userspace.

When allocating RAM, pages will be demand-allocated, unless the RESERVE flag is set. This allows overcommit, which is particularly useful when allocating regions such as stack.

Additionally, there is a system heap similar to the Unix sbrk call. This is managed by calling IncreaseHeap and DecreaseHeap. There is a maximum amount of heap, which acts as a sort of release valve to ensure a process does not allocate too much memory.

It is also possible to use MapMemory to create your own heap, at which point the amount of memory is currently unlimited. This amount may be limited in the future.

Servers

A process may start a Server and receive Messages. A thread connects to a Server using a 128-bit ID. This ID may be textual if the server uses a well-known name that is exactly 16-bytes wide such as *b"ticktimer-server" or *b"xous-name-server", or it may be a random number generated by a TRNG.

A thread can create a server by making one of the following syscalls:

  • CreateServerWithAddress(14, u32, u32, u32, u32): Create a server with a given 128-bit ID
  • CreateServer(29): Create a server with a random ID and return the ID

Additionally, there are two syscalls that help manage random server IDs:

  • ConnectForProcess(30, u32 /* pid */, u32, u32, u32, u32): Connect a given server ID to a process
  • CreateServerId(31): Generate a random 128-bit ID for later use

Finally, a server is destroyed by using the following syscall:

  • DestroyServer(34, u32, u32, u32, u32): Destroy the given SID, provided it was created in this process.

A server contains a fixed amount of buffer that limits how many messages can be stored in a server mailbox – current it’s 128 messages. Messages will fill up this buffer until it’s full, at which point processes can no longer send messages to the server.

A server can retrieve messages using one of the following syscalls:

  • ReceiveMessage(15, u32, u32, u32, u32): Receive a message from the provided server, blocking until one is available
  • TryReceiveMessage(28, u32, u32, u32, u32): Receive a message from the provided server, returning if none is available

Note that multiple threads may call these functions, enabling worker pools.

Messages

Messages are the core unit of communication in Xous. Messages are guaranteed to be delivered, provided the syscall succeeds – there is no concept of a message getting lost in transit. Additionally, messages are FIFO.

A message may be one of the following:

  • Scalar: Five usize values. This message is nonblocking. Examples of Scalar messages might be notification messages or registration calls.
  • BlockingScalar: A variant of Scalar that blocks, waiting for a return value. The return value may be up to five usize values. BlockingScalar calls are used to implement mutexes and sleep calls.
  • Send: Moves memory from one process to another. A Send message contains a pointer and a length, as well as two additional usize values. The memory must be page-aligned must be a multiple of the page size, currently 4096 bytes.
  • Lend: Temporarily sends memory from one process to another. Like Send messages, Lend messages contain a pointer and a length, as well as two additional usize values, and must be page-aligned and page-sized. Lend memory can be useful for write() calls or for graphics operations, since sending pages of memory is extremely cheap.
  • MutableLend: Temporarily sends memory from one process to another such that the process may update the memory and send it back. Contains a pointer and a length as well as two usize values. These values may be updated by the recipient to indicate information such as the amount of data that was modified. MutableLend is particularly well suited for read() calls.

Scalar and Send messages are non-blocking and will return immediately. BlockingScalar, Lend, and MutableLend are all blocking calls and will wait for the server to return the value before continuing.

A common idiom in Rust is to allocate the contents for a buffer on the stack. This makes memory messages very cheap, though care must be taken to ensure you don’t overflow stack:

fn main() {
    #[repr(C, align(4096))]
    struct MemoryMessageContents {
        data: [u8; 4096],
    }
    let mut contents = MemoryMessageContents { data: [0u8; 4096] };
    send_message(contents.data.as_mut_ptr(), contents.data.len(), 0, 0);
}