Introduction
Xous is a microkernel operating system with processes, threads, and messages. It is designed to have an extremely small kernel that delegates as much as possible to userspace. This book describes the operating system kernel as well as the services that support normal operating system behavior.
As this book is a work in progress, some chapters are placeholders and will appear blank.
The book is written for two audiences: kernel maintainers, and application developers.
Chapters 2 (Server Architecture), 3 (Introducing the Kernel), and 5 (System Startup) are primarily for kernel maintainers and system programmers.
Chapters 1 (Getting Started), 4 (Renode Emulation), 6 (Build System Overview), 7 (Messages) and 8 (Graphics) are more appropriate for application developers.
Chapter 9 (PDDB) covers the Plausibly Deniable DataBase, and has sub-sections for both kernel and application developers.
Getting Help on Devices
This book focuses on the Xous kernel. For device-specific questions (e.g., how to update or factory reset a Precusor), please visit our wiki. You can also join our Matrix community and chat with our community members for help.
Architecture
Xous is a collection of small, single purpose Servers which respond to Messages. The Xous Kernel delivers Messages to Servers, allocates processing time to Servers, and transfers memory ownership from one Server to another. Every Xous Server contains a central loop that receives a Message, matches the Message Opcode, and runs the corresponding rust code. When the operation is completed, the Server waits to receive the next Message at the top of the loop, and processing capacity is released to other Servers. Every service available in Xous is implemented as a Server. Every user application in Xous is implemented as a Server.
Architecturally, Xous is most similar to QNX, another microkernel message-passing OS.
Servers
There are only a few "well known" Servers which are always available to receive Messages, and run the requested Opcode:
- The
xous-name-server
maintains a list of all registered Servers by name, and guards a randomised 128-bit Server ID for each of the Servers. The xous-name-server arbitrates the flow of Messages between Servers. - The
ticktimer-server
provides time and time-out related services. - The
xous-log-server
provides logging services. - The
timeserverpublic
provides real-time (wall-clock time) services. It is only accessed viastd::time
bindings.
The remaining servers are not "well known" - meaning that the xous-name-server
must be consulted to obtain a Connection ID in order to send the Server a Message. Such Servers include aes
com
dns
gam
jtag
keyboard
llio
modals
net
pddb
trng
.
Messages, aka IPC
Every Message contains a Connection ID and an Opcode. The Connection ID is a "delivery address" for the recipient Server, and the Opcode specifies a particular operation provided by the recipient Server. There are two flavours of messages in Xous:
- Scalar messages are very simple and very fast. Scalar messages can transmit only 4 u32 sized arguments.
- Memory messages can contain larger structures, but they are slower. They "transmit" page-sized (4096-byte) memory chunks.
Rust struct
s need to be serialized into bytes before they can be passed using Memory Messages. Xous provides convenience bindings for rkyv
, so any struct
fully-annotated with #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
can be serialized into a buffer by the sender and deserialized by the recipient.
The most simple Server communication involves a non-synchronizing "fire and forget" style of Messaging. The Sender sends a Message and continues processing immediately. The Recipient will receive the Message when it arrives, and process the Opcode accordingly. End of story. The ownership of the Message memory passes from the Sender to the Recipient and is Dropped by the Recipient. While there will be a delay before the Message is received - the sequence is assured. In the code, these are referred to as either Scalar
Scalar Messages or Send
Memory Messages.
Alternatively, A Server can send a synchronous Message, and wait (block) until the Recipient completes the operation and responds. In this arrangement, the Message memory is merely lent to the Recipient (read-only or read-write) and returned to the Sender on completion. While the sender Server "blocks", its processing quanta is not wasted, but also "lent" to the Recipient Server to complete the request promptly. In the code, these are referred to as either BlockingScalar
Scalar Messages, or Borrow
or MutableBorrow
Memory Messages. Borrow
messages are read-only, MutableBorrow
are read-write, with semantics enforced by the Rust borrow checker.
asynchronous Message flow is also possible. The Sender will send a non-synchronous Message, which the kernel will amend with a "return token". The Recipient Server will complete the operation, and then send a non-synchronous Message in reply to this return token.
A Server may also send a synchronous Message and wait for a deferred-response. This setup is needed when the recipient Server cannot formulate a reply within a single pass of the event loop. Rather, the recipient Server must "park" the request and continue to process subsequent Messages until the original request can be satisfied. The request is "parked" by either saving the msg.sender
field (for Scalar messages) or keeping a reference to the MessageEnvelope
(for Memory messages). Memory Messages automatically return-on-Drop, relying on the Rust borrow checker and reference counting system to enforce implicit return semantics.
Acknowledgement
This project is funded through the NGI0 PET Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310.