Messages and Message Passing

Messages form the basis of interprocess communication on Xous. A process exists in isolation and can only communicate to the outside world by sending messages. The limited API provided by the kernel means that almost all interactions are provided by userspace Servers, which must be communicated with using Messages.

overview of message passing

Connecting to and Disconnecting from Servers

To connect to a server you must supply it an Server ID. A Server ID is a 16-byte value of some sort that is shared in a universal namespace. If you know a Server's Server ID then you can connect to that Server.

There are a few well-known Server IDs. These include bare minimum IDs that are required by any process to do anything useful. They are:

  • b"xous-log-server ": Output log messages to the console, as well as basic println!() support
  • b"ticktimer-server": Used for sleep() as well as time-based Mutexes
  • b"xous-name-server": A central nameserver that is used for connecting to all other servers

To connect to a Server, call xous::connect(). For example, to connect to the ticktimer-server, call:

let connection_id = xous::connect(xous::SID::from_bytes(b"ticktimer-server").unwrap())?;

This will provide you a Connection to that server. If the Server is not available, the call will block until it is created. To fail if the server does not exist, use try_connect() instead of connect().

Connection Limitations

Connections are limited on a per-process basis. Each process may only establish a connection to at most 32 servers. When this number is exceeded, xous::connect() will return Error::OutOfMemory.

If you call xous::connect() twice with the same Server ID, then you will get the same connection_id.

Disconnecting

To disconnect from a server, call unsafe { xous::disconnect(connection_id)};. This function is unsafe because you can copy connection IDs, so it is up to you to ensure that they are no longer in use when disconnecting.

For example, if you connect() to a Server and spawn a thread with that connection ID, you should only call disconnect() once that thread has finished with the connection. Similarly, if you Copy the connection ID to the thread, you must make sure that both uses of the Connection ID are destroyed prior to disposing of the connection.

Because of this, it is recommended that you use an ARC<CID> in order to ensure that the connection is only closed when it is no longer in use.

Furthermore, recall that subsequent calls to connect() with the same argument will reuse the connection_id. Because of this, it is vital that you only call disconnect() when you are certain that all instances are finished with the connection.

Message Overview

Messages come in five kinds: Scalar, BlockingScalar, Borrow, MutableBorrow, and Send. Scalar and Send messages are nonblocking and return immediately, while the others wait for the Server to respond.

Borrow, MutableBorrow, and Send all detach memory from the client and send it to the server.

Scalar and BlockingScalar Messages

These messages allow for sending four usizes of data plus one usize of command. This can be used to send short updates to the Server. Scalar messages return to the client immediately, meaning the Server will receive the message after a short delay.

BlockingScalar messages will pause the current thread and switch to the Server immediately. If the message is handled quickly, the Server can respond to the message and switch back to the Client before its quantum expires.

BlockingScalar messages can return one or two usizes worth of data by returning Result::Scalar1(usize) or Result::Scalar2(usize, usize).

As an example of what can be done, the ticktimer server uses BlockingScalar messages to implement msleep() by delaying the response until a timer expires.

Borrow, MutableBorrow, and Send Messages

These messages allow for sending memory from one process to another. Memory must be page-sized and aligned, but may be any memory available to a process. For example, a hardware process may want to reserve all MMIO peripherals in the system and then share them with processes as desired.

The memory message types allow for one usize worth of tag data which can be used to describe what the message is used for.

Furthermore, messages may also contain two advisory fields: offset and valid. These fields may be used to define an offset with the memory block where interesting data occurs. Similarly, the valid field could be used to define how large the data is.

When memory is passed via MutableBorrow then the memory is mapped into the Server's address space as writable. Additionally, the offset and valid fields become writable and may be updated in the server. As an example, if a Server implemented bzero() to clear a memory range to zero, then it might clear the contents of the buffer, then set both offset and valid to 0.

Internally, the MutableBorrow is updated by passing the new fields to ReturnMemory() where it gets updated in the client.