Inter-thread communication
Last updated
Last updated
The previous chapter discussed synchronization between threads, and mentioned concepts such as semaphores, mutexes, and event sets; this chapter continues the content of the previous chapter and explains inter-thread communication. In bare metal programming, global variables are often used for communication between functions. For example, some functions may change the value of a global variable due to some operations, and another function reads this global variable and performs corresponding actions based on the read global variable value to achieve the purpose of communication and collaboration. RT-Thread provides more tools to help pass information between different threads, and this chapter will introduce these tools in detail. After studying this chapter, you will learn how to use mailboxes, message queues, and signals for communication between threads.
Mailbox service is a typical inter-thread communication method in real-time operating systems. For example, there are two threads. Thread 1 detects the key status and sends it, while Thread 2 reads the key status and changes the LED on and off accordingly. Here, the mailbox method can be used for communication. Thread 1 sends the key status as an email to the mailbox, and Thread 2 reads the email in the mailbox to obtain the key status and turns the LED on and off.
Thread 1 here can also be expanded into multiple threads. For example, there are three threads in total, thread 1 detects and sends the key status, thread 2 detects and sends ADC sampling information, and thread 3 performs different operations according to the type of information received.
The mailbox of the RT-Thread operating system is used for inter-thread communication, which is characterized by low overhead and high efficiency. Each email in the mailbox can only hold a fixed 4-byte content (for a 32-bit processing system, the size of a pointer is 4 bytes, so one email can hold just one pointer). A typical mailbox is also called an exchange message. As shown in the figure below, a thread or interrupt service routine sends a 4-byte email to the mailbox, and one or more threads can receive and process these emails from the mailbox.
The non-blocking mail sending process can be safely applied to interrupt services. It is an effective means for threads, interrupt services, and timers to send messages to threads. Generally speaking, the mail receiving process may be blocking, depending on whether there are mails in the mailbox and the timeout set when receiving mails. When there are no mails in the mailbox and the timeout is not 0, the mail receiving process will become blocking. In this case, only threads can receive mails.
When a thread sends mail to a mailbox, if the mailbox is not full, the mail will be copied to the mailbox. If the mailbox is full, the sending thread can set a timeout, choose to wait and suspend, or directly return - RT_EFULL. If the sending thread chooses to suspend and wait, then when the mail in the mailbox is collected and space is freed up, the waiting and suspended sending thread will be awakened to continue sending.
When a thread receives mail from a mailbox, if the mailbox is empty, the receiving thread can choose whether to wait and suspend until a new mail is received and wake up, or set a timeout. When the set timeout is reached and the mailbox still does not receive mail, the thread that chooses to timeout will be woken up and return - RT_ETIMEOUT. If there is mail in the mailbox, the receiving thread will copy the 4-byte mail in the mailbox to the receiving buffer.
In RT-Thread, the mailbox control block is a data structure used by the operating system to manage mailboxes, represented by the structure struct rt_mailbox. Another C expression, rt_mailbox_t, represents the handle of the mailbox, which is implemented in C language as a pointer to the mailbox control block. For the detailed definition of the mailbox control block structure, please see the following code:
The rt_mailbox object is derived from rt_ipc_object and is managed by the IPC container.
The mailbox control block is a structure that contains important parameters related to events and plays an important role in the implementation of mailbox functions. The mailbox-related interface is shown in the figure below. The operations on a mailbox include: creating/initializing a mailbox, sending mail, receiving mail, and deleting/leaving a mailbox.
Creating and Deleting Mailboxes
To dynamically create a mailbox object, you can call the following function interface:
When creating a mailbox object, a mailbox object will be allocated from the object manager first, and then a memory space will be dynamically allocated to the mailbox to store mails. The size of this memory is equal to the product of the mail size (4 bytes) and the capacity of the mailbox. Then the number of received mails and the offset of the sent mails in the mailbox are initialized. The following table describes the input parameters and return values of this function:
parameter
describe
name
Email Name
size
Mailbox capacity
flag
Mailbox flag, which can take the following values: RT_IPC_FLAG_FIFO or RT_IPC_FLAG_PRIO
return
——
RT_NULL
Creation failed
A handle to the mailbox object
Create Success
Note
Note: RT_IPC_FLAG_FIFO is a non-real-time scheduling method. Unless the application is very concerned about first-come-first-served, and you clearly understand that all threads involving the mailbox will become non-real-time threads, you can use RT_IPC_FLAG_FIFO. Otherwise, it is recommended to use RT_IPC_FLAG_PRIO, that is, to ensure the real-time nature of the thread.
When a mailbox created with rt_mb_create() is no longer in use, it should be deleted to free up the corresponding system resources. Once the operation is completed, the mailbox will be permanently deleted. The function interface for deleting a mailbox is as follows:
When deleting a mailbox, if a thread is suspended on the mailbox object, the kernel first wakes up all threads suspended on the mailbox (the thread return value is -RT_ERROR), then releases the memory used by the mailbox, and finally deletes the mailbox object. The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
return
——
RT_EOK
success
Initializing and uninitializing a mailbox
Initializing a mailbox is similar to creating a mailbox, except that it is used to initialize static mailbox objects. Unlike creating a mailbox, the memory of a static mailbox object is allocated by the compiler when the system is compiled, and is generally placed in the read-write data segment or the uninitialized data segment. The rest of the initialization work is the same as when creating a mailbox. The function interface is as follows:
When initializing a mailbox, the function interface needs to obtain the mailbox object control block that the user has applied for, the pointer to the buffer, the mailbox name, and the mailbox capacity (the number of emails that can be stored). The following table describes the input parameters and return values of the function:
parameter
describe
mb
A handle to the mailbox object
name
Email Name
msgpool
Buffer pointer
size
Mailbox capacity
flag
Mailbox flag, which can take the following values: RT_IPC_FLAG_FIFO or RT_IPC_FLAG_PRIO
return
——
RT_EOK
success
The size parameter here specifies the capacity of the mailbox, that is, if the number of bytes of the buffer pointed to by msgpool is N, then the capacity of the mailbox should be N/4.
Detaching a mailbox will detach the statically initialized mailbox object from the kernel object manager. Detaching a mailbox uses the following interface:
After using this function interface, the kernel first wakes up all threads hanging on the mailbox (the thread gets the return value - RT_ERROR), and then detaches the mailbox object from the kernel object manager. The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
return
——
RT_EOK
success
A thread or interrupt service program can send emails to other threads through the mailbox. The email sending function interface is as follows:
The mail sent can be 32-bit data in any format, an integer value or a pointer to a buffer. When the mailbox is full, the thread or interrupt program that sends the mail will receive a return value of -RT_EFULL. The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
value
Email Content
return
——
RT_EOK
Send successfully
-RT_EFULL
The mailbox is full
Users can also send emails to the specified mailbox through the following function interface:
The difference between rt_mb_send_wait() and rt_mb_send() is that there is a waiting time. If the mailbox is full, the sending thread will wait for the mailbox to be freed up due to receiving mail according to the set timeout parameter. If the set timeout period is reached and there is still no free space, the sending thread will be awakened and return an error code. The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
value
Email Content
timeout
Timeout
return
——
RT_EOK
Send successfully
-RT_ETIMEOUT
time out
-RT_ERROR
Failed, returns error
The process of sending an urgent email is almost the same as sending an email. The only difference is that when sending an urgent email, the email is directly inserted into the queue and placed at the head of the email queue. In this way, the recipient can receive the urgent email first and process it in time. The function interface for sending an urgent email is as follows:
The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
value
Email Content
return
——
RT_EOK
Send successfully
-RT_EFULL
Mailbox Full
Only when there is mail in the mailbox, the receiver can get the mail immediately and return the return value of RT_EOK. Otherwise, the receiving thread will be suspended in the waiting thread queue of the mailbox or return directly according to the timeout setting. The receiving mail function interface is as follows:
When receiving mail, the recipient needs to specify the mailbox handle for receiving mail, the storage location of the received mail, and the maximum waiting time. If the timeout is set when receiving, if the mail is still not received within the specified time, - RT_ETIMEOUT will be returned. The following table describes the input parameters and return values of this function:
parameter
describe
mb
A handle to the mailbox object
value
Email Content
timeout
Timeout
return
——
RT_EOK
Received successfully
-RT_ETIMEOUT
time out
-RT_ERROR
Failed, returns error
This is a mailbox application routine, which initializes two static threads and a static mailbox object. One thread sends mail to the mailbox, and the other thread receives mail from the mailbox. The code is as follows:
Mailbox usage routine
Note: RT-Thread 5.0 and later versions have
ALIGN
changed the keyword tort_align
, so please pay attention to the modification when using it.
The simulation results are as follows:
The example demonstrates how to use the mailbox. Thread 2 sends mails, a total of 11 times; Thread 1 receives mails, a total of 11 mails are received, the mail content is printed out, and the process is judged to be finished.
Mailbox is a simple way to pass messages between threads, which is characterized by low overhead and high efficiency. In the implementation of RT-Thread operating system, one 4-byte email can be delivered at a time, and the mailbox has certain storage functions and can cache a certain number of emails (the number of emails is determined by the capacity specified when the mailbox is created and initialized). The maximum length of an email in the mailbox is 4 bytes, so the mailbox can be used to pass messages of no more than 4 bytes. Since 4 bytes of content on a 32-bit system can just hold a pointer, when a larger message needs to be passed between threads, a pointer to a buffer can be sent to the mailbox as an email, that is, the mailbox can also pass pointers, for example:
For such a message structure, which contains the pointer data_ptr pointing to the data and the variable data_size of the data block length, when a thread needs to send this message to another thread, it can use the following operations:
In the receiving thread, because what is received is a pointer, and msg_ptr is a newly allocated memory block, the corresponding memory block needs to be released after the receiving thread completes the processing:
Message queue is another common way of communication between threads, which is an extension of mailbox. It can be applied in many occasions: message exchange between threads, receiving variable-length data through serial port, etc.
The message queue can receive messages of unfixed length from threads or interrupt service routines and cache the messages in its own memory space. Other threads can also read corresponding messages from the message queue, and when the message queue is empty, the reading thread can be suspended. When a new message arrives, the suspended thread will be awakened to receive and process the message. The message queue is an asynchronous communication method.
As shown in the figure below, a thread or an interrupt service routine can put one or more messages into a message queue. Similarly, one or more threads can get messages from a message queue. When there are multiple messages sent to a message queue, the message that enters the message queue first is usually passed to the thread first. In other words, the thread gets the message that enters the message queue first, which is the first-in-first-out principle (FIFO).
The message queue object of the RT-Thread operating system consists of multiple elements. When a message queue is created, it is assigned a message queue control block: message queue name, memory buffer, message size, and queue length, etc. At the same time, each message queue object contains multiple message boxes, each of which can store a message; the first and last message boxes in the message queue are called the message list head and message list tail, respectively, corresponding to msg_queue_head and msg_queue_tail in the message queue control block; some message boxes may be empty, and they form a free message box list through msg_queue_free. The total number of message boxes in all message queues is the length of the message queue, which can be specified when the message queue is created.
In RT-Thread, the message queue control block is a data structure used by the operating system to manage the message queue, represented by the structure struct rt_messagequeue. Another C expression rt_mq_t represents the handle of the message queue, which is implemented in C language as a pointer to the message queue control block. For the detailed definition of the message queue control block structure, please see the following code:
The rt_messagequeue object is derived from rt_ipc_object and is managed by the IPC container.