QNX From The Board Up #8 - Message-based IPC, Part 2
Learn about the asynchronous side of message-based IPC on QNX: pulses.

Welcome to the blog series "From The Board Up" by Michael Brown. In this series we create a QNX image from scratch and build upon it stepwise, all the while looking deep under the hood to understand what the system is doing at each step.
With QNX for a decade now, Michael works on the QNX kernel and has in-depth knowledge and experience with embedded systems and system architecture.
In the last post we talked about how to:
- receive a message on a channel,
- send a message to a channel,
- reply to a received message on a channel,
... and how those operations were synchronous, i.e. blocking until something happens.
Now we'll talk about the other big aspect of message-based IPC on QNX: pulses.
Pulses
Messages are nice, but, what if your message is basically "FYI!" and you don't need a reply. Or, the message is really small and you don't need a reply.
That's where pulses come in.
A pulse is kinda like a very small message, but it can't be replied to.
Let's look at how to send a pulse.
Sending Pulses: MsgSendPulse()
The signature for this function is:
int MsgSendPulse ( int coid,
int priority,
int code,
long value );
There's the coid
to identify the connection over which to send the pulse. Then, skipping priority
for now, we see the "data" for our wee message: code
and value
, where:
code
is an 8-bit value, andvalue
is a 64-bit value.
8 bits and 64 bits. That might not seem like a lot of data, but technically you have 2^(64+8) = 4,722,366,482,869,645,213,696 unique values with which to work.
Technically. But, not actually. We'll come back to this.
Receiving Pulses
There are two ways to receive a pulse:
MsgReceivePulse()
andMsgReceive()
Let's take a look at the obvious one first:
int MsgReceivePulse( int chid,
void * pulse,
size_t bytes,
struct _msg_info * info );
chid
aka channel id makes sense: you send a pulse over a connection, and receive it on a channel.
Let's take a look at the structure for holding a received pulse:
struct _pulse
{
uint16_t type;
uint16_t subtype;
int8_t code;
uint8_t zero[3];
union sigval value;
int32_t scoid;
};
An obvious question is "If I send an 8-bit code
and a long
value
in a pulse, where are they in the received pulse?"
I hope the code
is obvious, but, we have to expand the value
in the struct _pulse
which is a union sigval
:
union sigval {
int sival_int;
void* sival_ptr;
long sival_long;
};
... i.e. the value
field in the struct _pulse
is a union
; it's a bit of syntactic sugar.
To understand why we use a union
instead of a long
like in MsgSendPulse()
, we have to understand how pulse codes are used.
Pulse Codes
We said earlier that a code is an 8-bit value, but, we can see from the _pulse
struct that it's treated as a signed 8-bit value, i.e. can be between -128 and 127, inclusively.
There's a good reason for this, and it's because one of the users of pulses is QNX; we need to reserve some pulse codes for ourselves. By using an int8_t
, we can make it really simple:
- negative pulse codes, [-128, -1], are reserved for QNX;
- positive pulse codes, [ 0, 127], are reserved for use by user code.
For the user range, we've defined two preprocessor macros:
_PULSE_CODE_MINAVAIL
and_PULSE_CODE_MAXAVAIL
so that you can create your own pulse codes, e.g.:
#define MTB_PULSE_CODE_MAUDE (_PULSE_CODE_MIN_AVAIL + 0)
#define MTB_PULSE_CODE_DONNY (_PULSE_CODE_MIN_AVAIL + 1)
#define MTB_PULSE_CODE_MARTY (_PULSE_CODE_MIN_AVAIL + 2)
...
and not conflict with QNX's pulse codes.
Similar to messages, which start with a message type that indicates how the rest of the data in the message is to be interpreted, a pulse's code determines how to interpret the rest of the data, i.e. the value
. We don't know how you are going to use your codes and what data you pass for your codes, so, we provided a union to make it slightly easier, syntactically.
That means we've now accounted for the code
and value
that we sent. What are the other fields in the _pulse
struct? type
? subtype
?
This is where it gets a little tricky, but, not really once we take into account the other way to receive a pulse: MsgReceive()
.
MsgReceive()
In the last article we talked about how to receive a message on a channel using MsgReceive()
. And, we said that it's common to have messages that start with a type
field which indicates the message type. Well, in practice it's also common for messages to have a subtype
too (or some other extremely useful 16-bit value), with the rest of the data in the message being specific to the (type
, subtype
) pair.
Now, given this, and that MsgReceive()
is used to receive messages AND pulses, and therefore will be used to receive:
- a variety of messages of type (
type
,subtype
), and - a variety of pulses with (
code
,value
)
... it made sense to just add type
, subtype
to the header of a pulse. In a pulse, they'll both have the value of (_PULSE_TYPE
, _PULSE_SUBTYPE
).
The next question you might then have is: how does MsgReceive()
tell you if you received a message or a pulse?
Well, we know that when we receive a message, we get a "receive id" which we can use to reply to the message. But, we don't need to respond to a pulse, because that's it's whole thing: asynchronous notification that doesn't need a reply.
To make it possible to distinguish between receiving a pulse vs. a message, if you receive a pulse then MsgReceive()
returns 0
(which is guaranteed to never be used as a proper receive id).
MsgReceive()
0 bytes?
Remember last time when we discussed MsgRead()
how one could, in theory, call MsgReceive()
to receive 0
bytes and then read with MsgRead()
? That is one way to live your life, yes. However, because of a couple things, i.e.
- pulses, and
- messages having a type and subtype for demultiplexing,
... most people will call MsgReceive()
with a buffer big enough to receive enough for the message headers, or the majority of the entirety of their messages, and then call MsgRead()
if there's any extra data to follow. This approach helps to keep the number of kernel calls to a minimum. Kernel calls are fairly cheap, but they're not free. Might as well do what you can to save yourself a trip.
dispatch Framework
And, because of this, the dispatch framework also allows you to provide guidance on how big a buffer to use when the framework calls MsgReceive()
inside dispatch_block()
. e.g. The msg_max_size
field of the message_attr_t
struct used when you attach a message handler.
We'll revisit this when we discuss the dispatch framework in isolation.
Quick Recap
OK, we've covered:
- sending and receiving messages, and
- replying to messages,
- sending and receiving pulses.
There's a lot more to talk about with messages and pulses (priority inheritance, UNBLOCK pulses, DISCONNECT pulses, sigevents, server monitor, ...), but, that can wait.
Coming Up...
Now that we've covered the basics of kernel calls, and message-based IPC, we can start talking about the interfaces through which the QNX kernel provides its services, and the architecture of the QNX kernel.
But first, let's revisit initializing the system to get to the point of executing the first line of the initialization script, where those QNX kernel services start being used. That's coming up next!