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).
Requirement for Dynamic Task Creation
This capability is offered by most commercial RTOS products. However, only a small proportion of applications really have a need for the dynamic behavior. It is very common for a system to start, create all the necessary tasks (and other objects) then never create or destroy any more during the running of the application code. The dynamic task creation functionality is so ubiquitous simply because it became a “checkbox item”. One RTOS vendor introduced it, and all the others followed suit.
It is notable that the OSEK/VDX standard demands a static architecture – even though this may be applied to quite complex applications. A result of this requirement is the inability to implement OSEK/VDX by means of an intermediate layer – a “wrapper” – on top of a conventional (dynamic) RTOS product.
Implications of Dynamic Task Creation
There are several issues associated with dynamic behavior, which may be of concern.
Firstly, there is the introduction of complexity, which means that the data structures that describe tasks (TCBs) need additional information; generally, they are implemented as doubly linked lists, which adds a two-pointer overhead to the memory footprint.
All the data that describes a task must be held in RAM. This is inefficient, as much of it may just be constant data items copied from ROM. Also, on low-end processors (microcontrollers), RAM may be in short supply.
Probably the biggest worry is the potential for unpredictable resource shortages, leading to the failure to create new objects. Since the crux of a real-time system is its predictability, this sounds like an unacceptable situation. So, care is needed in the use of dynamic task (and other object) creation.
Although it is conceivable that a real time embedded system might be implemented without the use of interrupts, it would be most unusual.
Interrupts and the Kernel
When an RTOS is in use, an interrupt service routine (ISR) is normally implemented to be as light weight as possible – to “steal” the minimum amount of CPU time from the scheduled tasks. Often, a device may simply be serviced and any required work be queued up ready for processing by a task. Beyond this, it is hard to generalize about interrupts and their relationship to kernels simply because there is a lot of variability. At one extreme, the RTOS designer may conclude that interrupts are not the business of the kernel at all and it is left to the programmer to be careful not to compromise task scheduling by using too much CPU time in ISRs. At the other end of the scale, an RTOS may take complete control of the entire interrupt subsystem. Neither approach is right or wrong; they are just different.
An ISR always needs to save the “context” so that the interrupted code is unaffected by the computations of the ISR. In a system implemented without an RTOS, this is simply a matter of preserving any registers used by the ISR – generally on the stack – and restoring them before return. Some processors have provision for a dedicated ISR stack – others simply use the same one as the application code.
When an RTOS is in use, the approach may be exactly the same. In the same fashion, the stack used by the ISR may be “borrowed” from the current running task or it may be another stack dedicated to interrupts. Some kernels implement this capability even if the CPU itself does not facilitate an interrupt stack. The situation becomes more complex if the ISR makes an API call which affects the scheduler. This may result in the interrupt returning to a different task from the one that was running when the interrupt occurred.
Interrupts and the Scheduler
There are several circumstances when the ISR code operation may result in a return being made to a different task:
With a priority scheduler, the ISR may have made a ready a task with higher priority than the current one.
The ISR might suspend the current task.
With a time-slice scheduler, the clock interrupt service routine will manage the time slices and may call the scheduler when required. More on this below.
It is very common for an embedded system to have a periodic “tick clock”. Indeed, some RTOS implementations mandate such a facility being available. Commonly though, the availability of a tick clock is optional and its absence simple precludes the availability of certain facilities. The tick clock ISR may typically provide four types of functionality:
If a time-slice (or composite) scheduler is in use, the clock ISR will manage the tick count and schedule a new task each time it expires.
Commonly “system time” is maintained. This is generally just a 32-bit variable which is incremented by the clock ISR and may be set up or queried by tasks.
If the RTOS supports application timers, they will be maintained by the clock ISR, which would deal with any expiration or rescheduling requirements.
If an RTOS supports timeouts on blocking API calls or tasks can be “asleep”, these timings will be managed by the clock ISR.
In the next article, we will start to look at inter-task communication and synchronization.
Colin Walls has over thirty years experience in the electronics industry, largely dedicated to embedded software. A frequent presenter at conferences and seminars and author of numerous technical articles and two books on embedded software, Colin is an embedded software technologist with Mentor Embedded [the Mentor Graphics Embedded Software Division], and is based in the UK. His regular blog is located at: http://blogs.mentor.com/colinwalls. He may be reached by email at firstname.lastname@example.org