home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Shareware Overload
/
ShartewareOverload.cdr
/
progm
/
ctask.zip
/
CTASK.DOC
next >
Wrap
Text File
|
1988-03-01
|
74KB
|
2,014 lines
CTask Manual Version 0.1 (Beta) 88-03-01 Page 1
CTask
A Multitasking Kernel for C
Version 0.1 (Beta-Release) 88-03-01
Public Domain Software written by
Thomas Wagner
Patschkauer Weg 31
D-1000 Berlin 33
West Germany
BIXmail: twagner
General Notes
=============
What is CTask?
CTask is a set of routines that allow functions in a C program to
run concurrently. This is accomplished by giving each defined
task-function a slice of the processor time, switching tasks on
each system timer tick, or if the task is waiting for an external
event to occur, or is synchronizing with another task. This gives
the illusion of simultaneous execution of the tasks if switching
is fast enough, and no task excessively blocks system resources.
CTask includes the basic kernel, which provides the building
blocks for implementing more complex multitasking systems, plus
additional drivers for DOS access, keyboard and printer buffering,
and serial asynchronous I/O, which allow writing multitasking
applications under DOS with little effort.
What can CTask be used for?
CTask can be used for utilities requiring background processing of
serial communication, printer spooling, disk updates, etc., and
also for multitasking software in stand-alone (i.e. not MS-DOS
based) applications. Very few functions of the CTask kernel are
system specific, so adaptation for other systems is relatively
painless.
What can CTask NOT be used for?
CTask is not intended to provide for multitasking on the command
level of MS-DOS. Although CTask includes a module for channeling
simultaneous DOS requests inside a program, the strategy used in
this module is not sufficient for the functionality required when
switching between programs. Adding this functionality would not be
trivial (although certainly worthwile).
CTask Manual Version 0.1 (Beta) 88-03-01 Page 2
Also, there is no warranty that CTask does perform without errors,
or does exactly what you or I intended. So CTask should not be
used in applications in which malfunction of routines of this
package would result in damage to property or health of any person
without *very* extensive testing under all kinds of loads. In
using CTask, you do so at your own risk.
What is required to use CTask?
To compile CTask, Microsoft C 5.0 or later, or Turbo C 1.0 or
later are required. Microsoft MASM 5.0 or later is required for
the assembler parts. Conversion to other compilers is possible if
they conform to the new ANSI standard. Conversion of the assembler
parts to other Assembler versions requires substitution of the
simplified model directives by explicit segment definitions, and
adding the DGROUP to offsets referencing data.
Converting CTask for stand-alone operation requires few changes.
Mainly, the timer interrupt handler (in "tsktim.asm") has to be
rewritten, and the initialisation code (in "tskmain.c") may have
to be changed. Changes to other modules (naturally except the
optional hardware drivers) should not be necessary.
Another requirement is a good debugger. If you never before wrote
multitasking applications, you're in for some surprises. The
normal debugging tools (Symdeb, Codeview) are of only limited use,
since they use DOS calls for their I/O, and thus may conflict with
your background tasks. One safety measure is to first thoroughly
test your program with preemption disabled, possibly inserting
some schedule() calls, and only allow task preemption if you found
most major bugs. I personally recommend Periscope for debugging,
since it can be made resident and so is always available, and
because it does not use DOS. Periscope III is the most expensive
solution, and the best tool you can imagine (except for Periscope
IV, announced for April), but the less costly versions will also
help a lot.
Do I have to pay for using CTask?
No. One reason for writing CTask was to provide a free, no strings
attached, utility, instead ot the usual "for personal use only"
restriction. Writing a multitasking application for personal use
only dosen't seem too interesting to me. CTask is completely free,
and there is no restriction on its use. You may incorporate all or
parts of CTask in your programs, and redistribute it in source or
binary form by any means. I also do not restrict the use of CTask
in commercial applications. Since trying to distribute the
unmodified CTask for money will only give you a bad name, you may
even do that if you find someone dumb enough to buy it. Naturally,
if you make a bundle from it, or simply like CTask, I would not
reject a donation. However, this is not required, and it will not
give you any additional support.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 3
What support can I expect?
None. Naturally, I will try my best to eliminate any bugs reported
to me, and to incorporate suggested enhancements and changes. If I
fail to do so, however, you're out of luck. Sound familiar?
At the time of this writing, I'm connecting to BIX almost daily.
Problems of limited general interest can be reported via BIXmail,
suggested enhancements and changes, plus bug reports, should be
posted in the c.language/tools conference on BIX (until responses
warrant a new conference, or I can't afford BIX any longer), so
they can be discussed among all interested (I hope there will be a
few). Normal mail can be used, too, but don't expect fast
responses.
Why is this a Beta Release?
Because I expect input from you. The kernel alone may be nice, but
I can imagine several changes to make it more suitable for every-
day use.
For one, the algorithms used are relatively simple and straight-
forward. Singly linked lists are used for chaining tasks (except
for the timer queue), and all list processing (except in the
scheduler) is done in C. The main reason for this is that this
allows the routines to be (in my opinion) very easy to comprehend,
and changes can easily be incorporated.
Also, interrupts are disabled most of the time during event pro-
cessing and queue rearrangement to allow interrupt handlers to use
most of the intertask communication functions.
All this may be completely acceptable, given the quality of the
code both MS C and Turbo C produce, and the usually relatively
small number of tasks simultaneously enqueued in the same queue,
but there may be applications for which the current approach is
insufficient.
So feel free to voice your complaints, suggest enhancements,
supply your own code for inclusion in the package (note that any
code submitted for inclusion must not be copyrighted or usage
restricted, but I'll naturally respect copyrights in code submit-
ted for demo purposes), or suggest additions to this manual.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 4
Multitasking Basics
===================
Tasks
In CTask, a "task" is defined as a (far) C function. The number of
tasks is not limited, and one function may be used for several
tasks. There is little difference between a task function and a
normal function. The usual form of a task function is
void far my_task (farptr arg)
{
one-time initialisation code
while (TRUE)
{
processing code
}
}
A task function is (usually) never called directly. Rather, it is
specified in the call to the create_task routine, and started by
start_task. It will then continue to run, sharing the processor
time with all other tasks, until it is "killed" by kill_task.
Returning from the routine will have the same effect as a kill.
The sharing of processor time is accomplished by "preempting" the
tasks. Preemption means that the task is interrupted in the middle
of some statement by a hardware interrupt (usually the timer), and
is *not* immediately restarted when the interrupt handler returns.
Instead, the next task that is able to run is activated by the
"scheduler", with the interrupted task continuing its duty at some
(normally unpredictable) later time. You can also have multi-
tasking without preemption, and CTask supports this, too, but this
requires full cooperation of all tasks in the system, such that no
task continues to run for an extended period of time without
passing control to other tasks by an explicit scheduling request,
or by waiting for an event.
The optional argument to the task function may be used if one
function is to be used for more than one task, for example to pass
a pointer to a static data area for use by this specific instance
of the function.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 5
Events
Tasks alone would be of limited use. If you have several routines
which just do number crunching or sorting or such, making them
into parallel tasks would be sensible only on a multiprocessor
system. Normally, at least some of your tasks will wait for some
outside "event" to happen, be it the user pressing a key, or a
character arriving from the modem. Then there may be tasks which
have to wait until another task finishes processing on some piece
of data before they can continue. For this synchronization, there
are a number of constructs in CTask, which I summarize under the
name "event". Common to all events are the operations of waiting
for an event to happen, and signalling that the event has
happened. Using a CTask event is much more efficient than looping
while waiting on some shared data location to change state, and
also eliminates concurrency problems inherent in such a simple
approach. Tasks waiting for an event are taken off the scheduler
queue, so they no longer use processor time.
Reentrancy
One of the biggest problem with multitasking in general, and C on
the PC in particular, is reentrancy. Reentrancy means that you can
use a routine, be it you own, or one of the C run-time library,
from different tasks at the same time. When writing your own code,
you can easily avoid problems, but when using the run-time library
routines, you often can only guess if the routines are reentrant
or not.
A routine is NOT reentrant if it modifies static data. This can be
illustrated by the following nonsense example:
int non_reentrant (int val)
{ static int temp;
temp = val;
return temp * 2;
}
Now take two tasks, which call this routine. Task1 calls it with
val=3, Task2 with val=7. What will be the return value for both
tasks? You never know. There are three possible outcomes:
1) The tasks execute sequentially. Task1 will get 6, and Task2
14 as a result. This is what one normally expects.
2) Task1 runs up to "temp = val", then is interrupted by the
timer. Task2 executes, and gets 14 as result. Then Task1
continues. Return for Task1 is 14.
3) Task2 runs up to "temp = val", then is interrupted by the
timer. Task1 executes, and gets 6 as result. Then Task2
continues. Return for Task2 is 6.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 6
add to this the effects of optimization, and a loop, and the
outcome is completly random.
Most routines in the C library will not explicitly do something
like this, but all functions that
- do file I/O (read, write, printf, scanf, etc.) or
- change memory allocation (malloc, free, etc.)
have to use some static data to do buffering, or to store the
chain of memory blocks in. Interrupting such an operation may have
disastrous effects. The most devilish aspect of non-reentrancy is
that the effects are unpredictable. Your program may run 1000
times without any error, and then on the 1001th time crash the
system so completely that only the big red switch will help.
So what can you do about it? A lot. There are several ways to
protect "critical regions" from being entered in parallel. The
most simple method is to disable interrupts. This, however, should
be used only for *very* short periods of time, and not for rou-
tines that might themselves re-enable them (like file-I/O). A
better method is to temporarily disable task preemption. The
routines tsk_dis_preempt and tsk_ena_preempt allow this form of
short-term task switch disable. Interrupts may still be processed,
but tasks will not be preempted. The best way is to use a
"resource". A resource is a special kind of event, which only one
task can possess at a time. Requesting the resource before
entering the routine, and releasing it afterwards, will protect
you from any other task simultaneously entering the critical
region (assuming that this task also requests the resource).
It is also reasonably safe to use file-I/O without protection if
it goes to different files. The C file control blocks for
different files are distinct, so there will be no conflict between
tasks. Since DOS access is automatically protected by CTask,
concurrent file I/O is possible.
What you may NEVER do is to use a non-reentrant routine that is
not protected by an interrupt disable from an interrupt handler.
An interrupt handler is not a task, and so can not safely request
a resource or disable task preemption. This is the reason why the
CTask routines generally disable interrupts before manipulating
the internal queues rather than only disabling task preemption.
Deadlocks
One thing to watch out for when using resources or similar event
mechanisms is not to get into a situation where Task 1 waits for a
resource that Task 2 has requested, while at the same time Task 2
waits for a resource that Task 1 already has. This situation is
called a deadlock (or, more picturesque, deadly embrace), and it
can only be resolved with outside help (e.g. waking up one of the
tasks forcibly). To illustrate, consider the following example:
CTask Manual Version 0.1 (Beta) 88-03-01 Page 7
void far task_1 ()
{
...
request_resource (&rsc1, 0L);
request_resource (&rsc2, 0L);
...
}
void far task_2 ()
{
...
request_resource (&rsc2, 0L);
request_resource (&rsc1, 0L);
...
}
Since interrupts are always enabled on return from a task switch,
even if the statements are enclosed in a critical region, there is
no guarantee that the request_resource calls will be executed
without interruption. In this example, the problem is obvious, but
in a more complex application, where resource requests or other
waits might be buried in some nested routine, you should watch out
for similar situations. One way to avoid problems would be in this
example to change task_2 to
void far task_2 ()
{
int again;
...
do {
request_resource (&rsc2, 0L);
if (again = c_request_resource (&rsc1))
{
release_resource (&rsc2);
delay (2L);
}
} while (again);
...
}
Note that this is only one of many possible approaches, and that
this approach favours task_1 over task_2.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 8
Multitasking and DOS
CTask includes (and automatically installs) a routine which traps
all DOS calls, and makes sure that no two tasks simultaneously
enter DOS. This is accomplished using the resource mechanism, with
special provisions for the limited multitasking capabilities
provided by DOS. There are a few calls, namely those with function
codes <= 0x0c, which allow functions with codes > 0x0c to be
executed while DOS is waiting for an external device (generally
the keyboard) to get ready. This, however, limits the use of some
C library functions, namely scanf and fread, for console input.
Both these functions use handle input, and thus can not be
interrupted. When writing routines for handling user input,
keyboard read functions should either use the low-level calls
getch and gets, or, better yet, the direct entries into the
keyboard handler, t_read_key and t_keyhit, and then process the
string with sscanf if desired. The keyboard handler (contained in
tskkbd.asm) traps all keyboard interrupts, storing entered keys in
a pipe. If a task reads from the keyboard, it is automatically
waiting for this pipe. Using getch and gets is less desirable
since they use polling instead of waiting, and thus degrade system
performance. Also, the t_read_key and t_keyhit functions do not
use DOS, so DOS functions <= 0C can be executed concurrently.
You should NEVER circumvent DOS by calling the BIOS-disk-I/O
function (INT 13) directly. This entry is not protected by CTask,
and using it in parallel to DOS file-I/O may send your hard-disk
FAT and directory into never-never land. If you really should need
this interrupt, you should consider adding support for it in the
DOS-handler module "tskdos.asm". Using the direct sector read and
write interrupts 25 and 26 is supported, however. Using other BIOS
interrupts directly should also be avoided, unless you are
absolutely sure they will not be used by other routines via DOS,
or you provide your own critical region handling. Using interrupt
16, the keyboard interrupt, is safe, since it is redirected to the
keyboard handler pipe.
The DOS access module has been tested to work with DOS 3.30, and
with the DOS 3.30 PRINT routine running in the background. Special
provisions are built into the DOS module to detect background DOS
calls. Using Sidekick also hasn't lead to any problems, although
you may trash Sidekick's screen display by writing to the screen
while Sidekick is active (note that your application *continues*
to run while Sidekick or other pop-ups are active). I can not
guarantee complete compatibility with all background and pop-up
programs under all versions of DOS. Specifically, DOS versions
prior to 3.1 have significant problems with multitasking.
Upgrading to a newer DOS version is recommended if you are still
using DOS < 3.2 (DOS 3.1 has some bugs in other areas).
Critical errors and Control C occurring while concurrent DOS
access takes place may also be fatal. Using the ctrlbrk() and
dosexterr() functions of Turbo C, or their equivalents in MS C, to
trap critical errors and Control C is highly recommended.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 9
Using CTask
===========
CTask comes archived with both source and binary. The binary
version is compiled in the large model, but since the kernel
routines don't use any functions from the C library, you can use
all functions in small or other model programs (except Tiny). The
include files provided specify all model dependencies, so you
don't have to use the large model for your application, but always
remember to include "tsk.h" for the type and routine prototypes.
The C source files will work without changes for both Microsoft
and Turbo C. The library files are not compatible, so use
"ctaskms.lib" for Microsoft, and "ctasktc.lib" for Turbo C.
When compiling your application, turn stack checking off. The
standard stack check is of little use with task stacks, and may
interfere with CTask's operation. The stack area for tasks should
be allocated on the main program's stack, not in the static or
heap data space. The reason for this is that some of the C library
routines check for stack overflow regardless of your compile-time
switches, and will crash your application if you use stacks
outside the normal stack. The stack allocation parameter with LINK
(MS-C), or the _stacksize variable (Turbo C) have to be increased
to reflect the additional stack space. When calculating task stack
sizes, keep in mind that library routines (esp. printf) allocate a
lot of space on the stack for temporary variables. A minimum of 1k
for tasks using library routines is recommended, 2k puts you on
the safe side. Tasks not using C library routines may use a
smaller stack, about 256 bytes at a minimum, plus space for any
local varibles and nested routines. Then add up all task stacks,
and add space for the main task (the function calling
install_tasker), with this size also dependent on what you will do
in the main task while CTask is active. Stacks for tasks that do
not use C library routines may be allocated anywhere.
The keyboard and DOS handlers are always installed with CTask.
Using the serial I/O and printer drivers is optional, so you have
to install them separately, but only *after* installing CTask.
When using the serial driver, include "sio.h" in your modules,
when using the printer driver, include "prt.h".
Remember that tasks are not automatically started after creation.
Use start_task to allow a created task to run.
Always use create_xxx on resources, pipes, etc. Using the event
routines without doing so will have unpredictable results.
Before exiting the program, all installed drivers and CTask must
be uninstalled, or you'll crash the system.
Deleting events before exiting the program is not mandatory, but
recommended to kill all tasks waiting for the event. You should be
careful not to kill tasks while they are active in DOS. The
CTask Manual Version 0.1 (Beta) 88-03-01 Page 10
kill_task routine should be reserved for fatal error handling. The
best way is to let the tasks kill themselves depending on some
global variable or event. If a task is killed while waiting for
input in DOS, DOS operation may be severly impaired. If you use
the C console input routines, make sure that the task returns from
DOS before it is killed, if necessary by requesting the user to
press a key.
Priority Handling
=================
CTask provides for prioritized task execution and event
processing. This means that a task that has a higher priority will
be run before any other tasks having lower priority. Also, a
higher priority task will gain access to resources, counters,
pipes, and mail, before lower priority tasks. With fixed
priorities, this means that a high priority task can monopolize
CPU time, even if it calls schedule() explicitly. Variable
priority increases each eligible task's priority by one on each
scheduler call, so that lower priority tasks will slowly rise to
the head of the queue until they get executed. The priority will
be reset to the initial priority when a task is run.
Since variable priority increases processing time in the critical
region of the scheduler, it is not recommended for systems in
which a larger number of tasks is expected to be eligible
simultaneously.
Usually, all tasks in a system should have the same priority,
with only very few exceptions for non-critical background
processing (low priority) or very time-critical tasks (high
priority). High priority tasks should be written in such a way
that they either reduce their priority when processing is
completed, or that they wait for an event. Busy waiting in a high
priority task will severely impair system operation with variable
priority enabled, and will stop the system until the task is
placed in a waiting state with fixed priority.
The (automatically created) main task is started with the highest
possible priority below the timer task, so that it can process all
initialisations before other tasks start running. To allow the
system to operate, the priority of the main task must be reduced,
or the main task must wait for an event or a timeout.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 11
CTask Data Types
================
Note that you do not have to know the innards of the structures.
All structure fields are filled by the "create_xxx" routines and
modified by the CTask functions. You should NEVER modify a field
in one of the structures directly. The structures are explained
here shortly only for those wanting to modify the routines.
If you only want to use the routines, you should simply include
the file "tsk.h" in your source, and define variables of the types
tcb - for task control blocks
tcbptr - for far pointers to tcbs
flag - for flag events
flagptr - for far pointers to flags
resource - for resource events
resourceptr - for far pointers to resources
counter - for counter events
counterptr - for far pointers to counters
mailbox - for mailbox events
mailboxptr - for far pointers to mailboxes
pipe - for pipe events
pipeptr - for far pointers to pipes
wpipe - for word pipe events
wpipeptr - for far pointers to word pipes
without caring what's behind them.
Additionally, you may use the types
byte - for unsigned characters
word - for unsigned short integers
dword - for unsigned long integers
funcptr - for pointers to task functions
farptr - for far pointers to anything
byteptr - for far pointers to byte arrays
wordptr - for far pointers to word arrays
in defining or typecasting items to be passed as parameters to
CTask functions.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 12
Typedefs used for simplified type specifications
typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;
typedef void (cdecl far *funcptr)();
typedef void far *farptr;
typedef byte far *byteptr;
typedef word far *wordptr;
Error return values for mailbox functions
#define TTIMEOUT ((farptr) -1L)
#define TWAKE ((farptr) -2L)
The task control block structure
The "next" field points to the next tcb in a queue. The "queue"
pointer points to the head of the queue the task is enqueued in,
or will be enqueued in on the next schedule request in the case of
the current running task.
"stack" contains the saved task stack pointer (offset and segment)
if the task is not running. The field "stkbot" contains the bottom
address of the task stack. It is set by create_task, but is
currently not used anywhere else. Stack checking routines might
use this value to test for stack overflow and/or stack usage.
"prior" contains the tasks current priority, with 0xffff the
highest possible priority, and 0 the lowest. "initprior" initially
contains the same value. If variable priority is enabled, "prior"
is incremented on each scheduler call, and reset to "initprior"
when the task is activated.
"state" and "flags" contain the tasks state and flags.
"timerq" is a dlink structure used to chain the tcb into the timer
queue, if the task is waiting for a timeout. In this case, the
F_TIMER flag is set. "timeout" in the timerq structure counts
down the ticks when waiting for a timeout. "follow" and "prev" in
the dlink point to the next and previous dlinks in the tcb's in
the timer queue. Since dlinks point to dlinks, the field "tcbp" in
a dlink points to the beginning of the tcb to which the dlink
belongs to allow access to the tcb when steping through the timer
queue.
The fields "retptr" and "retsize" are used in event handling. They
are used when a task is waiting for an event by the task acti-
vating the event, and also by timeout and wake to indicate error
returns. The use of these pointers eliminates the need to loop
for an event, which requires slightly more code in the event
handling routines, but reduces the need for task switching.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 13
typedef struct dlink_rec far *dlinkptr;
struct dlink_rec {
dlinkptr follow;
dlinkptr prev;
dword timeout;
tcbptr tcbp;
};
typedef struct dlink_rec dlink;
typedef struct tcb_rec far *tcbptr;
typedef tcbptr far *tqueptr;
struct tcb_rec {
tcbptr next;
tqueptr queue;
byteptr stack;
byteptr stkbot;
word prior;
word initprior;
byte state;
byte flags;
dlink timerq;
farptr retptr;
int retsize;
};
typedef struct tcb_rec tcb;
Task states
#define ST_KILLED -1
#define ST_STOPPED 0
#define ST_DELAYED 1
#define ST_WAITING 2
#define ST_ELIGIBLE 3
#define ST_RUNNING 4
CTask Manual Version 0.1 (Beta) 88-03-01 Page 14
Possible task states and queue association
ST_KILLED The task has been killed. Restarting the task is
not possible. Queue pointers are invalid.
ST_STOPPED The task is not enqueued in any queue. To be in-
cluded in scheduling, it has to be explicitly
started. The queue head pointer is NULL.
ST_DELAYED The task is enqueued in the timer queue only. When
the timer expires, it is placed in the eligible
queue. The queue head pointer is NULL.
ST_ELIGIBLE The task is enqueued in the queue of processes
eligible for running. It can not be chained in the
timer queue. The queue head pointer points to the
eligible queue.
ST_RUNNING The task is the current running process. Although
it is not enqueued in any queue, the queue head
pointer in its control block is valid and points to
the queue the process will be enqueued in on the
next schedule request.
ST_WAITING The task is waiting for an event to happen. It can
also be chained into the timer queue if a timeout
was specified in the call. The queue head pointer
points to the queue head in the event control block
for the event the process is waiting on.
Possible state transitions and their reasons
stopped -> eligible by start_task ()
delayed -> killed by kill_task ()
-> eligible by timer task, or wake_task ()
eligible -> killed by kill_task ()
-> running by scheduler
running -> killed by kill_task ()
-> stopped by delay (0)
-> delayed by delay (n != 0)
-> eligible by scheduler
-> waiting by wait_xxx ()
waiting -> killed by kill_task ()
-> eligible by event happening, timeout,
or wake_task()
CTask Manual Version 0.1 (Beta) 88-03-01 Page 15
Task flags
#define F_TIMER 0x80 Task is enqueued in timer queue
#define F_CRIT 0x01 Task may not be preempted
The flag event structure
Contains two queues for processes waiting on a flag state (clear
or set), plus the flag state (0 = clear, 1 = set).
typedef struct {
tcbptr wait_set;
tcbptr wait_clear;
int state;
} flag;
typedef flag far *flagptr;
The counter event structure
Similar to a flag, but contains a doubleword state counter.
typedef struct {
tcbptr wait_set;
tcbptr wait_clear;
dword state;
} counter;
typedef counter far *counterptr;
The resource event structure
Contains a queue for the tasks waiting for access to the resource,
a pointer to the current owner of the resource (to check for
illegal "release_resource" calls), and the resource state (0 = in
use, 1 = free).
typedef struct {
tcbptr waiting;
tcbptr owner;
int state;
} resource;
typedef resource far *resourceptr;
CTask Manual Version 0.1 (Beta) 88-03-01 Page 16
The mailbox event structure
The msgptr type is only used internally to chain mail blocks into
the mailbox. The mailbox type contains a queue of the tasks
waiting for mail, and a first and last pointer for the chain of
mail blocks.
struct msg_header {
struct msg_header far *next;
};
typedef struct msg_header far *msgptr;
typedef struct {
tcbptr waiting;
msgptr mail_first;
msgptr mail_last;
} mailbox;
typedef mailbox far *mailboxptr;
The pipe and word pipe event structure
Contains queues of the tasks waiting to read or write to the pipe,
indices for reading (outptr) and writing (inptr) into the buffer,
the buffer size, the number of bytes or words currently in the
pipe ("filled"), and the pointer to the buffer. A word pipe is
handled exactly the same as a byte pipe, the only difference being
the element size placed in the buffer. With normal pipes, charac-
ters are buffered, with word pipes, words.
typedef struct {
tcbptr wait_read;
tcbptr wait_write;
tcbptr wait_clear;
word bufsize;
word filled;
word inptr;
word outptr;
byteptr contents;
} pipe;
typedef pipe far *pipeptr;
CTask Manual Version 0.1 (Beta) 88-03-01 Page 17
typedef struct {
tcbptr wait_read;
tcbptr wait_write;
tcbptr wait_clear;
word bufsize;
word filled;
word inptr;
word outptr;
wordptr wcontents;
} wpipe;
typedef wpipe far *wpipeptr;
NOTE: When modifying CTask structures, take care to modify the
equivalent definitions in the assembler include file "tsk.mac".
Some of the assembler routines have to use field offsets into
pointers, so having different offsets in C and assembler will
crash the system.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 18
CTask Routines
==============
Installation and Removal
void install_tasker (int varpri, int speedup);
Installs the multitasker. Must be called prior to any other
routine. The calling routine is defined as the main task, and
assigned the highest priority. To allow other tasks to
execute, the main task must have its priority reduced, be
delayed, or wait on an event.
The "varpri" parameter enables variable priority if nonzero.
The "speed" parameter is defined for the IBM PC/XT/AT as the
clock tick speedup factor. The timer tick frequency will be
set to
speed ticks/sec msecs/tick
0 18.2 54.9 (normal clock)
1 36.4 27.5
2 72.8 13.7
3 145.6 6.9
4 291.3 3.4
Note that all timeouts are specified in tick units, so
changing the speed parameter will influence timeouts and
delays. The system clock will not be disturbed by changing
the speed. Using values above 2 can lead to interrupt
overruns on slower machines and is not recommended.
void remove_tasker (void);
Uninstalls the multitasker. Must only be called from the main
task, and MUST be called before exiting the program, or the
system will crash.
Miscellaneous
void preempt_off (void);
Disables task preemption. This is the default after installa-
tion. With preemption turned off, only delays, event waits,
and explicit scheduler calls will cause a task switch.
void preempt_on (void);
Enables task preemption. Task switches will occur on timer
ticks.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 19
void far schedule (void);
Explicit scheduling request. The highest priority eligible
task will be made running.
void far c_schedule (void);
Conditional scheduling request. Scheduling will take place
only if preemption is allowed.
void tsk_dis_preempt (void)
Temporarily disable task preemption. Preemption will be re-
enabled by tsk_ena_preempt or an unconditional scheduler
call.
void tsk_ena_preempt (void)
Re-enable task preemption. Note that tsk_dis_preempt and
tsk_ena_preempt do not change the global preemption state set
by preempt_off and preempt_on.
int tsk_dis_int (void)
Disable interrupts. Returns the state of the interrupt flag
prior to this call (1 if interrupts were enabled).
void tsk_ena_int (int state)
Enables interrupts if "state" is nonzero. Normally used in
conjunction with tsk_dis_int.
The routines tsk_dis_int and tsk_ena_int may be used in a simpli-
fied scheme with the defines
CRITICAL; Declares "int intsav;".
C_ENTER; Expands to "intsav = tsk_dis_int ();"
C_LEAVE; Expands to "tsk_ena_int (intsav);".
void tsk_cli (void)
Disables interrupts.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 20
void tsk_sti (void)
Unconditionally enables interrupts.
void tsk_outp (int port, byte b)
Outputs the value "b" to hardware-port "port".
byte tsk_inp (int port)
Returns the value read from port "port".
The following entry points may be used from assembler routines:
extrn scheduler: far
Direct entry into the scheduler. The stack must be set up as
for an interrupt handler, e.g.
pushf
cli
call scheduler
extrn _sched_int: far
Conditional scheduling call. The stack must be set up as for
an interrupt handler.
Task Operations
void create_task (tcbptr task, funcptr func, byteptr stack,
word stksz, word prior, farptr arg);
Initialises a task. Must be called prior to any other opera-
tions on this task. The task is in the stopped state after
creation. It must be started to be able to run.
"task" is a pointer to a tcb.
"func" is a pointer to a void far function, with a single
dword sized parameter.
"stack" is a pointer to a stack area for the task.
"stksz" is the size of the stack area in bytes.
"prior" is the tasks priority (0 lowest, 0xffff highest).
"arg" is an argument to the created task. It may be used
to differentiate between tasks when using the same
function in different tasks.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 21
void kill_task (tcbptr task);
Kills a task. The task can no longer be used.
int start_task (tcbptr task);
Starts a task, i.e. makes it eligible for running.
Returns 0 if task was started, -1 if the task was not
stopped.
A value of NULL for "task" will start the "main task".
int wake_task (tcbptr task);
Prematurely wakes a delayed or waiting task.
If the task was waiting for an event, it will be removed from
the waiting queue, with the operation terminating with an
error return value.
Returns 0 if task was waked, -1 if the task was not delayed
or waiting.
A value of NULL for "task" will wake the "main task".
void set_priority (tcbptr task, word prior)
Sets the priority of the specified task to "prior".
Note that you should NOT modify the priority field in the tcb
structure directly.
A value of NULL for "task" will set the priority of the "main
task".
void set_task_flags (tcbptr task, byte flags)
Sets the flags of the task to "flags". Currently, the only
flag that can be changed is
F_CRIT the task can not be preempted if set.
Note that you may NOT modify the flag field in the tcb
structure directly.
A value of NULL for "task" will set the flags of the "main
task".
Timer Operations
Timeouts:
When waiting for an event, a timeout may be specified. The
operation will terminate with an error return value if the
timeout is reached before the event occurs.
NOTE that the timeout parameter is not optional. To specify
no timeout, use the value 0.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 22
Delays:
int delay (dword ticks);
Delay the current task for "ticks" clock ticks.
If ticks is 0, the task is stopped.
Event Operations
Resources
A Resource is either in use or free, the default state is free.
If a task has requested a resource, its state is in use.
Tasks requesting a resource while it is in use will be made
waiting. If the resource is released, the highest priority waiting
task is made eligible, and is assigned the resource. Interrupt
handlers may not use resource functions other than
"check_resource".
void create_resource (resourceptr rsc);
This initialises a resource. Must be used prior to any other
operations on a resource.
void delete_resource (resourceptr rsc);
Calling this routine is optional. It will kill all tasks
waiting for the resource.
void release_resource (resourceptr rsc);
Release the resource. If the task does not own the resource,
the call is ignored.
If tasks are waiting, the highest priority task will gain
access to the resource.
int request_resource (resourceptr rsc, dword timeout);
Requests the resource. If it is not available, the task is
suspended. A timeout may be specified.
Returns 0 if resource was allocated, -1 if a timeout
occurred, -2 on wake.
This call is ignored (returns a 0) if the calling task
already owns the resource.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 23
int c_request_resource (resourceptr rsc);
Requests the resource only if it is available.
Returns 0 if resource was allocated, -1 if unavailable.
int check_resource (resourceptr rsc);
Returns 0 if resource is allocated, 1 if free.
Flags
A Flag can be either on or off, the default state is off (0).
Tasks can wait on either state of the flag. If the state is
changed, all tasks waiting for the state are made eligible.
Interrupt handlers may use the "set_flag", "clear_flag", and
"check_flag" functions.
void create_flag (flagptr flg);
This initialises a flag. Must be used prior to any other
operations on a flag. The state is set to 0.
void delete_flag (flagptr flg);
Calling this routine is optional. It will kill all tasks
waiting for the flag.
void set_flag (flagptr flg);
This sets the flag. All tasks waiting for the set state will
be made eligible for running.
void clear_flag (flagptr flg);
This clears the flag. All tasks waiting for the clear state
will be made eligible for running.
int wait_flag_set (flagptr flg, dword timeout);
Waits for the set state of the flag. If the flag is not set,
the task is suspended. A timeout may be specified.
Returns 0 if the flag was set, -1 on timeout, -2 on wake.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 24
int wait_flag_clear (flagptr flg, dword timeout);
Waits for the clear state of the flag. If the flag is not
clear, the task is suspended. A timeout may be specified.
Returns 0 if the flag was cleared, -1 on timeout, -2 on wake.
int check_flag (flagptr flg);
Returns 0 if flag clear, 1 if set.
Counters
A Counter can have any value from 0L to 0xffffffffL, the default
value is 0. Tasks can wait for a counter being zero or non-zero.
If the counter is cleared or decremented to zero, all tasks wai-
ting for the zero condition are made eligible.
If the counter is incremented, the highest priority task waiting
for non-zero is made eligible, and the counter is decremented by
one.
Interrupt handlers may use the "clear_counter", "inc_counter", and
"check_counter" functions.
void create_counter (counterptr cnt);
This initialises a counter. Must be used prior to any other
operations on a flag. The value is set to 0.
void delete_counter (counterptr cnt);
Calling this routine is optional. It will kill all tasks
waiting for the counter.
void clear_counter (counterptr cnt);
Clears the counter to zero. All tasks waiting for the zero
state will be made eligible for running.
int wait_counter_set (counterptr cnt, dword timeout);
Waits for the counter having a nonzero value. If the value is
zero, the task is suspended. The value is decremented when
the task gets access to the counter. A timeout may be speci-
fied.
Returns 0 if the counter was nonzero, -1 on timeout, -2 on
wake.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 25
int wait_counter_clear (counterptr cnt, dword timeout);
Waits for the counter having a zero value. If the value is
nonzero, the task is suspended. A timeout may be specified.
Returns 0 if the counter was zero, -1 on timeout, -2 on wake.
void inc_counter (counterptr cnt);
Increments the counter. If tasks are waiting for the nonzero
state, the highest priority task is given access to the
counter.
dword check_counter (counterptr cnt);
Returns the current value of the counter.
Mailboxes
A Mailbox can hold any number of mail blocks.
Tasks can send mail to a mailbox and wait for mail to arrive. A
mail block is assigned to the highest priority waiting task. Care
must be exercised not to re-use a mail block until it has been
processed by the receiving task. The mail block format is user
defineable, with the first doubleword in a block reserved for the
tasking system. Interrupt handlers may use the "send_mail",
"c_wait_mail", and "check_mailbox" functions.
Note that mailboxes are well suited to provide the mechanism for
managing a chain of (equally sized) free mail blocks. On initiali-
sation, all free blocks would be "sent" to the manager box.
Requesting a free block would use "wait_mail", and freeing a used
block would again "send" it to the manager mailbox.
void create_mailbox (mailboxptr box);
This initialises a mailbox. Must be used prior to any other
operations on a mailbox.
void delete_mailbox (mailboxptr box);
Calling this routine is optional. It will kill all tasks
waiting for mail.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 26
void send_mail (mailboxptr box, farptr msg);
Sends a message to the specified mailbox. If tasks are wai-
ting for mail, the highest priority task will get it.
farptr wait_mail (mailboxptr box, dword timeout);
Waits for mail. If no mail is available, the task is suspen-
ded. A timeout may be specified.
Returns the pointer to the received mail block, TTIMEOUT
(-1) on timeout, TWAKE (-2) on wake.
farptr c_wait_mail (mailboxptr box);
Reads mail only if mail is available.
Returns NULL if there is no mail, else a pointer to the
received message.
int check_mailbox (mailboxptr box);
Returns 0 if mailbox is empty, 1 otherwise.
Pipes
A Pipe has a buffer to hold character or word sized items.
An item may be written to a pipe if there is space in the buffer.
Otherwise, the writing task will be made waiting. A reading task
will be made waiting if the buffer is empty. If an item has been
read, the highest priority task waiting to write to the pipe will
be allowed to write.
Interrupt handlers may only use pipe functions "check_pipe",
"c_write_pipe", and "c_read_pipe".
Note that the values -1 and -2 (0xffff and 0xfffe) should be
avoided when writing to word pipes. These values are used to mark
timeout and wake when reading, or pipe empty when checking a word
pipe, and thus may lead to erroneous operation of your routines.
void create_pipe (pipeptr pip, farptr buf, word bufsize);
void create_wpipe (wpipeptr pip, farptr buf, word bufsize);
This initialises a pipe. Must be used prior to any other
operations on a pipe. "bufsize" specifies the buffer size in
bytes. With word pipes, the buffer size should be divisible
by two.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 27
void delete_pipe (pipeptr pip);
void delete_wpipe (wpipeptr pip);
Calling this routine is optional. It will kill all tasks
waiting to read or write messages.
int read_pipe (pipeptr pip, dword timeout);
word read_wpipe (wpipeptr pip, dword timeout);
Read an item from a pipe. If no item is available, the task
is suspended. A timeout may be specified.
Returns the item, or -1 on timeout, -2 on wake.
int c_read_pipe (pipeptr pip);
word c_read_wpipe (wpipeptr pip);
Reads an item from a pipe only if one is available.
Returns the received item, or -1 if none is available.
int write_pipe (pipeptr pip, byte ch, dword timeout);
int write_wpipe (wpipeptr pip, word ch, dword timeout);
Writes an item to a pipe. If the buffer is full, the task is
suspended. A timeout may be specified.
If tasks are waiting to read, the item will be assigned to
the highest priority waiting task.
Returns 0 on success, or -1 on timeout, -2 on wake.
int c_write_pipe (pipeptr pip, byte ch);
int c_write_wpipe (wpipeptr pip, word ch);
Writes an item to a pipe only if enough space is available.
Returns 0 on success, or -1 if no space available.
int wait_pipe_empty (pipeptr pip, dword timeout);
int wait_wpipe_empty (wpipeptr pip, dword timeout);
Waits for the pipe to be emptied. If the pipe is already
empty on entry, the task continues to run. Returns 0 on
empty, -1 on timeout, -2 on wake.
int check_pipe (pipeptr pip);
word check_wpipe (pipeptr pip);
Returns -1 if the pipe is empty, else the first item in the
pipe. The item is not removed from the pipe.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 28
word pipe_free (pipeptr pip);
word wpipe_free (wpipeptr pip);
Returns the number of free items available in the pipe.
Buffers
A Buffer has a buffer to hold message strings.
A message may be written to a buffer if it fits into the buffer.
Otherwise, the writing task will be made waiting. A reading task
will be made waiting if the buffer is empty. If a message has
been read, the highest priority task waiting to write to the
buffer will be allowed to write if its message fits into the
available space.
Interrupt handlers may not use buffer functions other than
"check_buffer".
The buffer routines are implemented using resources and pipes, and
thus are not part of the true "kernel" routines.
void create_buffer (bufferptr buf, farptr pbuf, word bufsize);
This initialises a buffer. Must be used prior to any other
operations on a buffer.
The minimum buffer size is the length of the longest expected
message plus two.
void delete_buffer (bufferptr buf);
Calling this routine is optional. It will kill all tasks
waiting to read or write messages.
int read_buffer (bufferptr buf, farptr msg, int size,
dword timeout);
Read a message from a buffer. If no message is available, the
task is suspended. A timeout may be specified.
The message will be copied to the buffer "buf", with a
maximum length of "size" bytes.
Returns the length of the received message, -1 on timeout, -2
on wake.
int c_read_buffer (bufferptr buf, farptr msg, int size);
Reads a message from a buffer only if one is available.
Returns the length of the received message, or -1 if none is
available.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 29
int write_buffer (bufferptr buf, farptr msg, int size,
dword timeout);
Writes a message from "msg" with length "size" to a buffer.
If not enough space for the message is available, the task is
suspended. A timeout may be specified.
If tasks are waiting for messages, the highest priority task
will get it.
Returns the length of the message, -1 on timeout, -2 on wake,
-3 on error (length < 0 or length > buffer buffer size).
int c_write_buffer (bufferptr buf, farptr msg, int size);
Writes a message to a buffer only if enough space is
available.
Returns the length of the message, -1 if no space available,
or -3 on error (length < 0 or length > buffer buffer size).
word check_buffer (bufferptr buf);
Returns the current number of bytes (not messages) in the
buffer, including the length words.
The Keyboard Handler
====================
word t_read_key (void)
Waits for a key to be entered. Returns the ASCII-code in the
lower byte, and the scan-code in the upper byte, or -2
(0xfffe) on wake.
word t_wait_key (dword timeout)
Waits for a key to be entered. Returns the ASCII-code in the
lower byte, and the scan-code in the upper byte, or -1
(0xffff) on timeout, -2 (0xfffe) on wake.
word t_keyhit (void)
Returns -1 (0xffff) if no key was entered, else the value of
the key. The key is not removed.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 30
The Serial I/O handler
======================
The serial I/O handler provides full duplex interrupt driven I/O
on the serial ports. Support for COM1 and COM2 is included, adding
other ports is possible by changing the source.
int v24_install (int port, farptr rcvbuf, word rcvsize,
farptr xmitbuf, word xmitsize);
Installs the handler for the specified port. Currently, ports
0 (COM1) and 1 (COM2) are supported. Returns -1 if an invalid
port is specified, else 0. Both ports may be used simultaneo-
usly, the buffer areas for the ports must not be shared.
"rcvbuf" is a word pipe buffer area for received characters,
with "rcvsize" specifying the buffer size in bytes. Note that
the buffer will hold rcvsize / 2 received characters.
"xmitbuf" is a byte pipe buffer for characters waiting for
transmissions, "xmitsize" gives its size in bytes.
void v24_remove (int port);
Must be called for all ports installed before exiting the
program.
void v24_change_rts (int port, int on);
Changes the state of the RTS output line. A nonzero value for
"on" will turn the output on.
void v24_change_dtr (int port, int on);
Changes the state of the DTR output line. A nonzero value for
"on" will turn the output on.
extern void far v24_change_baud (int port, long rate);
Changes the baud rate. The following baud rates are suppor-
ted:
50, 75, 110, 134, 150, 300, 600, 1200, 1800, 2000,
2400, 3600, 4800, 7200, 9600, 19200L, 38400L.
Note that baud rates above 9600 may cause problems on slow
machines.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 31
extern void far v24_change_parity (int port, int par);
Changes parity. The parameter must be one of the following
values defined in "sio.h":
PAR_NONE no parity checks
PAR_EVEN even parity
PAR_ODD odd parity
PAR_MARK mark parity
PAR_SPACE space parity
extern void far v24_change_wordlength (int port, int len);
Changes word length. Values 5, 6, 7, 8 may be given.
extern void far v24_change_stopbits (int port, int n);
Changes Stopbits. Values 1 and 2 are allowed.
extern void far v24_watch_modem (int port, byte flags);
The modem status input lines specified in the "flags"
parameter must be active to allow transmission. Transmission
will stop if one of the lines goes inactive. The parameter
may be combined from the following values defined in "sio.h":
CTS to watch clear to send
DSR to watch data set ready
RI to watch ring indicator
CD to watch carrier detect
A value of zero (the default) will allow transmission
regardless of modem status.
extern void far v24_protocol (int port, int prot,
word offthresh, word onthresh);
Sets the handshake protocol to use. The "prot" parameter may
be combined from the following values:
XONXOFF to enable XON/XOFF (DC1/DC3) handshake
RTSCTS to enable RTS/CTS handshake
The "offthresh" value specifies the minimum number of free
items in the receive buffer. If this threshold is reached, an
XOFF is transmitted and/or the RTS line is inactivated.
The "onthresh" value specifies the minimum number of items
that must be free before XON is transmitted and/or the RTS
line is re-activated.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 32
Enabling XONXOFF will remove all XON and XOFF characters from
the input stream. Transmission will be disabled when XOFF is
received, and re-enabled when XON is received.
Enabling RTSCTS will stop transmission when the CTS modem
input line is inactive.
int v24_send (int port, byte ch, dword timeout);
Transmits the character "ch". A timeout may be specified.
Returns -1 on timeout, -2 on wake, else 0.
int v24_receive (int port, dword timeout);
Waits for a received character. A timeout may be
specified. Returns -1 on timeout, -2 on wake, else the
character in the lower byte, plus an error code in the
upper byte. The error code is the combination of
0x02 overrun error
0x04 parity error
0x08 framing error
0x10 break interrupt.
int v24_check (int port);
Returns -1 if no receive character is available, else the
next character from the pipe. The character is not
removed.
int v24_overrun (int port);
Checks for receive pipe overrun. Returns 1 if an overrun
occurred, 0 otherwise. Clears the overrun flag.
int v24_modem_status (int port);
Returns the current modem status word. The CTS, DSR, RI,
and CD defines may be used to extract the modem input
line status.
int v24_complete (int port);
Returns 1 if all characters in the transmit pipe have been
sent, else 0.
CTask Manual Version 0.1 (Beta) 88-03-01 Page 33
int v24_wait_complete (int port, dword timeout);
Waits for the transmit pipe to be empty. Returns -1 on
timeout, -2 on wake, else 0.
The Printer Output Driver
=========================
The printer output driver provides for buffered output to up to
three printer ports (more can be added by editing the source).
Interrupt or polling may be selected. Due to the usual hardware
implementation of printers and print buffers, and the level
triggered interrupt structure of the IBM XT/AT, using polling is
recommended. When using interrupts, you should not simultaneously
install both port 0 and port 1 with interrupt enabled, since both
ports share the same interrupt line.
int prt_install (int port, byte polling, word prior,
farptr xmitbuf, word xmitsize);
Installs the printer driver for the specified port. Ports 0
(LPT1), 1 (LPT2), and 2 (LPT3) are supported.
The "polling" parameter specifies polling output when
nonzero, interrupt output when zero.
The "prior" parameter sets the priority of the printer output
task.
"xmitbuf" is a buffer area for the printer output buffer,
"xmitsize" specifies its size in bytes.
void prt_remove (int port);
Removes the printer driver for the specified port. All
installed ports must be removed before terminating the
program, or the system may crash when interrupt processing
was selected.
void prt_change_control (int port, byte control);
Changes the printer control output lines of the port. The
value for "control" may be combined from the following values
defined in "prt.h":
AUTOFEED will enable printer auto feed if set
INIT will initialise (prime) the printer if clear
SELECT will select the printer if set
CTask Manual Version 0.1 (Beta) 88-03-01 Page 34
int prt_write (int port, byte ch, dword timeout);
Write a byte to the printer. A timeout may be given. Returns
0 on success, -1 on timeout, -2 on wake.
int prt_status (int port);
Returns the current printer status lines, combined from the
values
BUSY Printer is busy when 0
ACK Acknowledge (pulsed 0)
PEND Paper End detected when 0
SELIN Printer is selected (on line) when 1
ERROR Printer error when 0
int prt_complete (int port);
Returns 1 if the printer buffer has been completely transmit-
ted, 0 otherwise.
int prt_wait_complete (int port, dword timeout);
Waits for printer output to complete. Returns 0 on success,
else -1 on timeout, -2 on wake.