Tom Green is an engineer at Central Data Corp. in Champaign, Ill. He can be reached at 217-359-8010.
About two years ago, the company I work for started using a multitasking kernel on our Multibus II SCSI and communications controllers. This was my first exposure to a multitasking kernel, and I was curious about the algorithms required to run multiple threads of code on a single processor. I decided to try to write a small preemptive kernel (a kernel that switches tasks by using a timer) for the MS-DOS environment. For my first attempt I used Turbo C, and I wrote a small working kernel in a couple of weeks. The kernel stole the MS-DOS timer-tick interrupt and switched between tasks (C functions that repeat forever) with each timer tick.
When the Zortech C++ compiler came out for MS-DOS machines, I immediately bought it and started to rewrite my kernel. C++ objects, I thought, were perfect for a multitasking kernel. Tasks would be objects that the task_control object would communicate with and switch between. C++ would also allow task objects to make all of their data private, and task objects could be created with a constructor that required parameters. The task_control object would also keep all data private and supply several routines so that users could add tasks, start and stop multitasking, and so on.
I used Modula-2 for the model when designing task communication. The signal object is modeled on the Modula-2 data type that you see in many Modula-2 packages. This object is used by a task to send or receive a message, and it supplies a fast and lean interface. I added a block routine (which allows a task to voluntarily give up control) and a task-control routine that allows a task to stop task switching if it is calling a routine that is not reentrant (MS-DOS, BIOS, or C library routines).
The kernel presented here may seem like a very minimal kernel, and that's because it was meant to be. If you understand how the kernel works, however, you will find that it is easy to expand and customize. There are many different design philosophies for kernels, but I think that this kernel supplies what you need for most programming situations.
This kernel uses three C++ classes: task_control, task, and signal. If you are new to C++, a quick explanation might be in order.
A "class" is similar to a C structure; in fact, in C++ a structure is a class in which all members are public. The data in a class can be public or private. A class can also have "methods" (another name for a function) associated with it. An object is a variable of a specific class. When an object is instantiated, space is allocated for all of the data (public and private) and a set of methods (functions) then exist to interface with that object. These methods are used just like data in a structure, with the . (period [structure field selection]) operator or the -> (arrow [structure field selection with indirection]) operator. This does not mean that there are separate copies of a method's code for each object. The Zortech compiler (and also, I assume, C++ preprocessors) passes to the method a hidden pointer to the object. This pointer is used by the method to access the object's data.
When I talk about objects, I will also refer to "constructors" and "destructors." A constructor is called when an object is declared; it is used to initialize the object. A destructor is called when an object goes out of scope; it is most often used to free the memory allocated by the constructor.
All of the data in each of this kernel's objects is private to prevent someone from accidently destroying the workings of the kernel. A few public routines (or methods) are provided for initialization and for communicating with the task_control object.
A signal object is a queue of task objects. The data for a signal object is two task-object pointers, which are used to create a queue (a head and tail pointer). This is the simplest object in the kernel. It has only two methods: get_task_q, which gets the next pointer to a task object from the queue, and put_task_q, which appends to a task object pointer to the queue. These methods are private and can only be called by the task_control object (which is a friend of a signal object). A constructor for signal objects initializes the queue head and tail pointers.
A task object contains several pieces of data that are used by the task_control object. Except for a constructor and a destructor, there are no methods for interfacing with a task object. Signal objects and the task_control object are "friends" of task objects. This means that they may directly manipulate the private data of a task object. I did this to avoid the overhead that I might incur by using methods to interface with task object private data. C++ supports in-line functions, but, unfortunately, the Zortech compiler does not generate in-line code in all cases.
When a task object is declared, the constructor is called and the private data is initialized. A workspace or stack is allocated for the task object. A task "image" is set up in the allocated memory. The "image" is an area of memory that has an image of all of the registers (8086) of the task when it was last stopped. In this case, we want to set up a register image to be used the first time the task is switched to. The task object carries a pointer to the task image which is loaded into the stack pointer when you want to activate the task.
See the structure called task_image in Listing One, (task.hpp) page 84, to get an idea of how this task image looks. It shows how the stack would look after an interrupt handler had been entered and all of the registers had been saved, which is how the kernel saves the image of a running task before switching to another task.
When the task image is set up, a routine called getCS is called to get the code segment of the task. This is necessary because of a compiler bug that returns the contents of the DS register when you try to get the segment of a near function pointer. Because of this, the kernel will only work with the small model of the compiler.
The task object also carries a task-state flag and a pointer to a task object. This next_task pointer is used by the signal's methods to append and remove task objects from a signal queue. The destructor for a task object frees the memory allocated for a task object's stack or workspace.
The task_control object takes care of switching between tasks and provides an interface to the outside world. Its methods allow a task to wait for or send a signal, turn on and off task switching, and so on. Before multitasking is started, task objects must be added to the task_control object with a call to add_new_task. The task_control object has a signal object called ready_q to which all tasks that are ready to run are added. This object is not really used to signal tasks, but it is used because a signal object is a queue. After all of the task objects have been added, a call to start_tasks saves the old timer-tick interrupt handler, installs a new interrupt handler, and starts up the first task when the first timer interrupt occurs. The task_control object continues to switch between tasks until stop_tasks is called. When this happens, the original timer-tick interrupt handler is reinstalled, and execution is resumed at the point after the start_tasks routine was called.
When the kernel is running, tasks are switched 18.2 times a second. When a timer-tick interrupt occurs, the task that is running is appended to ready_q and the next task to run is removed from the head of ready_q. The new task runs until the next timer interrupt and the process is repeated.
Tasks can also voluntarily give up control and call the kernel to run the next task on the ready_q. A task may block, send a signal, or wait for a signal. Using send and wait allows tasks to communicate.
A task can use task_control::block(), which takes no parameters and is called by a task to voluntarily give up control. The calling task is appended to ready_q and the next task to run is removed from the head of ready-q. This is used by a task to allow other tasks to run when it is done.
A task calls task_control::wait() with the address of a signal object to wait for a signal from another task. The calling task is appended to the signal object's queue, and the next task to run is removed from the head of ready-q.
Tasks call task_control::send() with the address of a signal object to send a signal to a waiting task. If there are any task objects in the signal object's queue, the task from the head of the queue is removed and appended to ready-q. The calling task object is then appended to ready-q. The next task to run is removed from the head of ready_q.
The task_control object has two methods to enable and disable task switching by the timer-tick interrupt. This allows a task to prevent a switching if it is calling a routine (MS-DOS, BIOS, or C library) that is not reentrant. Call task_control::lock() to disable task switching and task_control::unlock() to reenable task switching. These routines do not affect a task that is voluntarily giving up control and calling the kernel (block, send, or wait).
One warning about the task_control object: there can only be one. If you look in Listing Two on page 84, you will find a global variable called gl_tptr, which is a pointer to a task_control object. In the constructor for a task_control object, this pointer is initialized to the address of the task_control object (the "this" pointer). The pointer gl_tptr is used by the timer-tick interrupt handler and the save_image routine in Listing Three (timer.asm), page 88. This is the hidden pointer (passed to a C++ routine) that is needed for calling task_control::task_switch(). This code is a little ugly, but it allows the task-switching code to be written in C++, and it allows task_switch to access the private data of task and signal objects (by being a friend).
Task switching would not be possible if you could not save a task image of all of the 8086 registers. This is accomplished by the timer_handler and the save_image routines in Listing Three. These routines both do the same thing: they save the task image on the task's stack by pushing the necessary registers and then call the C++ task_switch routine. Let's look at these routines more closely.
The new timer-tick interrupt handler is timer_handler. It saves all necessary registers and restores the DS register so the C++ code can find its data. The original timer-tick interrupt handler is called to take care of the hardware needs for the timer interrupt. Next, several parameters are pushed to prepare for a call to the C++ task_switch routine. I will discuss these parameters later. After the task_switch routine is called, a far pointer to the new task object's image is returned. The sp and ss registers are loaded with this pointer, and the new task object's registers are popped from the stack. An iret instruction is executed, and we are running our new task.
The save_image assembly routine is called by the C++ block, send, and wait methods. This routine is used to save the image of a task just as timer_handler does. This routine is a little tricky because it is passed two parameters (a flag and a signal pointer) on the stack. These parameters are popped off the stack to pass to the C++ task_switch routine, then pushed back on, and then the stack is rearranged so the flags and a far return address are on the stack (as if an interrupt occurred). The rest of the registers are pushed as in timer_handler. The rest of the routine is just like the timer_handler routines; the C++ task_switch method is called, and on return the SS and SP registers are set up to run the next task.
The task_switch method is called by the timer_handler and save_image routines. These routines pass a far pointer to the image of the currently running task. The pointer to the task object is put on ready-q or on the queue of a signal. The flag passed by the routines is used to tell task_switch where it was called from. task_switch needs to know if a timer interrupt or a send, wait, or block call occurred. Once task_switch decides what task should run, the far pointer to this task's image is returned to the save_image or timer_handler. The task runs after registers are restored and an iret is executed.
I hope that this has shown how easy task switching really is. The important part of task switching is saving all of the registers of the running task and then saving a far pointer to that area of memory. Each task has its own stack (memory that has been allocated with malloc). The far pointer points into this allocated stack to the area where the registers were pushed. Restoring a task to its last running state just requires getting the far pointer, loading it into the ss and sp registers, restoring the registers, and doing an iret. The new task will then be running.
To compile the multitasking program shown in Listing Four, page 90, which demonstrates the concepts discussed so far, type ztc taskdemo task timer. The Zortech ztc program looks at each of the source files and runs the C++ compiler or assembler as necessary. The linker then creates the executable file.
The taskdemo program has five tasks that demonstrate all of the ways that tasks can be switched. Not much happens in the program. The five tasks run, and counters are incremented. The task0 task takes care of printing the counters for the other four tasks. Let's look at each task.
The task0 task makes a lock( ) call to make sure it is not switched until it has finished writing to the screen. The counter for each of the other four tasks is printed. Then stop_tasks( ) is called if a key has been pressed. If no key has been pressed, the unlock( ) routine is called to reenable task switching. The block( ) routine is then called to allow other tasks to run.
The task1 and task2 tasks increment their counters and will run until a timer interrupt occurs. These tasks will receive a big chunk of time to run, so the counters increment very quickly.
The task3 task increments its counter and then calls wait(), which allows the next task to run. Next, task4 increments its counter and then calls send(), which causes task3 to be placed on ready_q. The call to send() allows the next task on the ready_q queue to run. The net effect of this is that the counters of task3 and task4 increment very slowly, because each task allows another task to run after incrementing its counter.
It should be easy to change or add features to this simple kernel. For example, in the Modula-2 implementations of send() that I have seen, if there is no task on the signal queue, then a flag is set so the next task to wait on the signal queue will get this message. In my version of the kernel, this message would be lost. If you do not think a message should be lost, then just add a flag to the signal class.
Modula-2-style task communication was the perfect approach for the "simple and lean" kernel approach. It allows the task_control object to get by with a very small data structure, because signal objects carry around all of the data they need. The interface to these signal objects consists of a couple of simple and fast methods to get and put pointers on a queue. The task_control object uses the same simple methods to get the next task to run off of ready-q. If you would like to see a more complete explanation of Modula-2-style task communication, I recommend Chapter 16 of the book Modula-2: A Software Development Approach.
I do have a couple of suggestions on how to improve this kernel. Assigning a priority to a task can be a handy feature. This would simply entail adding a ready_q to the task_control object for each priority level, or perhaps using an array of ready_q queues. Do not use too many different priority levels; five would probably be enough. The task_switch routine would then search the ready_q queues, highest priority first, to find the next task to run. When a task was appended to a ready_q, you could simply check the task's priority and append it to the correct ready_q.
Adding a way for a task to sleep for a number of clock ticks might be useful as well. This would be a little more complicated, and there are several approaches you could take. I hope you can see, however, that adding these features or customizing the kernel will be very simple.
Ford, Gary A., and Weiner, Richard S. Modula-2: A Software Development Approach, John Wiley & Sons. New York,: 1985.
Weiner, Richard S., and Pinson, Lewis J. An Introduction to Object-Oriented Programming and C++, Addison-Wesley. Reading, Mass.: 1988.
_A C++ Multitasking Kernel_
by Tom Green
[LISTING ONE]
Copyright © 1989, Dr. Dobb's Journal
/********************************************/
/* TASK.HPP */
/* Tom Green */
/********************************************/
/* this file contains classes needed to use multi-tasking kernel */
/* include this file in your source code and then link with */
/* task.cpp and timer.asm */
/* this is used when a task is initialized */
/* this is a pointer to a function */
typedef void (*func_ptr)(void);
/* this is used for interrupt handler to call old interrupt handler */
/* this is a far pointer to a function */
typedef void (far *far_func_ptr)(void);
/* this is how the registers will look on the stack for a task */
/* after they have been saved for a task switch. the sp and ss */
/* registers will point to this when a task is started from the */
/* interrupt handler or save_image */
typedef struct task_image{
unsigned int bp;
unsigned int di;
unsigned int si;
unsigned int ds;
unsigned int es;
unsigned int dx;
unsigned int cx;
unsigned int bx;
unsigned int ax;
unsigned int ip;
unsigned int cs;
unsigned int flags;
}task_image;
/* a task object. contains information needed by task_control object */
/* to do task switching and a pointer to the task's workspace (stack) */
class task{
private:
friend class task_control; // task_control object needs access
friend class signal; // signal needs access to next_task
task_image far *stack_ptr; // task stack ("image") pointer
unsigned char task_state; // task state flag
unsigned char *workspace; // address of allocated task stack
task *next_task; // pointer to next task in queue
public:
task(func_ptr func,unsigned int workspace_size); // constructor
~task(); // destructor
};
/* this is a queue for tasks */
/* it is called signal so user can define a signal for task communication */
class signal{
private:
friend class task_control; // task_control needs access
task *head;
task *tail;
task *get_task_q(void); // get next task off of queue
void put_task_q(task *tptr); // append task to queue
public:
signal(void){head=tail=0;}; // constructor
};
/* task_control object */
/* routines and methods to interface with and control tasks */
/* this object will initialize and restore interrupt vectors, */
/* keep track of timer ticks, and switch execution between the */
/* task objects */
class task_control{
private:
signal ready_q; // queue of tasks ready to run
task *current_task; // current active task
task_image far *old_stack_ptr; // return to this stack when done
unsigned int task_running; // task switching enabled flag
unsigned long timer_ticks; // 18.2 ticks/second
unsigned int task_lock; // lock out task switching
task_image far *task_switch(task_image far *stk_ptr,
unsigned int flag,
signal *sig);
public:
task_control(void); // constructor
void add_new_task(task *new_task); // add new task object to ready q
void start_tasks(void); // start switching tasks on ready_q
void stop_tasks(void){task_running=0;};
unsigned long get_timer_ticks(void){return(timer_ticks);};
void lock(void){task_lock=1;}; // current task can not be switched
void unlock(void){task_lock=0;}; // allow task switching
void send(signal *sig); // put task from sig q on ready q
void wait(signal *sig); // put task on sig q
void block(void); // task allows next to run
};
[LISTING TWO]
/********************************************/
/* TASK.CPP */
/* by Tom Green */
/********************************************/
/* this file implements the methods used by task_control and task */
/* objects */
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <int.h>
#include "task.hpp"
/* task states */
#define TASK_INACTIVE 0
#define TASK_ACTIVE 1
#define TASK_READY 2
#define TASK_WAITING 3
#define TASK_ERROR 0xff
/* flags for interface routines */
#define TASK_TIMER_INTR 0
#define TASK_SEND 1
#define TASK_WAIT 2
#define TASK_BLOCK 3
/* system timer interrupt or "timer tick" */
#define TIMER_INT 8
/* routines we need from timer.asm */
unsigned int getCS(void);
extern void timer_handler(void);
extern void save_image(unsigned int flag,signal *sig);
/* global for timer_handler to call old interrupt routine */
far_func_ptr old_timer_handler;
/* this is really ugly. */
/* when constructor for task_control object is called we save the */
/* this pointer for task switch routine to call our task_control object */
/* task_switch. this means we can only have 1 task_control object. sorry */
task_control *gl_tptr;
/* constructor for a new task. workspace will be the stack space for */
/* the task. when the timer interrupt happens the tasks "image" */
/* is saved on the stack for use later and the task_image *stack_ptr */
/* will point to this image */
task::task(func_ptr func,unsigned int workspace_size)
{
task_image *ptr;
/* get stack or "workspace" for task */
if((workspace=(unsigned char *)malloc(workspace_size))==NULL){
task_state=TASK_ERROR; // do not let this one run
return;
}
/* now we must set up the starting "image" of the task registers */
/* ptr will point to the register image to begin task */
ptr=(task_image *)(workspace+workspace_size-sizeof(task_image));
/* now save the pointer to the register image */
stack_ptr=MK_FP(getDS(),(unsigned int)ptr);
ptr->ip=(unsigned int)func; // offset of pointer to task code
ptr->cs=getCS(); // segment of pointer to task, compiler bug
ptr->ds=getDS();
ptr->flags=0x200; // flags, interrupts on
task_state=TASK_INACTIVE; // task is inactive
next_task=0;
/* destructor for a task object */
task::~task(void)
{
free(workspace);
}
/* get the next task off of a task queue */
task *signal::get_task_q(void)
{
task *temp;
temp=head;
if(head)
head=head->next_task;
return(temp);
}
/* append a task to the end of a task queue */
void signal::put_task_q(task *tptr)
{
if(head)
tail->next_task=tptr;
else
head=tptr;
tail=tptr;
tptr->next_task=0;
}
/* constructor for task_control */
/* inits private stuff for task control */
task_control::task_control(void)
{
gl_tptr=this;
task_running=0;
current_task=0;
timer_ticks=0L;
task_lock=0;
}
/* adds a task to the task ready_q */
/* call this routine after creating a task object */
void task_control::add_new_task(task *new_task)
{
if(new_task->task_state!=TASK_ERROR){
new_task->task_state=TASK_READY;
ready_q.put_task_q(new_task);
}
}
/* call to start up tasks after you have created some */
/* and added them to the ready_q */
void task_control::start_tasks(void)
{
unsigned int offset,segment;
task_running=1;
/* get address of old timer interrupt handler */
int_getvector(TIMER_INT,&offset,&segment);
old_timer_handler=(far_func_ptr)(MK_FP(segment,offset));
/* set up our new timer interrupt handler */
int_setvector(TIMER_INT,(unsigned int)timer_handler,getCS());
/* tasks will now start running */
while(task_running)
; // do nothing, trick to wait for tasks to start up
/* falls through to here when multi-tasking is turned off */
}
/* gets the next task off of sig queue and puts it */
/* on the ready_q. this suspends operation of the calling */
/* task which is also put on the ready queue */
void task_control::send(signal *sig)
{
save_image(TASK_SEND,sig);
}
/* puts the calling task on the sig queue to wait for a signal */
void task_control::wait(signal *sig)
{
save_image(TASK_WAIT,sig);
}
/* this causes the current task to be placed on the ready queue */
/* and a switch to the next task on the ready_q */
void task_control::block(void)
{
save_image(TASK_BLOCK,(signal *)0);
}
/* this routine is called to do a task switch. it is */
/* passed a task_image far * to the current stack or task "image". */
/* also pass a flag (described above) and a signal pointer if needed. */
/* a task_image * to the "image" of the next task is returned */
task_image far *task_control::task_switch(task_image far *stk_ptr,
signal *sig)
{
task_image far *temp;
task *tptr;
if(flag==TASK_TIMER_INTR) // increment clock if it is a timer interrupt
timer_ticks++;
/* this saves a pointer to stack when we first start multi-tasking */
/* allows us to return to where start_tasks was called */
if(!current_task){ // no current task so save state for restoring
old_stack_ptr=stk_ptr; // save stack pointer
current_task=ready_q.get_task_q(); // set up a current task
}
/* we have an active task, so do task switch if we can */
if(current_task->task_state==TASK_ACTIVE){
current_task->stack_ptr=stk_ptr; // save stack pointer
current_task->task_state=TASK_READY; // task is ready to go
/* do not allow task switching if tasks are locked and */
/* it is timer interrupt */
if(!task_lock || flag!=TASK_TIMER_INTR){
/* check and see what caused task_switch to be called */
switch(flag){
case TASK_WAIT:
current_task->task_state=TASK_WAITING;
sig->put_task_q(current_task);
break;
case TASK_SEND:
if((tptr=sig->get_task_q())!=0)
ready_q.put_task_q(tptr);
// fall through
case TASK_BLOCK:
case TASK_TIMER_INTR:
current_task->task_state=TASK_READY;
/* put old task on ready queue */
ready_q.put_task_q(current_task);
break;
}
/* get next task to go */
current_task=ready_q.get_task_q();
}
}
/* if we are still multi-tasking, get task ready to run */
if(task_running){
current_task->task_state=TASK_ACTIVE;
temp=current_task->stack_ptr; // get stack pointer of task
}
/* multi-tasking has stopped, get ready to return where we started */
else{ // someone called stop_tasks
int_setvector(TIMER_INT,FP_OFF(old_timer_handler),
FP_SEG(old_timer_handler));
temp=old_stack_ptr; // get back original stack
}
/* return far pointer to stack_image to do task switch */
return(temp);
}
[LISTING THREE]
;***************************************************************************
; TIMER.ASM
; by Tom Green
; Timer interrupt handler
; Timer interrupt handler calls original handler first and then calls the
; task_control object task switcher. a pointer to the stack "image"
; of the new task is returned by the task switcher.
; getCS
; returns current code segment
; save_image
; saves "image" of task as if interrupt had happened and then calls the
; task_control object task switcher. a pointer to the stack "image"
; of the new task is returned by the task switcher.
;***************************************************************************
.MODEL SMALL
.8086
.DATA
extrn _old_timer_handler:dword
extrn _gl_tptr:word
extrn __task_control_task_switch:near
.CODE
;***************************************************************************
; unsigned int getCS(void) - returns current code segment.
; this is needed because of compiler bug. when a function is cast
; to a far function, and you try to get the segment, DS is returned
; instead of CS.
;***************************************************************************
_getCS proc near
public _getCS
mov ax,cs
ret
_getCS endp
;***************************************************************************
; timer_handler - this replaces the MS-DOS timer tick interrupt (8H).
; this routine saves everything on stack, calls original timer interrupt
; handler, and then calls task_control object task switcher.
;***************************************************************************
_timer_handler proc near
public _timer_handler
push ax ;save everything
push bx
push cx
push dx
push es
push ds
push si
push di
push bp
mov bp,dgroup
mov ds,bp ;get our data segment back
pushf
call dword ptr dgroup:_old_timer_handler ;call original handler
sti
xor dx,dx
mov ax,ss
mov bx,sp
push dx ;push 0 for last 2 parameters
push dx ;meaning timer interrupt
push ax
push bx
mov dx,_gl_tptr ;push hidden pointer for C++ object
push dx
;stack is now set up for call to task_control object task_switch
cli ;turn off interrupts for task switch
call __task_control_task_switch
;no need to clean up the stack because it will change
sti
mov ss,dx ;new ss returned in dx
mov sp,ax ;new sp returned in ax
pop bp ;restore registers
pop di
pop si
pop ds
pop es
pop dx
pop cx
pop bx
pop ax
iret
_timer_handler endp
;***************************************************************************
; void save_image(unsigned int flag,signal *sig) - send, wait, block
; etc. all call through here to save the "image" of the task. this
; code simulates what will happen with an interrupt by saving the task
; image on the stack. the flag passed is passed on to the task_control
; object task switcher so it knows if it was called by the timer
; interrupt handler, send, wait, block, etc. the second parameter
; is a signal pointer which is used by send and wait and is passed
; through to the task switcher.
;***************************************************************************
_save_image proc near
public _save_image
;since this is a C call we can destroy some registers (ax bx cx dx),
;so now we will set up the stack as if an interrupt call had happened.
;leave parameters on stack, because calling routine will adjust on
;return. bx and cx will have the parameters that were passed.
pop ax ;get return address offset on stack
pop bx ;get first parameter off stack
pop cx ;get second parameter off stack
push cx ;put them back on stack
push bx
pushf ;save flags for iret
mov dx,cs ;get code segment
push dx ;save code segment for return address
push ax ;push saved return address offset
push ax ;save everything
push bx
push cx
push dx
push es
push ds
push si
push di
push bp
sti
mov ax,sp ;stack pointer parameter
push cx ;second parameter passed
push bx ;first parameter passed
mov bx,ss
push bx ;far pointer to stack, parameter passed
push ax
mov ax,_gl_tptr ;push hidden pointer for C++ object
push ax
;stack is now set up for call to task_control object task_switch
cli ;turn off interrupts for task switch
call __task_control_task_switch
;no need to clean up the stack because it will change
sti
mov ss,dx ;new ss returned in dx
mov sp,ax ;new sp returned in ax
pop bp ;restore registers
pop di
pop si
pop ds
pop es
pop dx
pop cx
pop bx
pop ax
iret
_save_image endp
end
[LISTING FOUR]
/********************************************/
/* TASKDEMO.HPP */
/* by Tom Green */
/********************************************/
/* this file is a demonstration of how to use the C++ multi-tasking */
/* kernel. 5 tasks are run and the various means of task switching */
/* and communication are shown */
/* you must have the Zortech C++ compiler version 1.5 and linker and */
/* Microsoft MASM 5.xx to compile this code. */
/* type "ztc taskdemo task timer" and the ztc.com will take */
/* care of compiling, assembling, and linking */
#include <stdio.h>
#include <disp.h>
#include "task.hpp"
void task0(void);
void task1(void);
void task2(void);
void task3(void);
void task4(void);
/* our task_control object (just 1 please) */
task_control tasker;
void main(void)
{
/* task objects */
task t0((func_ptr)task0,1024);
task t1((func_ptr)task1,1024);
task t2((func_ptr)task2,1024);
task t3((func_ptr)task3,1024);
task t4((func_ptr)task4,1024);
/* add task objects to our task_control object ready q */
tasker.add_new_task(&t0);
tasker.add_new_task(&t1);
tasker.add_new_task(&t2);
tasker.add_new_task(&t3);
tasker.add_new_task(&t4);
/* use zortech display package */
disp_open();
disp_move(0,0);
disp_eeop();
/* start tasks up and wait for them to finish */
tasker.start_tasks();
disp_move(0,0);
disp_eeop();
disp_close();
}
static unsigned long counter[]={0L,0L,0L,0L,0L};
static signal sig;
/* task 0 prints the values of the counters for the other 4 tasks. */
/* lock is used to prevent task switching while the screen is being */
/* updated. when the task is finished, block is called to transfer */
/* control to the next task on the ready q */
void task0(void)
{
while(1){
/* disable task switching */
tasker.lock();
disp_move(5,10);
disp_printf("Task 1 %lx",counter[1]);
disp_move(5,50);
disp_printf("Task 2 %lx",counter[2]);
disp_move(15,10);
disp_printf("Task 3 %lx",counter[3]);
disp_move(15,50);
disp_printf("Task 4 %lx",counter[4]);
/* if key pressed then stop the kernel and return */
if(kbhit())
tasker.stop_tasks();
/* re-enable task switching */
tasker.unlock();
/* let next task run */
tasker.block();
}
}
/* tasks 1 and 2 just update counters. these tasks will run until */
/* a timer interrupt occurs, so they get a very large chunk of time */
/* to run, so the counters increase rapidly */
void task1(void)
{
while(1){
counter[1]++;
}
}
void task2(void)
{
while(1){
counter[2]++;
}
}
/* task 3 waits for a signal from task 4 each time the counter is */
/* incremented. when a task waits, it is put on a signal q and the */
/* next task on the ready q is run. this means task 3 and 4 counters */
/* will increment very slowly. in task 4 when a signal is sent, the */
/* task signal q is checked for a task to put on the ready q. the task */
/* sending the signal is then placed on the ready q */
void task3(void)
{
while(1){
counter[3]++;
/* wait for a signal from task 4 */
tasker.wait(&sig);
}
}
void task4(void)
{
while(1){
counter[4]++;
/* send signal to task 3 */
tasker.send(&sig);
}
}