Tasks, the context switch, and interrupts
In the previous article, we looked at the basics of tasks and scheduling. This time, we will consider some more details of these topics.
Clearly it is necessary to be able to identify and specify each task in a system. This is, of course, a requirement for other kernel objects (discussed in future articles) as well, but tasks have some subtle nuances that make a discussion of the broader topic appropriate here.
The designers of different RTOSes have taken different approaches to task identifiers, but four broad strategies can be identified:
A task is identified by means of a pointer to its “control block”. This approach seems sensible; such a pointer would always be unique to a specific task and is useful, as access to the control block is required by many API calls. The downside is that this implies that all the data about a task is held in contiguous memory (RAM), which may be inefficient. A pointer is also likely to require 32 bits of storage.
A task may be identified by an arbitrary “index number”. This value may be useful by giving access to entries in specific tables. Such an identifier may only require eight bits or less of storage, depending on any limitations on the number of task that are supported by the RTOS.
Some RTOSes only allow one task per priority level and, hence, use the priority to uniquely identify the task. This implies that a task’s priority may not be changed. This approach is really a variation of the previous approach.
Tasks might have names which are character strings. This may be useful for debugging, but is unlikely to be an efficient means of uniquely identifying a task otherwise. RTOSes that support task naming generally have an additional identifier (like a pointer), which is used by API calls etc. For most embedded systems, textual names are an unnecessary overhead on the target; a good debugger would allow naming locally on the host.
The Context Switch
When the scheduler changes which task is currently running, this is termed a “context switch”. This is a matter which is worth closer study, as the way that a context switch works is fundamental to an RTOS design.
What is a Task?
We know that a task is a quasi-independent program, which shares CPU time with a number of other tasks under the control of an RTOS. But we need to think about what really characterizes a task.
A task is ultimately a unique set of processor register values. These are either loaded into the CPU registers (i.e. the task is current) or stored somewhere ready to be loaded when the task is scheduled. In an ideal world, a CPU would have multiple sets of registers, such that one may be allocated to each task. This has been realized in some special cases. Many years ago, the Texas TI9900 series had multiple, per-task register sets, but these were implemented in main memory, which yielded limited performance. The SPARC architecture – well known historically for its use in Unix desktop systems – has a facility for maintaining multiple register sets in a ring structure, but the total number available is rather limited.
A task will probably have its own stack, the size of which may be set on a task-by-task basis or may be a global setting for all tasks in the system. This, along with the registers, provides task-specific data storage. There may be other data storage memory areas which are dedicated to a specific task.
Just about everything else may be shared between tasks.
Code may commonly be shared – either certain functions or the complete code for a task. Care is needed to ensure that the code is reentrant – primarily no static variables (either explicitly declared static or simply declared outside of functions) should be used. Beware of standard library modules that are not designed for embedded use; there are commonly many non-reentrant functions.
Sharing data is also possible, but care is needed to ensure that access to the data is tightly controlled. Ideally just one task is “owner” of the data at any one time.
When a task is de-scheduled (i.e. ceases to be current), its set of registers needs to be saved somewhere. There are at least two possibilities:
The registers may be stored in a task-specific table. This may be part of a Task Control Block (TCB). The size is predictable and constant (for a specific CPU architecture).
The registers may be pushed onto the task’s stack. This necessitates the allocation of sufficient extra stack space and provision of storage for the stack pointer (in the TCB, possibly).
Which mechanism is chosen depends on the design of a particular RTOS and on the target processor. Some (typically 32-bit) devices can address stack very efficiently; others (8-bit for example) may be better with tables.
Dynamic Task Creation
A fundamental aspect of the architecture of an RTOS is whether it is “static” or “dynamic”.
With a static RTOS, everything is defined at application build time – notably the number of tasks in the system. This seems reasonably logical for embedded applications, which typically have a specific, bounded functionality.
A dynamic RTOS, on the other hand, starts up with a single task (which may be a special “master” task or suchlike) and creates and deletes other tasks as and when required. This enables the system to be adaptable to changing requirements and is a much closer analog to a desktop system, which behaves in exactly this fashion.
The static/dynamic characterization also applies to other kernel objects (which will be discussed in future articles).