Inter-task communication and synchronization
In previous articles, we have looked at the multi-tasking model and we have seen that each task is a quasi-independent program. Although tasks in an embedded application have a degree of independence, it does not mean that they have no “awareness” of one another. Although some tasks will be truly isolated from others, the requirement for communication and synchronization between tasks is very common. This represents a key part of the functionality provided by an RTOS. The actual range of options offered by a different RTOSes may vary quite widely – as will some of the terminology – so the best we can do in this article is review the commonly available facilities.
A Range of Options
There are three broad paradigms for inter-task communications and synchronization:
Task-owned facilities – attributes that an RTOS imparts to tasks that provide communication (input) facilities. The example we will look at some more is signals.
Kernel objects – facilities provided by the RTOS which represent stand-alone communication or synchronization facilities. Examples include: event flags, mailboxes, queues/pipes, semaphores and mutexes.
Message passing – a rationalized scheme where an RTOS allows the creation of message objects, which may be sent from one to task to another or to several others. This is fundamental to the kernel design and leads to the description of such a product as being a “message passing RTOS”.
The facilities that are ideal for each application will vary. There is also some overlap in their capabilities and some thought about scalability is worthwhile. For example, if an application needs several queues, but just a single mailbox, it may be more efficient to realize the mailbox with a single-entry queue. This object will be slightly non-optimal, but all the mailbox handling code will not be included in the application and, hence, scalability will reduce the RTOS memory footprint.
Shared Variables or Memory Areas
A simplistic approach to inter-task communication is to just have variables or memory areas which are accessible to all the tasks concerned. Whilst it is very primitive, this approach may be applicable to some applications. There is a need to control access. If the variable is simply a byte, then a write or a read to it will probably be an “atomic” (i.e. uninterruptible) operation, but care is needed if the processor allows other operations on bytes of memory, as they may be interruptible and a timing problem could result. One way to effect a lock/unlock is simply to disable interrupts for a short time.
If you are using a memory area, of course you still need locking. Using the first byte as a locking flag is a possibility, assuming that the memory architecture facilitates atomic access to this byte. One task loads data into the memory area, sets the flag and then waits for it to clear. The other task waits for the flag to be set, reads the data and clears the flag. Using interrupt disable as a lock is less wise, as moving the whole buffer of data may take time.
This type of shared memory usage is similar in style to the way many inter-processor communication facilities are implemented in multicore systems. In some cases, a hardware lock and/or an interrupt are incorporated into the inter-processor shared memory interface.
Signals are probably the simplest inter-task communication facility offered in conventional RTOSes. They consist of a set of bit flags – there may be 8, 16 or 32, depending on the specific implementation – which is associated with a specific task.
A signal flag (or several flags) may be set by any task using an OR type of operation. Only the task that owns the signals can read them. The reading process is generally destructive – i.e. the flags are also cleared.
In some systems, signals are implemented in a more sophisticated way such that a special function – nominated by the signal owning task – is automatically executed when any signal flags are set. This removes the necessity for the task to monitor the flags itself. This is somewhat analogous to an interrupt service routine.
There will be more information about signals in a future article, which describes their implementation in Nucleus SE.
Event Flag Groups
Event flag groups are like signals in that they are a bit-oriented inter-task communication facility. They may similarly be implemented in groups of 8, 16 or 32 bits. They differ from signals in being independent kernel objects; they do not “belong” to any specific task.
Any task may set and clear event flags using OR and AND operations. Likewise, any task may interrogate event flags using the same kind of operation. In many RTOSes, it is possible to make a blocking API call on an event flag combination; this means that a task may be suspended until a specific combination of event flags has been set. There may also be a “consume” option available, when interrogating event flags, such that all read flags are cleared.
There is more information about event flag groups in a future article, which describes their implementation in Nucleus SE.