
Continuing the cycle of
QNX real-time operating system notes. This time, I would like to talk about cross-tasking interaction in QNX Neutrino (we will consider QNX 6.5.0). In RTOS, there is a wide range of inter-tasking mechanisms — from QNX-specific messaging to familiar signals to developers of UNIX and POSIX and shared memory. And although most of the notes will be devoted to messaging, the features of using signals, POSIX messages and shared memory will also be described. And those who read to the end will get two buns for tea.

Understanding the messaging principle is essential for a QNX system programmer, since This mechanism plays a fundamental role in the RTOS. Many familiar and familiar to the developers of the functions of the operating system are only add-ons and implemented through messaging (for example,
read()
and
write()
).
Qtx RTOS Interaction Forms
As mentioned above, messaging is a fundamental mechanism for inter-task interaction in QNX, on which several other mechanisms are based. All forms of inter-task interaction considered in this note are listed in the table below, indicating who is responsible for implementing this or that mechanism.
')
Table 1. Forms of inter-task interaction.Mechanism | Scope of implementation |
---|
Message exchange | microkernel |
Signals | microkernel |
Shared memory | procnto process procnto |
POSIX Message Queues | mqueue manager |
Unnamed (pipe) and named (FIFO) program channels | pipe manager |
In QNX RTOS 6.5.0, another form of inter-task interaction appeared - Persistent Publish / Subscribe (PPS). This is quite an interesting technology, which I will try to write about another time. Those interested can read the translation
of the PPS section of the QNX Neutruno System Architecture.
Message exchange
This is a synchronous mezhzadachny interaction mechanism implemented in the QNX Neutrino micronucleus. When developing the RTOS, this form of inter-task interaction was not accidentally chosen as the main one. Firstly, the mechanism itself is quite simple. Secondly, the synchronous transmission and reception of messages facilitates debugging. Third, testing of high-level forms of interaction (for example, software channels) based on QNX Neutrino messaging and implemented in a monolithic core revealed approximately the same performance characteristics.
The messaging mechanism in QNX is also called the SRR mechanism, after the first letters of the three main functions used in messaging
1 .
MsgSend()
is for sending a message,
MsgReceive()
is for receiving a message, and
MsgReply()
is for transmitting a response to the caller. First, consider each function separately to understand how they work, and then combine them in one example. All the arguments of the functions will not be intentionally cited so as not to be distracted and first understand the principle of operation.
MsgSend()
is used to send a message from the
client to the
server and receive a response. The concepts of
client and
server are quite arbitrary here, because the same program can be a server for some tasks and, at the same time, a client of others. For example, a database server is a server for database clients
2 . And at the same time, the database server will be the client for the file system manager. When the
MsgSend()
function is
MsgSend()
client is blocked in one of two states:
SEND
or
REPLY
.
SEND
status means that the
client has sent a message, and the
server has not yet received it. After the
server receives the message, the
client enters the
REPLY
state. When the
server returns a response message, the
client is unlocked.
MsgReceive()
is used to receive messages from
clients .
The server calls
MsgReceive()
and is blocked in the
RECEIVE
state if none of the
clients have yet sent him a message, i.e. did not call the
MsgSend()
function. After this happened (a message was sent to the
server ).
The server unlocks and continues its execution.
The server usually needs to perform some actions to process the received message and prepare to receive a new one. If the
server is working in several threads, then another thread can execute the message processing and response to the
client . Most often, the thread receiving the message works in the "perpetual" cycle and after processing the received message, it calls
MsgReceive()
again.
MsgReply()
used to send a response message to
client 3 . When the
MsgReply()
function is
MsgReply()
lock does not occur, i.e.
the server will continue to work. This is done because the
client is already in a locked state (
REPLY
) and no additional synchronization is required.
What else needs to be learned in order to make up from those building blocks of knowledge that we have, a bridge to understanding the QNX messaging mechanism? Not so much. Be patient, now the picture will begin to take shape.
The QNX Neutrino microkernel does not care about the contents of the transmitted message. Messages do not have any format. The message makes sense only for the
client and
server . The microkernel only copies the message (i.e., just the data buffer) from the
client 's address space to the
server's address space (and vice versa when responding) and there is no intermediate buffer for storing messages. And that means there is no intermediate copy, because the microkernel copies data directly from the
client ’s memory to the
server’s memory (and vice versa when responding). As a result, the speed of the message passing mechanism increases.
To synchronize the transmission, reception, and response to a message, the microkernel blocks the threads involved in exchanging messages in one of three states:
SEND
,
RECIEVE
and
REPLY
.
The client is blocked in the
SEND
state until the
server accepts its message.
The server is locked in the
RECEIVE
state if none of the
clients sent a message to it. After receiving the message by the
server , it is unlocked, and the
client enters the locked state
REPLY
. After the
server returns a response to the
client , the latter is unlocked. That's all.
So, we figured out how the QNX messaging engine works. Fig. 1 simply illustrates the above.
Fig. 1. Messaging in QNX Neutrino.How does the client find the server ?
If you understand how the messaging mechanism works in the QNX RTOS, you can move on. Probably, you should have one question, without deciding which, you will not be able to exchange messages in QNX. How does the
client find the
server ?
Messages are not transmitted directly between threads. Instead, channels and connections are used.
The server creates a channel using the
ChannelCreate()
function. Now, finally, the
server can call
MsgReceive()
and
MsgReply()
with a clear conscience. A piece of code below illustrates the operation of the
server :
chid = ChannelCreate( flags ); for (;;) { rid = MsgReceive( chid, &msg, sizeof( msg ), NULL ); switch ( msg.type ) { } MsgReply( rid, EOK, NULL, 0 ); }
In turn, the
client creates a connection to the
server channel using
ConnectAttach()
, and then calls
MsgSend()
.
Client code
is very simple:
coid = ConnectAttach( nd, pid, chid, _NTO_SIDE_CHANNEL, 0 ); MsgSend( coid, smsg, sizeof( smsg ), rmsg, sizeof( rmsg ) );
Now the last question remains. Where
chid
client find out the
server parameters:
nd
,
pid
,
chid
? These parameters are the
server address or even the phone number with the city code and extension number. Half of the answer to this question is that the
server itself knows all these parameters. But how can the
server report them to the
client ?
There are various ways to get this information from the
server . You can use
.pid
files or global variables. But the right way for small applications is to use the
name_attach()
function in the
server , and
name_open()
in the
client . An even more correct way is to implement the
server as a
resource manager 4 , when it is responsible for the namespace element.
Composite messages
As already mentioned, one of the main advantages of the QNX Neutrino messaging engine is its high performance. This is achieved by the fact that there is no intermediate copying of data, i.e. The message is copied directly from the
client ’s memory to the
server’s memory. It often happens that these messages are in different places. A typical case is when the data is a raw buffer received from the hardware and a structure with information about the data (message header). The raw data itself may be located in a circular buffer. Is it really necessary in this case to prepare a separate buffer and copy the header and data from the ring buffer there? Wouldn't it be overkill? This unnecessary copying of data before sending a message can be avoided in the QNX RTOS by using composite messages.
To form composite messages in QNX Neutrino, you need to declare an array of type
iov_t
, with the number of elements equal (or more) to the number of messages, and initialize each element using the
SETIOV()
macro, i.e. specify the address and size of each buffer. Fig. 2 illustrates the principle of operation of composite messages.
Fig. 2. An example of a composite message.To work with composite messages, the familiar functions of
MsgReceive()
and
MsgReply()
, but with the end of v, i.e.
MsgReceivev()
and
MsgReplyv()
. Since the function of sending a message
MsgSend()
also receives the result, it acquires a whole family:
MsgSendv()
,
MsgSendsv()
and
MsgSendvs()
. Now the microkernel will do all the extra work for us, and no additional copying and buffer with the whole message. I like that!
Impulses
Sometimes it is only required to inform another thread that something has happened, and no response is required. So it is not necessary and blocking on
MsgSend()
. In this case, the
MsgSendPulse()
function comes to the
MsgSendPulse()
. The pulse contains 8 bits of code and 32 bits of data. Very often, pulses are used in interrupt handlers. For pulses, queues are used, i.e. impulses will not be lost if the flow for some time did not accept them. But be prepared, sooner or later, to get an
EAGAIN
error if you send impulses to a stream that does not have time to read them.
Signals
The QNX Neutrino RTOS supports a signaling mechanism that should be familiar to UNIX developers. Both standard POSIX and real-time POSIX signals are supported. To work with both types of signals, the same microkernel code is used. As a result, the microkernel itself becomes more compact, and POSIX signals (at the request of an application) can be queued, just like their colleagues from the POSIX real-time signal group. Among other things, QNX Neutrino extends the POSIX standard and allows you to send signals to a specific stream. And this is sometimes very useful.
By the way, POSIX real-time signals contain 8 bits of code and 32 bits of data. Nothing like? Precisely, the same code that implements the signal mechanism is also used in the transmission of pulses. Convenient and reliable.
Signals use the familiar functions
kill()
,
sigaction()
,
sigprocmask()
, etc., as well as more interesting functions, for example,
pthread_kill()
and
pthread_sigmask()
.
Programming channels
Software channels should be familiar to UNIX users and developers. In QNX, all the familiar commands, functions and techniques also exist. For example, this is how an unnamed program pipe (pipe) is created on the command line:
To create a named pipe (FIFO), you must use the
mkfifo
command or the
mkfifo()
function.
There is only one feature. In order for QNX Neutrino to work with software channels, it is necessary to start the
pipe
manager.
POSIX Message Queues
Message queues are similar to named programmatic channels (FIFOs), but are more complex mechanisms because support message priorities. To work with POSIX message queues in QNX Neutrino, you must run the
mqueue
manager.
Note that message queues in the QNX RTOS can contain more than one slash '/', which means you can create directories. This extends the POSIX standard, which requires that the queue name begin with a slash and no longer contain this character. A rather convenient feature, since You can group the queues of one software package or one company in one directory.
Shared memory
Working with shared memory in QNX Neutrino is the same as in other UNIX systems. Since the shared memory mechanism is implemented in the
procnto
process
procnto
, which also contains the microkernel, you do not need to run anything else. You can read more in the documentation on the functions
shm_open()
and
mmap()
.
Separately, it should be noted that the shared memory itself is not suitable for inter-task interaction. Even if the
server only writes to the shared memory, and the
client only reads from it, it may happen that the client subtracts partially modified data. Such an error can be difficult to catch later, so it is better not to make it at all. To eliminate such a situation, it is necessary to apply one of the synchronization primitives, for example, mutexes or semaphores.
Not least of interest is the method of inter-task interaction, with the simultaneous use of the messaging mechanism and shared memory. This is especially useful if you plan to build a distributed system, because shared memory is not available over the network. This solution has a very high performance due to the use of shared memory, and also has the ability to synchronize and network transparency through the exchange of messages.
You should not try to build any interaction using shared memory, based on the fact that this is the fastest way. If synchronization primitives are used, then the speed can be comparable to the speed of the messaging mechanism. Significant gain will be only in the exchange of data of a very large amount.
Promised buns
Since I promised that there will be buns, they will. I did not forget, and I do not mind. The first thing is that when sending a message, you can use the same buffer for the message itself and the response to it. I would even say that they do it quite often. To make it more convenient, all structures describing various messages and the answers to them are grouped into one union. Normally, the message sent is not required by the
client after receiving a response from the
server . This way you can save on the buffer.
The second thing is that the POSIX file descriptor in QNX Neutrino is the same as the connection (for sending messages). And this means that functions that use file descriptors (
write()
,
read()
, etc.) are just wrappers with minor overheads that convert their arguments into messages to the
server . Serious enough optimization. So, if you want to develop system software for QNX, then learn to write a resource manager.
Bibliography
- QNX Neutrino Real-Time Operating System 6.3. System architecture ISBN 5-94157-827-X
- QNX Neutrino Real-Time Operating System 6.3. User's manual. ISBN 978-5-9775-0370-9
- Rob Krten, “An Introduction to QNX Neutrino 2. A Guide for Real-Time Application Developers,” 2nd Edition. ISBN 978-5-9775-0681-6
1 The prefix Msg
in the name of the functions MsgSend()
and others appeared only in QNX Neutrino, and in QNX4 these functions were called simply Send()
, Receive()
and Reply()
. Hence the name SRR.2 In the example, the database server and clients do not use messaging functions directly, but we already know that in QNX, under each read()
, write()
, sendto()
and others, the SRR mechanism is hidden.3 If the server should return only an error code, for example, if the message is not supported, it is more convenient to use the MsgError()
function.4 A description of resource managers is beyond the scope of this note. It is possible that I will write about this some other time.