TaskId Task::nextId = 0;
/**********************************************************************
*
* Method: Task()
*
* Description: Create a new task and initialize its state.
*
* Notes:
*
* Returns:
*
**********************************************************************/
Task::Task(void (*function)(), Priority p, int stackSize) {
stackSize /= sizeof(int); // Convert bytes to words.
enterCS(); ////// Critical Section Begin
//
// Initialize the task-specific data.
//
id = Task::nextId++;
state = Ready;
priority = p;
entryPoint = function;
pStack = new int[stackSize];
pNext = NULL;
//
// Initialize the processor context.
//
contextInit(&context, run, this, pStack + stackSize);
//
// Insert the task into the ready list.
//
os.readyList.insert(this);
os.schedule(); // Scheduling Point
exitCS(); ////// Critical Section End
} /* Task() */
Notice how the functional part of this routine is surrounded by the two function calls enterCS and exitCS . The block of code between these calls is said to be a critical section . A critical section is a part of a program that must be executed atomically. That is, the instructions that make up that part must be executed in order and without interruption. Because an interrupt can occur at any time, the only way to make such a guarantee is to disable interrupts for the duration of the critical section. So enterCS is called at the beginning of the critical section to save the interrupt enable state and disable further interrupts. And exitCS is called at the end to restore the previously saved interrupt state. We will see this same technique used in each of the routines that follow.
There are several other routines that I've called from the constructor in the previous code, but I don't have the space to list here. These are the routines contextInit and os.readyList.insert . The contextInit routine establishes the initial context for a task. This routine is necessarily processor-specific and, therefore, written in assembly language.
c ontextInit has four parameters. The first is a pointer to the context data structure that is to be initialized. The second is a pointer to the startup function. This is a special ADEOS function, called run , that is used to start a task and clean up behind it if the associated function later exits. The third parameter is a pointer to the new Task object. This parameter is passed to run so the function associated with the task can be started. The fourth and final parameter is a pointer to the new task's stack.
The other function call is to os.readyList.insert . This call adds the new task to the operating system's internal list of ready tasks. The readyList is an object of type TaskList . this class is just a linked list of tasks (ordered by priority) that has two methods: insert and remove . Interested readers should download and examine the source code for ADEOS if they want to see the actual implementation of these functions. You'll also learn more about the ready list in the discussion that follows.
Application Programming Interfaces
One of the most annoying things about embedded operating systems is their lack of a common API. This is a particular problem for companies that want to share application code between products that are based on different operating systems. One company I worked for even went so far as to create their own layer above the operating system solely to isolate their application programmers from these differences. But surely this was just adding to the overall problem — by creating yet another API.
The basic functionality of every embedded operating system is much the same. Each function or method represents a service that the operating system can perform for the application program. But there aren't that many different services possible. And it is frequently the case that the only real difference between two implementations is the name of the function or method.
This problem has persisted for several decades, and there is no end in sight. Yet during that same time the Win32 and POSIX APIs have taken hold on PCs and Unix workstations, respectively. So why hasn't a similar standard emerged for embedded systems? It hasn't been for a lack of trying. In fact, the authors of the original POSIX standard (IEEE 1003.1) also created a standard for real-time systems (IEEE 1003.4b). And a few of the more Unix-like embedded operating systems (VxWorks and LynxOS come to mind) are compliant with this standard API. However, for the vast majority of application programmers, it is necessary to learn a new API for each operating system used.
Fortunately, there is a glimmer of hope. The java programming language has support for multitasking and task synchronization built in. That means that no matter what operating system a Java program is running on, the mechanics of creating and manipulating tasks and synchronizing their activities remain the same. For this and several other reasons, Java would be a very nice language for embedded programmers. I hope that there will some day be a need for a book about embedded systems programming in Java and that a sidebar like this one will, therefore, no longer be required.
The heart and soul of any operating system is its scheduler. This is the piece of the operating system that decides which of the ready tasks has the right to use the processor at a given time. If you've written software for a mainstream operating system, then you might be familiar with some of the more common scheduling algorithms: first-in-first-out, shortest job first, and round robin. These are simple scheduling algorithms that are used in nonembedded systems.
First-in-first-out (FIFO) scheduling describes an operating system like DOS, which is not a multitasking operating system at all. Rather, each task runs until it is finished, and only after that is the next task started. However, in DOS a task can suspend itself, thus freeing up the processor for the next task. And that's precisely how older version of the Windows operating system permitted users to switch from one task to another. True multitasking wasn't a part of any Microsoft operating system before Windows NT.
Shortest job first describes a similar scheduling algorithm. The only difference is that each time the running task completes or suspends itself, the next task selected is the one that will require the least amount of processor time to complete. Shortest job first was common on early mainframe systems because it has the appealing property of maximizing the number of satisfied customers. (Only the customers who have the longest jobs tend to notice or complain.)
Round robin is the only scheduling algorithm of the three in which the running task can be preempted, that is, interrupted while it is running. In this case, each task runs for some predetermined amount of time. After that time interval has elapsed, the running task is preempted by the operating system and the next task in line gets its chance to run. The preempted task doesn't get to run again until all of the other tasks have had their chances in that round.
Читать дальше