Mailboxes
A mailbox is a kernel object that provides enhanced message queue capabilities that go beyond the capabilities of a message queue object. A mailbox allows threads to send and receive messages of any size synchronously or asynchronously.
Concepts
Any number of mailboxes can be defined (limited only by available RAM). Each mailbox is referenced by its memory address.
A mailbox has the following key properties:
A send queue of messages that have been sent but not yet received.
A receive queue of threads that are waiting to receive a message.
A mailbox must be initialized before it can be used. This sets both of its queues to empty.
A mailbox allows threads, but not ISRs, to exchange messages. A thread that sends a message is known as the sending thread, while a thread that receives the message is known as the receiving thread. Each message may be received by only one thread (i.e. point-to-multipoint and broadcast messaging is not supported).
Messages exchanged using a mailbox are handled non-anonymously, allowing both threads participating in an exchange to know (and even specify) the identity of the other thread.
Message Format
A message descriptor is a data structure that specifies where a message’s data is located, and how the message is to be handled by the mailbox. Both the sending thread and the receiving thread supply a message descriptor when accessing a mailbox. The mailbox uses the message descriptors to perform a message exchange between compatible sending and receiving threads. The mailbox also updates certain message descriptor fields during the exchange, allowing both threads to know what has occurred.
A mailbox message contains zero or more bytes of message data. The size and format of the message data is application-defined, and can vary from one message to the next.
A message buffer is an area of memory provided by the thread that sends or receives the message data. An array or structure variable can often be used for this purpose.
A message that has neither form of message data is called an empty message.
Note
A message whose message buffer exists, but contains zero bytes of actual data, is not an empty message.
Message Lifecycle
The life cycle of a message is straightforward. A message is created when it is given to a mailbox by the sending thread. The message is then owned by the mailbox until it is given to a receiving thread. The receiving thread may retrieve the message data when it receives the message from the mailbox, or it may perform data retrieval during a second, subsequent mailbox operation. Only when data retrieval has occurred is the message deleted by the mailbox.
Thread Compatibility
A sending thread can specify the address of the thread to which the message
is sent, or send it to any thread by specifying K_ANY
.
Likewise, a receiving thread can specify the address of the thread from which
it wishes to receive a message, or it can receive a message from any thread
by specifying K_ANY
.
A message is exchanged only when the requirements of both the sending thread
and receiving thread are satisfied; such threads are said to be compatible.
For example, if thread A sends a message to thread B (and only thread B) it will be received by thread B if thread B tries to receive a message from thread A or if thread B tries to receive from any thread. The exchange will not occur if thread B tries to receive a message from thread C. The message can never be received by thread C, even if it tries to receive a message from thread A (or from any thread).
Message Flow Control
Mailbox messages can be exchanged synchronously or asynchronously. In a synchronous exchange, the sending thread blocks until the message has been fully processed by the receiving thread. In an asynchronous exchange, the sending thread does not wait until the message has been received by another thread before continuing; this allows the sending thread to do other work (such as gather data that will be used in the next message) before the message is given to a receiving thread and fully processed. The technique used for a given message exchange is determined by the sending thread.
The synchronous exchange technique provides an implicit form of flow control, preventing a sending thread from generating messages faster than they can be consumed by receiving threads. The asynchronous exchange technique provides an explicit form of flow control, which allows a sending thread to determine if a previously sent message still exists before sending a subsequent message.
Implementation
Defining a Mailbox
A mailbox is defined using a variable of type k_mbox
.
It must then be initialized by calling k_mbox_init()
.
The following code defines and initializes an empty mailbox.
struct k_mbox my_mailbox;
k_mbox_init(&my_mailbox);
Alternatively, a mailbox can be defined and initialized at compile time
by calling K_MBOX_DEFINE
.
The following code has the same effect as the code segment above.
K_MBOX_DEFINE(my_mailbox);
Message Descriptors
A message descriptor is a structure of type k_mbox_msg
.
Only the fields listed below should be used; any other fields are for
internal mailbox use only.
- info
A 32-bit value that is exchanged by the message sender and receiver, and whose meaning is defined by the application. This exchange is bi-directional, allowing the sender to pass a value to the receiver during any message exchange, and allowing the receiver to pass a value to the sender during a synchronous message exchange.
- size
The message data size, in bytes. Set it to zero when sending an empty message, or when sending a message buffer with no actual data. When receiving a message, set it to the maximum amount of data desired, or to zero if the message data is not wanted. The mailbox updates this field with the actual number of data bytes exchanged once the message is received.
- tx_data
A pointer to the sending thread’s message buffer. Set it to
NULL
when sending an empty message. Leave this field uninitialized when receiving a message.- tx_target_thread
The address of the desired receiving thread. Set it to
K_ANY
to allow any thread to receive the message. Leave this field uninitialized when receiving a message. The mailbox updates this field with the actual receiver’s address once the message is received.- rx_source_thread
The address of the desired sending thread. Set it to
K_ANY
to receive a message sent by any thread. Leave this field uninitialized when sending a message. The mailbox updates this field with the actual sender’s address when the message is put into the mailbox.
Sending a Message
A thread sends a message by first creating its message data, if any.
Next, the sending thread creates a message descriptor that characterizes the message to be sent, as described in the previous section.
Finally, the sending thread calls a mailbox send API to initiate the message exchange. The message is immediately given to a compatible receiving thread, if one is currently waiting. Otherwise, the message is added to the mailbox’s send queue.
Any number of messages may exist simultaneously on a send queue. The messages in the send queue are sorted according to the priority of the sending thread. Messages of equal priority are sorted so that the oldest message can be received first.
For a synchronous send operation, the operation normally completes when a receiving thread has both received the message and retrieved the message data. If the message is not received before the waiting period specified by the sending thread is reached, the message is removed from the mailbox’s send queue and the send operation fails. When a send operation completes successfully the sending thread can examine the message descriptor to determine which thread received the message, how much data was exchanged, and the application-defined info value supplied by the receiving thread.
Note
A synchronous send operation may block the sending thread indefinitely, even when the thread specifies a maximum waiting period. The waiting period only limits how long the mailbox waits before the message is received by another thread. Once a message is received there is no limit to the time the receiving thread may take to retrieve the message data and unblock the sending thread.
For an asynchronous send operation, the operation always completes immediately. This allows the sending thread to continue processing regardless of whether the message is given to a receiving thread immediately or added to the send queue. The sending thread may optionally specify a semaphore that the mailbox gives when the message is deleted by the mailbox, for example, when the message has been received and its data retrieved by a receiving thread. The use of a semaphore allows the sending thread to easily implement a flow control mechanism that ensures that the mailbox holds no more than an application-specified number of messages from a sending thread (or set of sending threads) at any point in time.
Note
A thread that sends a message asynchronously has no way to determine which thread received the message, how much data was exchanged, or the application-defined info value supplied by the receiving thread.
Sending an Empty Message
This code uses a mailbox to synchronously pass 4 byte random values to any consuming thread that wants one. The message “info” field is large enough to carry the information being exchanged, so the data portion of the message isn’t used.
void producer_thread(void)
{
struct k_mbox_msg send_msg;
while (1) {
/* generate random value to send */
uint32_t random_value = sys_rand32_get();
/* prepare to send empty message */
send_msg.info = random_value;
send_msg.size = 0;
send_msg.tx_data = NULL;
send_msg.tx_target_thread = K_ANY;
/* send message and wait until a consumer receives it */
k_mbox_put(&my_mailbox, &send_msg, K_FOREVER);
}
}
Sending Data Using a Message Buffer
This code uses a mailbox to synchronously pass variable-sized requests from a producing thread to any consuming thread that wants it. The message “info” field is used to exchange information about the maximum size message buffer that each thread can handle.
void producer_thread(void)
{
char buffer[100];
int buffer_bytes_used;
struct k_mbox_msg send_msg;
while (1) {
/* generate data to send */
...
buffer_bytes_used = ... ;
memcpy(buffer, source, buffer_bytes_used);
/* prepare to send message */
send_msg.info = buffer_bytes_used;
send_msg.size = buffer_bytes_used;
send_msg.tx_data = buffer;
send_msg.tx_target_thread = K_ANY;
/* send message and wait until a consumer receives it */
k_mbox_put(&my_mailbox, &send_msg, K_FOREVER);
/* info, size, and tx_target_thread fields have been updated */
/* verify that message data was fully received */
if (send_msg.size < buffer_bytes_used) {
printf("some message data dropped during transfer!");
printf("receiver only had room for %d bytes", send_msg.info);
}
}
}
Receiving a Message
A thread receives a message by first creating a message descriptor that characterizes the message it wants to receive. It then calls one of the mailbox receive APIs. The mailbox searches its send queue and takes the message from the first compatible thread it finds. If no compatible thread exists, the receiving thread may choose to wait for one. If no compatible thread appears before the waiting period specified by the receiving thread is reached, the receive operation fails. Once a receive operation completes successfully the receiving thread can examine the message descriptor to determine which thread sent the message, how much data was exchanged, and the application-defined info value supplied by the sending thread.
Any number of receiving threads may wait simultaneously on a mailboxes’ receive queue. The threads are sorted according to their priority; threads of equal priority are sorted so that the one that started waiting first can receive a message first.
Note
Receiving threads do not always receive messages in a first in, first out (FIFO) order, due to the thread compatibility constraints specified by the message descriptors. For example, if thread A waits to receive a message only from thread X and then thread B waits to receive a message from thread Y, an incoming message from thread Y to any thread will be given to thread B and thread A will continue to wait.
The receiving thread controls both the quantity of data it retrieves from an incoming message and where the data ends up. The thread may choose to take all of the data in the message, to take only the initial part of the data, or to take no data at all. Similarly, the thread may choose to have the data copied into a message buffer of its choice.
The following sections outline various approaches a receiving thread may use when retrieving message data.
Retrieving Data at Receive Time
The most straightforward way for a thread to retrieve message data is to
specify a message buffer when the message is received. The thread indicates
both the location of the message buffer (which must not be NULL
)
and its size.
The mailbox copies the message’s data to the message buffer as part of the receive operation. If the message buffer is not big enough to contain all of the message’s data, any uncopied data is lost. If the message is not big enough to fill all of the buffer with data, the unused portion of the message buffer is left unchanged. In all cases the mailbox updates the receiving thread’s message descriptor to indicate how many data bytes were copied (if any).
The immediate data retrieval technique is best suited for small messages where the maximum size of a message is known in advance.
The following code uses a mailbox to process variable-sized requests from any producing thread, using the immediate data retrieval technique. The message “info” field is used to exchange information about the maximum size message buffer that each thread can handle.
void consumer_thread(void)
{
struct k_mbox_msg recv_msg;
char buffer[100];
int i;
int total;
while (1) {
/* prepare to receive message */
recv_msg.info = 100;
recv_msg.size = 100;
recv_msg.rx_source_thread = K_ANY;
/* get a data item, waiting as long as needed */
k_mbox_get(&my_mailbox, &recv_msg, buffer, K_FOREVER);
/* info, size, and rx_source_thread fields have been updated */
/* verify that message data was fully received */
if (recv_msg.info != recv_msg.size) {
printf("some message data dropped during transfer!");
printf("sender tried to send %d bytes", recv_msg.info);
}
/* compute sum of all message bytes (from 0 to 100 of them) */
total = 0;
for (i = 0; i < recv_msg.size; i++) {
total += buffer[i];
}
}
}
Retrieving Data Later Using a Message Buffer
A receiving thread may choose to defer message data retrieval at the time
the message is received, so that it can retrieve the data into a message buffer
at a later time.
The thread does this by specifying a message buffer location of NULL
and a size indicating the maximum amount of data it is willing to retrieve
later.
The mailbox does not copy any message data as part of the receive operation. However, the mailbox still updates the receiving thread’s message descriptor to indicate how many data bytes are available for retrieval.
The receiving thread must then respond as follows:
If the message descriptor size is zero, then either the sender’s message contained no data or the receiving thread did not want to receive any data. The receiving thread does not need to take any further action, since the mailbox has already completed data retrieval and deleted the message.
If the message descriptor size is non-zero and the receiving thread still wants to retrieve the data, the thread must call
k_mbox_data_get()
and supply a message buffer large enough to hold the data. The mailbox copies the data into the message buffer and deletes the message.If the message descriptor size is non-zero and the receiving thread does not want to retrieve the data, the thread must call
k_mbox_data_get()
and specify a message buffer ofNULL
. The mailbox deletes the message without copying the data.
The subsequent data retrieval technique is suitable for applications where immediate retrieval of message data is undesirable. For example, it can be used when memory limitations make it impractical for the receiving thread to always supply a message buffer capable of holding the largest possible incoming message.
The following code uses a mailbox’s deferred data retrieval mechanism to get message data from a producing thread only if the message meets certain criteria, thereby eliminating unneeded data copying. The message “info” field supplied by the sender is used to classify the message.
void consumer_thread(void)
{
struct k_mbox_msg recv_msg;
char buffer[10000];
while (1) {
/* prepare to receive message */
recv_msg.size = 10000;
recv_msg.rx_source_thread = K_ANY;
/* get message, but not its data */
k_mbox_get(&my_mailbox, &recv_msg, NULL, K_FOREVER);
/* get message data for only certain types of messages */
if (is_message_type_ok(recv_msg.info)) {
/* retrieve message data and delete the message */
k_mbox_data_get(&recv_msg, buffer);
/* process data in "buffer" */
...
} else {
/* ignore message data and delete the message */
k_mbox_data_get(&recv_msg, NULL);
}
}
}
Suggested Uses
Use a mailbox to transfer data items between threads whenever the capabilities of a message queue are insufficient.
Configuration Options
Related configuration options:
API Reference
- group mailbox_apis
Defines
-
K_MBOX_DEFINE(name)
Statically define and initialize a mailbox.
The mailbox is to be accessed outside the module where it is defined using:
extern struct k_mbox <name>;
- Parameters:
name – Name of the mailbox.
Functions
-
void k_mbox_init(struct k_mbox *mbox)
Initialize a mailbox.
This routine initializes a mailbox object, prior to its first use.
- Parameters:
mbox – Address of the mailbox.
-
int k_mbox_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, k_timeout_t timeout)
Send a mailbox message in a synchronous manner.
This routine sends a message to mbox and waits for a receiver to both receive and process it. The message data may be in a buffer or non-existent (i.e. an empty message).
- Parameters:
mbox – Address of the mailbox.
tx_msg – Address of the transmit message descriptor.
timeout – Waiting period for the message to be received, or one of the special values K_NO_WAIT and K_FOREVER. Once the message has been received, this routine waits as long as necessary for the message to be completely processed.
- Return values:
0 – Message sent.
-ENOMSG – Returned without waiting.
-EAGAIN – Waiting period timed out.
-
void k_mbox_async_put(struct k_mbox *mbox, struct k_mbox_msg *tx_msg, struct k_sem *sem)
Send a mailbox message in an asynchronous manner.
This routine sends a message to mbox without waiting for a receiver to process it. The message data may be in a buffer or non-existent (i.e. an empty message). Optionally, the semaphore sem will be given when the message has been both received and completely processed by the receiver.
- Parameters:
mbox – Address of the mailbox.
tx_msg – Address of the transmit message descriptor.
sem – Address of a semaphore, or NULL if none is needed.
-
int k_mbox_get(struct k_mbox *mbox, struct k_mbox_msg *rx_msg, void *buffer, k_timeout_t timeout)
Receive a mailbox message.
This routine receives a message from mbox, then optionally retrieves its data and disposes of the message.
- Parameters:
mbox – Address of the mailbox.
rx_msg – Address of the receive message descriptor.
buffer – Address of the buffer to receive data, or NULL to defer data retrieval and message disposal until later.
timeout – Waiting period for a message to be received, or one of the special values K_NO_WAIT and K_FOREVER.
- Return values:
0 – Message received.
-ENOMSG – Returned without waiting.
-EAGAIN – Waiting period timed out.
-
void k_mbox_data_get(struct k_mbox_msg *rx_msg, void *buffer)
Retrieve mailbox message data into a buffer.
This routine completes the processing of a received message by retrieving its data into a buffer, then disposing of the message.
Alternatively, this routine can be used to dispose of a received message without retrieving its data.
- Parameters:
rx_msg – Address of the receive message descriptor.
buffer – Address of the buffer to receive data, or NULL to discard the data.
-
struct k_mbox_msg
- #include <kernel.h>
Mailbox Message Structure.
-
struct k_mbox
- #include <kernel.h>
Mailbox Structure.
-
K_MBOX_DEFINE(name)