Home > Multitasking, Projects, RTOS, SKC++ > SKC++: Prototype Task Class

SKC++: Prototype Task Class

Tuesday, October 6, 2009 Leave a comment Go to comments

Today’s post discusses the basic structure and characteristics of an SKC++ task. Here is a picture of the class, together with a skeletal but updated version of the Pooled class, from which Task inherits:

Task

I emphasise that this diagram displays only the features required to show how Task is structured and set up for use in an SKC++ system. The Task class, though, is central to SKC++ and as I post other topics you will see Task approaching its final state of usefulness.

It’s also worth noting that I am assuming that people reading these SKC++ articles have a basic understanding of multitasking systems. If you do not, please let me know in your comments and I will put some tutorial material elsewhere on this site.

The Task Class

Task is an abstract class intended to serve as a base class for the real tasks of the system, which may be many and varied. Its structure mirrors closely the popular structure hitherto used to “wrap” a legacy C-based kernel’s tasking facilities, so it will be superficially familiar to many.

One notable difference from historical offerings is that Task is a template class. The reason for this, and explanations for each of the template parameters, unfold as this post progresses.

Instances and Task Pools

Most tasks are singletons, so Task‘s template parameter numObj defaults to 1. There are circumstances, however, when having several instances is appropriate. Often these circumstances also include the need to delete tasks and, at some time, re-create them. For example, a telephone exchange might have a set of CallHandler tasks, one for each call in progress. When a call is dropped, its CallHandler task is deleted and is re-created at a later time to handle another call. If tasks are to come and go like this, a pool is required for them.

Instead of distinguishing between deletable and non-deletable tasks, I decided to make all tasks deletable, which means that each “real” task class has a pool. I wanted to use the Pooled class, and I have done so, but I had to update it by adding a third template parameter called extraBytes. This is explained below. The T parameter of Pooled is filled in with the corresponding value from Task, which is, in turn, the name of the user’s class derived from Task. Likewise the numObj argument supplied to Task by the user (or the default of 1) is passed on to Pooled.

Stacks

Each task has to have its own stack and the stack size must be specified by the designer of the particular task because the amount of stack used by any given task cannot be guessed by SKC++.

Earlier, I had the bright idea (so I thought) of putting the stack into the Task object as an array, using the stackSize template argument of the class to specify the size of the array. Unfortunately, real time kernels – even this one – have to descend into assembly language in order to set up initial stack frames for tasks and to switch stack pointers during scheduling. So I would have to make the absolute address of the stack base available to the assembly code. So, if the stack were a member of the Task object, I would have to convert a pointer-to-member into a raw address. There is no legal, standard way to do this in C++, so I had another think.

One trick is not to embed the stack inside the Task object, but to keep it accessible as raw memory. However, it simplifies the memory management to keep the two together, in one contiguous block, nevertheless. Consequently, I have enhanced the Pooled class to include a third template parameter, extraBytes, the value of which Pooled adds to the size of the pooled object when determining the block size. Most users of Pooled won’t be interested in extraBytes, so it defaults to 0. In the present case, though, extraBytes is passed the value of stackSize from the Task template. Thus, the user’s task object (of class UserTask, let’s say) is at the beginning of the block, addressed as this, and the stack area starts at this+sizeof(UserTask) and ends at this+sizeof(UserTask)+stackSize. Simple!

Another way, more OO, perhaps, but with more coupling and arguably less cohesion, is to define a TaskStack class (again a template class and Pooled, according to size) and to associate each Task object with its own but separate TaskStack object. This has some appeal, and I’m keeping an open mind about it for the time being. Either way, the user’s view of Task remains the same.

Creating and starting tasks

In C++, the standard way of setting up a new object (in this case some task, derived from Task) is to rely on its constructor. SKC++ has no problem with that. However, SKC++ imposes two important constraints:

  • Tasks must be created using new.
  • Starting a task (i.e. getting its activity function running) is a distinct operation performed after the complete construction of the task object.

Although, in theory, there is nothing wrong with constructing a task object statically or as a local variable, there are problems in practice with this approach. Static construction seems OK for setting up tasks which will run forever (as most do) but requiring the Task class to support this, in addition to dynamic construction with Pooled tasks, introduces extra complications which I do not believe are warranted. As a task must, in any case, be made to run by an explicit call to start(), it is no hardship to create the task dynamically, as well, just prior to starting it. The same argument applies to creating a task within a scope (i.e. as a local variable) but, in addition, doing this is highly error-prone: if the creating function returns, for example, the task is asynchronously destroyed; probably not what is wanted. Consequently, SKC++ forces all tasks to be created dynamically, using new. To avoid clutter, the enforcement trick is not made evident in today’s diagram or discussed further in this post.

From the application’s point of view, it can be very useful to have access to a group of task objects before any of them get to run: for example it may be necessary to set up various associations among the tasks before their code starts trying to make use of them. From SKC++’s point of view, separating starting from construction is absolutely necessary if the base Task model shown above – and tried and trusted by many before me – is to work. To see why this is so, let’s first review some of the workings of a multitasking system…

A task is an active object, in UML terms. That is to say, it has some code which runs autonomously, without being called as a function by the code of some other object. Although never called as such, the autonomous code of a task is coded as a function; there is no other way to write it in C++. I decided to call this function activity because this is the term UML uses to describe an algorithm which can be expressed in an activity diagram. It’s a good match. The activity function does whatever the task designer requires of it, usually (but not always) in an infinite loop. As many tasks run concurrently, it is the job of the kernel to switch between the various running activity functions in some way, in response to various events. How that is done is a story for another day (or more) but we can and must touch upon the essence of it here, in order to understand why tasks are started the way they are.

The kernel does not call or otherwise poke about in the activity function; rather, it is the other way round. The running activity function from time to time makes calls to kernel functions. I have not yet revealed what these will be in SKC++ but certainly it is necessary occasionally to wait for some event (caused by another task or directly by an ISR) to happen, so let us invent a wait function. When the running task’s activity calls wait, it gives the kernel the opportunity (by doing a stack swap, essentially) of putting the running task on the back burner and running some other task’s activity from the point it left off. How did it “leave off”? It called wait (or something else, as yet undefined) previously itself, and now the kernel will retrieve that task’s stack and return from the call originally made by the task’s activity function. Thus all calls to wait (or other kernel functions) will eventually return but in an order and at times determined by the kernel and not by the order of the calls. This is how we get a concurrent system.

So, if the kernel doesn’t actually call our activity function, how does it get started? Well, we pass the function’s start address to a kernel routine (part of which is necessarily in assembly language) whose job it is to build an artificial stack frame, looking  just like the frame created when the kernel’s task scheduler switches tasks. When scheduling eventually does take place, and our new task is the one to be run, the scheduler simply executes its “return” in the normal way. In this case, though, the “return address” is actually the start address of our activity function, which is fooled into thinking it’s been called as a normal function.

That’s the idea, anyway, but I lied slightly. The activity function is a non-static member function, so we have the same difficulty with getting its absolute address as I gave myself, initially, with the stack. That’s where the shell function comes in. It’s static, so we can easily pass its address to the kernel. However, the fact that it’s static and in the base class, Task, means that we can’t tailor its code for any particular user task derived from Task. We don’t have to, though. Here is shell‘s code (as it would appear when coded within the class declaration):

static void shell(Task * pObj)
{


pObj->activity();
delete pObj;


}

Because activity is a virtual function, the correct version, in the user’s task, gets called when the kernel invokes shell (by artificially returning to its entry point, as previously described). Or does it? We must ensure that shell is not invoked before the user’s task is fully constructed, otherwise an attempt will be made to run the activity of the base class, which is not good! The simple way of getting things right is to call the kernel’s setup routine from start and not from Task‘s constructor. This, then, is why we need a start function, to be called after construction.

Deleting Tasks

When writing about older multi-tasking systems, written in C, authors have always been keen to point out the folly of letting one task delete another. If a task needs to be deleted, the argument goes, it should delete itself, either spontaneously or in response to some request sent to it. This it should do so that it has a chance to release resources it has acquired and generally to tidy up.

This is still good advice for a C++ system but writing delete this is rightly considered, by C++ gurus, to be a bad habit to adopt. The way for a task to delete itself  in SKC++, therefore, is simply to allow the activity function to return when it has completed its internal cleanup. The shell function then deletes the task, as seen in the code snippet in the previous section.

Coming soon

Next time, I shall be looking at the concepts of an event-driven system, and how event-handling is integrated into the tasking model of SKC++. Following that, I’ll tackle message-passing.

Some of you may be wondering why I haven’t yet addressed the vital and more fundamental topic of mutual exclusion. The answer is that its implementation in SKC++ is fairly standard and boring! I promise I will cover it, but I’d rather deal first with the things which distinguish SKC++ from other kernels. In the meantime, Niall Cooling of Feabhas has blogged extensively on the subject of mutual exclusion. You can find his blog here.

Advertisements
Categories: Multitasking, Projects, RTOS, SKC++ Tags:
  1. Peter Bushell
    Friday, November 20, 2009 at 11:33

    I shamelessly updated the original article today (20/11/09). My previous comment, a corrigendum, then became irrelevant, so I deleted that, as well!

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: