QNX From The Board Up #8 - Message-based IPC, Part 2

Learn about the asynchronous side of message-based IPC on QNX: pulses.

QNX From The Board Up #8 - Message-based IPC, Part 2

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, and
  • value 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() and
  • MsgReceive()

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!