Intertask Communication


When one task needs to communicate with another task, it can be a simple matter of notification ("Hey! I'm finished doing what you asked me to do") or a more involved passing of detailed information ("Here's the table of values you asked me to calculate"). Portfolio provides mechanisms to handle both: signals for simple notification and messages for passing detailed information.

Signals

A signal is a kernel mechanism that allows one task to send a flag to another task. The kernel dedicates one 32-bit word in a task's TCB (task control block) for receiving signals. The kernel writes to that word each time it carries a signal to the task.

The 31 least-significant bits of the 32-bit signal word each specify a different signal; the most-significant bit is used for errors. Eight of the signal bits (0 to 7) are reserved by the kernel for system signals; the other 23 bits (8 to 30) are left for custom user-task signals.

Allocating Signals

A task can't receive signals from another user task unless it has allocated a signal bit on which to receive. To do so, it uses the AllocSignal() call, which returns a 32-bit value containing the next unused user signal bit (the 32-bit value is called a signal mask). The task can send its signal mask to other tasks; they can then send a signal back to the task, using the user signal bit set in its signal mask.

All tasks can receive system signals at any time. The lower-8 signal bits are reserved for this purpose. For example, the system sends a task a signal (SIGF_DEADTASK) whenever one of its child threads or task dies.

Sending a Signal

To send a signal to another task, a task prepares a 32-bit signal mask. It sets the appropriate bit (or bits, if it's sending more than one signal) in the mask to 1 and sets the rest of the bits to 0. For example, if the task wants to send a signal using bit 14, it creates the signal mask "00000000 00000000 01000000 00000000" (in binary). The task then uses the SendSignal() call to specify the item number of the task it wants to signal and passes along the signal mask. The kernel logically ORs the signal mask into the receiving task's TCB. Bits set in the t_SigBits field of the TCB indicate signals that the task has received, but not yet acknowledged.

Receiving a Signal

A task waits for one or more signals by using the WaitSignal() call. The kernel checks to see if any of the bits in the task's signal mask match the bit mask passed WaitSignal(), indicating that a signal has been received on that bit. If so, WaitSignal() clears the bits that match and immediately returns, letting the task act on any signals it has received. If there are no received signals in the signal mask, the task is put in the wait queue until it receives a signal it wants.

Freeing Signal Bits

To free up signal bits that a task has allocated, the task uses the FreeSignal() call to pass along a free signal mask. The free signal mask should have a 1 in each bit where the signal bit is to be freed (that is, set to 0 in the signal mask) and a 0 where the signal bit remains as it is.

Messages

A message is an item that combines three elements: a variable number of bytes of message data, 4 bytes available for an optional reply to the message, and a specified place (a reply port, explained later) where a reply to the message can be sent.

A message won't work without two message ports: one created by the task receiving the message and another created by the task sending the message. The message port is an item that sets a user signal bit for incoming message notification. It includes a message queue that receives and stores incoming messages.

Creating a Message

To create a message, a task can use a number of calls including CreateMsg(), CreateSmallMsg(), and CreateBufferedMsg(). These functions accept a string of text as the message's name, a priority for the message, and the item number of the reply port for replies to the message. It returns the item number of the newly created message for working with the message later.

Creating a Message Port

To create a message port, a task uses the CreateMsgPort() call, which it provides with a string of text as a message port name. The kernel creates a message queue in system RAM for the message port, automatically assigns a user-task signal bit for the message port, and gives the message port an item number. The task is now ready to receive messages at the port.

Sending a Message

If a task wants to send a message to another task, it must first know the item number of a message port of the receiving task. (If it knows the name of the message port, it can use the FindMsgPort() call to find the item number.) The sending task then uses either SendMsg() or SendSmallMsg() to fill out a message, providing a destination message port item number and some message data to pass. The kernel inserts the message, according to priority, into the destination port message queue, then signals the receiving task that a message has arrived at its message port.

Receiving a Message

To receive a message, a task has two options:

Replying to a Message

A task that sends a message usually needs a reply from the task that receives the message, so the sending task must specify a message port of its own as the reply port. (If the sending task doesn't have its own message port, it must create one before creating a message.) When the receiving task receives the message, it uses either ReplyMsg() or ReplySmallMsg() to return the same message to the reply port with a 4-byte reply written into the message (stored in the 4-byte msg_Result field of the Message structure). The sending task receives the reply and reads the 4-byte reply.

Interpreting a Message

When one task sends a message to another task, the meaning of the message data is completely arbitrary and is determined by the two tasks sharing the message. In many cases, the message data is composed of a pointer to a data structure created in the sending task's memory along with the data structure's size. The receiving task then uses the pointer and size to read the data at that address.