Dec 212017
A TP5 Multitasking TPU. Very Impressive, Shareware from W. Germany. | |||
---|---|---|---|
File Name | File Size | Zip Size | Zip Type |
CALLDOS.EXE | 18640 | 10410 | deflated |
CPMISC.TPU | 1600 | 859 | deflated |
CPMULTI.DOC | 72511 | 21399 | deflated |
CPMULTI.TPU | 16192 | 6938 | deflated |
MTEST.PAS | 2895 | 902 | deflated |
MTSHARE.TPU | 5328 | 2846 | deflated |
PRO_CON.PAS | 14635 | 3976 | deflated |
QUETEST.PAS | 2299 | 811 | deflated |
QUEUE.PAS | 11028 | 3041 | deflated |
TASKWIN.PAS | 2794 | 948 | deflated |
Download File MULTI.ZIP Here
Contents of the CPMULTI.DOC file
Dear Programmer,
thanks for using my Multi-Tasking Subsystem for Turbo Pascal 5.0.
This product was designed to enable everyone to study programming in a
multi-tasking environment, without having to spend a lot of money to
purchase an operating system like OS/2 or XENIX (and of course the
extension memory necessary to run it).
The Turbo Pascal Multi-Tasking Subsystem is a very inexpensive and
yet powerful tool that adds multi-tasking capabilities to your
Turbo Pascal 5.0 programs.
You will be able to study the programming of parallel processes and
problems of interprocess communication using the programming
environment you are accustomed to.
Although the Multi-Tasking Subsystem initially was designed as a means
of experimenting, it became very soon a powerful and easy to use
product, which I hope you will like as much as I do.
At first, I would like to outline the capabilities of the basic system:
- up to 50 (increased on demand) independently executing
tasks in a single program
- 3 priority levels
- code sharing
- preemptive scheduler, using a dynamic scheduling algorithm
- you may use DOS-functions in your tasks safely. A task is never
interrupted within a DOS-function or a critical interrupt
- size of timeslice and range of dynamic CPU-allocation programmable
- message passing
- semaphores
- programmable timers
- primitive event processing (up to 10 simultaneously active event
requests; increased on demand)
- executes on any PC, XT, AT, PS/2 and full compatible running DOS 2.x
or 3.x
- source code available to registered users
Registered users will get an additional UNIT for FREE(!!) which is
based upon the Multi-Tasking Subsystem and provides you with:
- extended keycodes (Chacter, Scan-Code, Shift-Statusword)
- manipulation of the keyboard buffer (clear, add keycodes at the
end or in front of the actual buffer contents)
- keyboard lock and unlock
- execute a DOS-program as subtask
- all you need to have tasks wait for a special hot-key that awakes
them
- all you need to have tasks pop up over the DOS-program and suspend
the DOS-task while they are executing (another way of writing memory
resident programs)
Well, no product is perfect! If you would like to have a function the
Multi-Tasking Subsystem does not include at present, contact me and
perhaps you will find it in the next version. IF I decide to
realize YOUR idea in a later version of the subsystem, you will
receive a FREE-upgrade! I hope you will understand that I reserve
the right to decide which functions will be added.
In this connection, I would like to thank L. David Baldwin and
TurboPower Software for their fabulous source-level debugger TDebug
PLUS 4.01, that helped me very much during the development of my
Multi-Tasking Subsystem.
Let me stop here! Please take the time to read carefully through the
documentation before you "dive" into the world of multi-tasking.
Best regards,
Christian Philipps
Turbo 5.0 Multi-Tasking Subsystem
User's Manual
(c) Copyright 1988 by
Christian Philipps, Moers
all rights reserved
Version 1.30 / November 1988
Author: Christian Philipps
Dsseldorfer Str. 316
4130 Moers 1
West-Germany
Trademarks Mentioned
TurboPower Software is trademark of TurboPower Software, Scotts Valley
Turbo Professional is trademark of Sunny Hill Software, used under
license to TurboPower Software
Turbo Pascal and TASM are registered trademarks of Borland International
MS-DOS, OS/2 and MASM are trademarks of Microsoft Corporation
A86 is trademark of Eric Isaacson Software, Bloomington
UNIX is trademark of AT & T
IBM PC, XT, AT, PS/2 and PC-DOS are trademarks of International
Business Machines Corporation
License Agreement
The Turbo Pascal Multi-Tasking Subsystem is marketed through the
ShareWare marketing concept.
Therefore I grant you the right to reproduce, distribute and use
copies of this ShareWare version of our Multi-Tasking Subsystem
(including the on disk documentation), on the express condition that
you do not receive any payment, commercial benefit, other
consideration for such reproduction or distribution, or change this
license agreement or the copyright notice.
The Multi-Tasking Subsystem, the additional unit MTPOPUP and the
documentation of both are copyright (c) 1988 by Christian Philipps,
Dsseldorfer Str. 316, 4130 Moers 1, West-Germany.
You have the right to evaluate this ShareWare version for a period of
30 days to find out whether it suits your needs. If you decide to
continue using this software, I expect that you become a registered
user by sending DM 50 or $35 (object code license) / DM 100 or $70
(object code + source code license) to
Christian Philipps
Dsseldorfer Str. 316
4130 Moers 1
West-Germany
As soon as I receive your registration, I will send you your
personal copy of the subsystem plus a FREE unit based upon the basic
subsystem (see introductory letter).
Furthermore you will get technical support through mail, e-mail or
phone during 1 year from the day of receipt.
Users purchasing an oject code license only may receive the source
code of the latest version later at any point of time by sending
another DM 50 or $35 to the adress above.
Disclaimer
I make no warranty of any kind, either express or implied, including
but not limited to implied warranties of merchantability and fitness
for a particular purpose, with respect to this software and the
accompanying documentation.
In no case, I will be liable for any damages (including damages for
loss of business profits, business interruption, loss of business
information, or other pecuniary loss) arising out of the use of or
inability to use this software, even if I have been advised of the
possibility of such damages.
Getting Started
Throughout this documentation I assume that you are familiar with
Turbo Pascal 5.0 in general and with the UNIT concept.
I also assume that you know how to use the advanced features of Turbo
Pascal 5.0, for example type-casting of pointers. If necessary,
please refer to your Turbo Pascal documentation for more information
on any of the compiler's internal functions or the functions contained
in the units Crt, Dos,...
The Multi-Tasking Subsystem mainly consists of four units:
a) CpMulti.TPU
This is the basic Multi-Tasking Subsystem. All further units are
based upon this piece of software.
b) MTPOPUP.TPU (registered users only)
MtPopUp is a unit, based upon the basic subsystem and providing
full control over the system keyboard and supports one DOS-program
to be run as a separate task. Tasks may be kept waiting for a
certain key-combination to be pressed and somewhat resident
programs can be written by executing another COMMAND.COM in the
foreground. Please note that this unit is available only to
registered users, who will receive it in return for their
registration.
c) QUEUE.TPU (Public-Domaine)
This unit contains several functions, which help you to create,
delete and manage forward-chained list-structures. The structure
of a single list element is defined by your application and may be
any data structure. Passing data between separate tasks, you will
shortly find out that list structures, as provided by this unit,
are convenient for implementing pipe-like data structures.
Studying the routines contained in the accompanying source code for
QUEUE, you will learn how to assure the consistency of shared data
at any time during execution.
d) CPMISC.TPU
Miscellaneous service routines, used by QUEUE. The source code of
this Unit is only partially made available.
Let's take a look at the way to USE the Multi-Tasking Subsystem: The
usage of this software is quite straightforward, provided you enable
your compiler to find the necessary units (please refer to your
compiler manual).
You will incorporate the Multi-Tasking library in your programs by
simply including CpMulti in the USES-statement. CpMulti must be the
first unit of the subsystem preceded, however, by the basic units Crt
(or alternatives) and Dos.
Example: USES Crt, Dos, CpMulti, MtPopUp, Queue;
The units CpMulti and MTPOPUP use Turbo's CRT-unit, so problems may
arise, when using an alternative CRT-unit (such as the TurboProfes-
sional unit TPCRT) in your program.
For the special case that you own the fantastic TurboProfessional
package of TurboPower Software, your diskette contains files with
the extension .PRO, which I compiled using the TPCRT-unit. You only
need to rename those files to the extension .TPU.
If you wish to use another alternative Crt-unit that conflicts with
the standard CRT-unit, you will have to order the source code and
recompile the subsystem.
After having set up your USES-statement properly, you will have to
include the compiler switch S-, which prevents the compiler
from generating object code that checks the stack boundaries. This
is VERY important, because each task has its own private stack
that is automatically reserved on the heap at the time of creation.
Without the S- directive, you would get a stack overflow abort
shortly after creating the first subtask.
Whenever a task-switch is performed, the Multi-Tasking Subsystem
checks the private stack of a subtask A task which uses up its
stackspace (minus a certain safety margin) is aborted. The subsystem
issues an error message and marks the task as "Crashed". The memory
occupied by its private stack is not freed and may be examined using a
debugger.
Now let's have a look at the system startup. When your program comes
to execution the initialisition code in CpMulti captures some
interrupt vectors and sets up the task-table. It creates a
null-process with lowest priority (a simple loop) that consumes
CPU-time whenever no other process is currently computable. Finally
the main program is started as task no. 2.
The shutdown again is fully automatic. When your program terminates
or a runtime error brings you to a halt, CpMulti's exit procedure
gains control, disables multi-tasking and restores the interrupt
vectors previously captured.
Only in rare occasions, your system will completely crash. This
could happen, when one of your tasks runs mad and overwrites
valuable system data.
You might start experimenting right now, BUT I recommend studying
the rest of this documentation before. The following sections will
give you a deeper understanding of the concept and the interior of the
Multi-Tasking Subsystem.
Multitasking is too complicated however, to go down to the last.
Those of you, who would like do get a deeper insight into the design
of multi-tasking operating systems should read the book "Operating
Systems, Design and Implementation" by Andrew S. Tanenbaum, Prentice
Hall or scan through the bookshelves of Prentice Hall or Adison
Wesley. The book mentioned above helped me very much in designing the
Multi-Tasking Subsystem and describes a UNIX-like operating system for
personal computers in detail (MINIX + source code available from
Prentice Hall).
The Basic System (CpMulti)
1. Introduction
At the moment, more and more programmers become interested in multi-
tasking environments for personal computers. I suppose, anyone who
intends to stay up to date, will have to deal with systems like OS/2
or UNIX / XENIX /...
Unfortunately, those systems are quite expensive and require lots of
extension memory. Only few wealthy private programmers (have you
ever seen one?) will be able to buy such an operating system already
today.
At the time I started developing the Multi-Tasking Subsystem, my
situation had been as described above. Now I'm in the position to
provide you with an inexpensive means to study the design and
synchronisation of parallel processes. In addition to that, the
Multi-Tasking Subsystem will help you to write programs more complex
and much more sophisticated, especially in areas like data
communication, where quite a lot of actions are to be taken nearly
simultaneously.
Concept
Like UNIX or OS/2, the Multi-Tasking Subsystem was designed as a
time-sharing system. It was not designed for real-time applications.
A time-sharing system tries to divide the available computing power
among all computable processes evenly. Therefore, a process that has
used up its timeslice, is temporarily suspended and another
computable process gains control (preemptive scheduling). This is
accomplished by having a timer-interrupt-service-routine monitor the
system activities about 18 times a second. This routine initiates a
task-switch whenever the running task has used up its quantum.
Although I could have used the AT-RTC (Real-Time Clock), which in fact
would decrease the minimum possible timeslice, I have decided not to
do so for several reasons:
a) The task-switch is the most time consuming action, the kernel has
to perform. A smaller timeslice, increases the number of possible
task-switches, but increases the kernel overhead as well.
b) The Multi-Tasking Subsystem could not run on PC or XT-computers,
because those do not have a real-time clock.
With a timer-interrupt occuring 18 times a second, the smallest
possible timeslice is about 55,5 milliseconds. During the
initialisation sequence the timslice is set to 110 milliseconds.
If you choose a 55,5 millisecond timeslice the amount of computing
power consumed by the mere task-switching process will be about 1,4
percent (12 MHz AT-compatible).
Dynamic Scheduling
Normally each task is temporarily suspended whenever it has used up
its timeslice. If this happes inside a DOS-function, however, the
kernel may not safely interrupt the currently executing task. It has
to wait, until it returns from the DOS-call.
Provided this task has to read thousands of records from a data file
and write them to another file, the kernel will be inside DOS
again, when the kernel tries to suspend it at the following
timer-tick. Again, a task switch is impossible! This procedure might
happen again and again until the task has finished.
As you see, under certain circumstances, a task may exceed its
quantum at an amount that cannot be foreseen by the system-kernel.
On the other hand, there might be a task that periodically, lets say
one time a second, look at a particular value taken from a port to
see, whether actions are to be taken. In this case, it will act upon
the condition encountered, otherwise it will go to sleep and give up
its timeslice. Such a task will use up its timeslice very seldom.
If, hovewer, actions are to be taken, it might be suspended in the
middle of its activities because it has used up its quantum. - Indeed
that's not fair!
What can we do to achieve some kind of compensation? OS/2 brought me
to the idea to implement a somewhat dynamic scheduling that proceeds
as follows: Whenever a task gives up its timeslice, the time not used
is placed to this task's credit. If it has to execute longer at a
later point of time, it will not be preempted until it has used up its
current quantum plus its credit. On the other hand, a task that
exceeds its quantum, is not rescheduled until its debit is "used up".
This sounds quite good, but there is a catch to it though. If we
would credit every single tick not used, the task mentioned above
could possibly run for hours if it has to take some action after
all.
For this reason, the amount of ticks beeing placed to a tasks
credit/debit, is limited. You may choose this limit freely and it
defaults to +/- 10 timer-ticks that is slightly above half of a
second.
Priority
The Multi-Tasking Subsystem supports three levels of priority:
"Pri_Nice" which is the lowest, "Pri_User" which is standard and
"Pri_Kernel" which is highest possible priority. Your Pascal
main-program is started at the level "Pri_User" during initialization.
Let's examine, how the different layers of priority are acted upon:
Inside each layer, the tasks are scheduled in straight round-robin
fashion. Whenever there are computable tasks at a higher level none
of the tasks in a level below that will gain control. That means, the
"idle loop" at level "Pri_Nice" will only be activated, if the
layers "Pri_User" and "Pri_Kernel" are empty.
The programmer has to take this concept into account when designing
his application system. The overall system performance depends very
much on the assignment of priorities. Any task can change its
priority at runtime by means of a special system service.
By the way: Throughout this documentation I use the terms "task" and
"process" interchangably, although some experts will be picking up
stones to throw at me at this very moment... I don't feel guilty
doing so, because those experts seem not to have reached an agreement
yet, about how to define these terms.
Tasks that spend most of their lives waiting for events (waiting for
incoming characters,...), should be given highest priority.
Tasks that spend their lives busily working all the time, should be
given standard pritority.
By doing so, the busy ones will be interrupted whenever an event, a
higher priorized task was waiting for occurs and will be able to work
the rest of the time (which will be most of the time) undisturbed.
The lowest priority level will mostly be occupied only by the idle
loop.
Another Catch To It
DOS does not know about interrupt controlled disk I/O in the
fashion of multi-tasking operating systems! There is a loop inside
your ROM that waits for the disk-controller to issue a completion
interrupt, but that is of no use for us, because a disk I/O
operation must never be interrupted by a task-switch.
Multi-tasking operating systems normally switch to another task,
while the currently running process has to wait for a disk operation
to complete. I didn't know how to achieve this without rewriting
the low-level disk I/O-routines. So for now, we'll wait...
As a consequence, a process that performs lots of disk I/O-operations
and which is running at high priority, will possibly block the whole
system.
In a real multi-tasking operating system however, it is very useful
running I/O-intensive processes at a high priority level for the
reasons stated earlier in the context of mostly waiting tasks.
Inter Process Communication (IPC)
The Multi-Tasking Subsystem contains functions for handling
semaphores and message-passing.
Shared-memory does not require system support, because every task
under DOS has access to any memory location in the whole system.
The programmer will have to use semaphores, however, to avoid race
conditions when accessing commonly used memory areas. The
demonstration program PRO_CON gives examples of how to synchronize
your tasks using semaphores.
Programmable Timers
Programmable timers are a very useful means to take care of timeout-
conditions (for example in communications software).
The Multi-Tasking Subsystem let you define any number (limited by
the available heap) of timers with a resolution of 55,5
milliseconds. If a timer expires, a byte located at the address given
at the time of creation is incremented.
The amount of CPU-time consumed by watching the timers currently
active is negligible and independent of the number of timers defined.
Event Handling
Sometimes it can be useful to have a task waiting for the contents of
a memory location to change. This is what I have chosen to call an
event. Realizing a task that waits for a buffer filled by an
interrupt routine, you can acchieve a very short response time at a
minimum waste of CPU-time by defining an event that reactivates your
task whenever the contents of the buffer's tail-pointer changes.
Starting with release 1.30, the subsystem kernel puts a task,
awakened by an event occuring, at the head of the task-queue
corresponding to its run level. Thus it is put in the position of
beeing able to react on the event very quickly.
3. How Is It Done
Most of the subsystem is written in Turbo Pascal 5.0. Only a small
part of the kernel (about 2 KB) is written in assembly language. If
you purchased a source code license, you will find two versions of
the assembly language module on your product diskette. The first one
assembles using Microsofts Macro Assembler V4.0 or higher or TASM 1.0,
the second one is for programmers using the well known ShareWare
assembler A86 V3.19 or higher, written by Eric Isaacson. Both parts
together form a TPU (Turbo Pascal Unit) that contains the whole
subsystem and may be USEd in your programs.
The Task Table
The central element of the Multi-Tasking Subsystem is called "task
table". For reasons of performance, this table was defined
statically in the data segment. It can hold a maximum of 50 tasks at
the moment, but can easily be expanded (if you own the source code).
Every task in the system occupies one entry in the task table. This
entry holds all pieces of information that describe the task's actual
state of execution.
At any point of time each task is in one of the following possible
states:
Running..: This task is currently executed.
Ready....: This task is computable, but another task currently is in
control of the CPU.
Sleeping.: This task has suspended itself for a certain number of
timer-ticks.
Crashed..: The subsystem has detected a stack overflow for this task.
Waiting..: This task is currently waiting for an event or a
resource (a semaphore for example).
Sending..: This task is waiting for the receiver of a message to
do a receive system call.
Receiving: This task is waiting for another task to send it a message.
Suspended: This task was temporarily disabled by another task in the
system and needs action from another task to be put into
the ready-state again.
If a slot in the task-table currently is not occupied by any task its
state-marker will contain the value "Available".
The following state diagram summarizes all possible task-states and
the state-transitions possible among these states. A state-transition
may only take place in the direction indicated by the arrow symbols.
13
> Running > Crashed
2 1
4
Ready < Waiting <3
6 Sleeping <5
7
8 Sending <
10 Receiving <9
12 Suspended <11
State Diagram, Multi-Tasking Subsystem Version 1.10
Task Management
Internally, the tasks are managed using queues. Dependent on its
state of execution, a task may be located in
a) the scheduler queue of its priority-level ("Running" or "Ready"),
b) the sleep-queue ("Sleeping") or
c) the sender-queue of the receiver-task, if the latter one is not
ready to receive the message and the sender indicates that it
wants to wait until the receiver gets ready ("Sending").
Tasks at the states "Suspended", "Receiving" or "Crashed" are
not located in any queue.
A task that is "Waiting" on an event or a resource may be chained to
the event-table or be located in a semaphore's task-queue.
Whenever a task is created, a task-table entry is allocated to
this task. The slot number of its task-table entry becomes the
task-id (task-number or process-number), which some system-calls
are to be passed as a parameter.
In addition to that, a private stack for the task is allocated and
set up.
The task creation can be completed sucessfully only if a) there is
an empty slot in the task-table and b) the private stack can be
allocated on the heap.
Code-Sharing
The Turbo Pascal compiler generates object code that is mostly
reentrant. Such code may be executed in code-sharing.
The term "Code-sharing" can have different meanings. On the one
hand there may be a number of subroutines which are used by more
than one task in the system. Thus more than one task may be
executing a certain subroutine at the "same" point of time, i. e.
the instruction pointers of those tasks point to a location inside
this subroutine at the same point of time.
Provided the code is reentrant, this will never lead to any
problems. You will have to take care of situations which lead to
more than one task modify global data at the same point of time, of
course, but the type of code-sharing described above will never lead to
a crash or the modification of local variables.
"Code-sharing" may also describe a situation, in which the same part
of code (here: the object code of a procedure - the task) is activated
more than once as a separate process. Each of these tasks is given
its own set of registers, its private stack and its local variables.
The latter are actually allocated on the private stack. Provided the
code is reentrant and provided that you, the programmer, take care of
race conditions, there may be an "unlimited" number of separate tasks
executing the same object code simultaneously.
I have already tried both kinds of code-sharing and did not encounter
any problems so far.
Programmable Timers
Lets now have a closer look at the timers. A timer, which in fact is
a memory location containing a counter beeing decremented at any
timer-tick, needs to be examined whenever a clock-interrupt occurs.
If the kernel realizes that the timer has expired, some action will
be to be taken.
As you see, a small extra amount of computing power is necessary to
countdown timers, act upon their expiration and finally remove them
from the system. Some types of application, communication programs
for example require extensive use of timers. Therefore one should
think about an inexpensive implementation of timers to avoid loss of
performance.
The implementation of timers in the Multi-Tasking Subsystem makes the
kernel overhead constant and independant from the number of timers
presently active. This is achieved by a special queueing method,
taken from the timer implementation of MINIX, a UNIX-like operating
system for the IBM PC. As stated earlier, MINIX has been developed by
Andrew S. Tanenbaum and is currently distributed by Prentice Hall. I
recommend MINIX for everybody, who would like to go deeper into
operating system design, it's fantastic!
Return to the timers: The timer-queue consists of a forward-chained
list of timer-structures. Only the first element of this list is
examined at every timer-tick. If the first timer expires, it will be
removed from the queue after having taken the appropriate action to
signal its expiration to the application. The second element now
becomes the head of the queue, and so on.
When a new timer is inserted into the timer-queue, its counter will be
adjusted to reflect the number of ticks remaining after all previous
elements of the timer-queue have expired.
Example:
Timer-Queue > 3 > 5 > 1
Time to wait: 3 Ticks 8 Ticks 9 Ticks
Event Handling
You might think of lots of events that could be handled by an
application.
Currently only the change of contents with respect to a single
memory location is supported. Practically speaking, your application
may wait (without consuming CPU) until the contents of a particular
memory location changes. This memory location is checked whenever a
task-switch occurs.
What to do with it? Well, I came up with this type of event as I
designed a special application. I had to realize a task that emptied
a ring-buffer which was filled by an interrupt routine. Why not use a
semaphore, you might ask! - Too slow! There was just enough time to
insert the characters received into the ring-buffer. On the other
hand the ring-buffer had to be emptied as quickly as possible, again
for the reasons of speed.
A high-priorized subtask permanently looping, keeping an eagle eye on
the ring-buffer is far from beeing a solution - it simply blocks the
whole system. OK, lets try to have the subtask sleep for a while,
take a look at the buffer and sleep again if no characters have
arrived. Well, in the meantime a buffer-overflow has occured...
I suppose, you now understand why the event has been born. Defining
an event that monitored the buffer's tail-pointer, I kept my
high-priority subtask waiting for the buffer to be filled without
blocking the whole system. Awakend very shortly after a change of the
tail-pointer signalizes incoming characters, it busily processed the
buffer contents and went to sleep again. - Works fine!
Inter Process Communication (IPC)
As mentioned earlier, the Multi-Tasking Subsystem provides for
various methods to exchange data between separate tasks and to
synchronize processes executing simultaneously.
I do not intend to go into detail about the underlying concepts,
because there are lots of books available in the market, which discuss
these topics much better than I could do here in this document. Again
I recommend the study of "Operating Systems Design and Implementation"
written by Andrew S. Tanenbaum.
A. Message Passing
The system-calls supporting message passing are quite
straightforward.
A task that wants to send a message to another task in the system
primarily has to know the task-id of the process it would like to
send to. Then it passes its message-buffer and the receiver-id to
the appropriate system-call. An additional parameter tells the
kernel, whether the sender wants to wait if the receiver is not
ready to receive the message or whether the send-request should be
rejected.
A task is ready to receive, whenever it executes a receive-system-
call. It may specify the task-id of the process it wants to receive
from or pass a don't-care code to the system-call, which causes the
kernel to pass through any message sent to this task.
Again, the task may wait until a message arrives or have the system
reject a receive system-call whenever no message is available.
B. Semaphores
There has been written a lot about this topic in the past. Let's have
a look at the internal represetation only at this point.
A semaphore as defined by the Multi-Tasking Subsystem, consists of a
two-byte signal count, which is set to 1 at the time of creation,
and a task-queue to which the processes waiting for this semaphore
are appended.
The busy-state is indicated by a signal-count of zero; any non-zero
value indicates a free-state. Whenever a wait system-call is executed
and the corresponding semaphore is currently busy, the requesting
task is suspended until another process releases this semaphore
through execution of a signal system-call.
Whenever a signal system-call is executed and the task-queue of the
corresponding semaphore is not empty, the first task of this queue
is made computable, otherwise the signal-count is incremented.
Despite the internal structure, tasks refer to semaphores through
untyped pointers. The task creating a semaphore receives this
pointer on return from the system-call and uses it as a handle
furtheron. The actual number of semaphores in the system is
delimited by the amount of free heap space only.
Critical Sections
You may use the standard functions/procedures supplied with your
compiler freely inside your tasks. BUT - procedures like WriteLn are
not designed to work in multi-tasking environments, that means, they do
not protect their critical sections from beeing entered more than once
at a time.
An example will help to understand what I try to explain: Lets think
of two equaly priorized tasks which spend their lives WriteLn-ing
strings to the terminal screen. The process of bringing a bunch of
characters to the screen is much more complicated than the simple
WriteLn-statement makes believe. Therefore it is likely that
sometimes both tasks will be preempted while in the middle of
performing the screen-write. As a consequence the screen output
becomes clobbered.
Imagine the effects of non-synchronized execution of GetMem and
FreeMem...
Another situation in which you have to block a certain section of code
is a routine, that modifies global data or performs some closely
connected actions that form an undivisible unity (atomic action).
Let me give you an example: Provided you got two tasks executing
simultaneously, which at some point of execution output a character to
the screen at a certain position. This is done by executing a GotoXY,
followed by a Write. If the first task was interrupted after having
executed the GotoXY but before having output the character, the second
task might move the cursor to another location on the screen, which
indeed wouldn't lead to the results desired.
There are two basically different approaches to solve this problem.
First you could force exclusive CPU-access during the execution of
your critical section. This solution is only applicable for very
small portions of code, a GotoXY/Write-sequence for example, because
a CPU-bind blocks any scheduler function including the sleep-queue
and timer management.
In most cases a protection scheme using semaphores is preferable.
You can find an example for this kind of process synchronisation in
the example program PRO_CON.PAS, routines RBuffGet and RBuffPut. A
section of code protected by a semaphore, may only be entered by one
task at a time. Any task trying to enter the critcal section whilst
another process is currently inside is suspended until the latter one
has left the protected area.
Global Type- and Data-Definitions
Global Type-Definitions
Priority = (Pri_Nice, Pri_User, Pri_Kernel);
This type-definition is used to describe the priority-level at
which a task is to be executed.
WaitFlagType = (Wait, NoWait);
Passed as a parameter to the system-calls concerned with
message-passing, this type indicates whether a process intends to
wait for a) a message to be received or b) a message available to
be received.
TaskReturn = (Task_Crashed, Task_NotFound, Task_NoMsg,
Task_NotReady, Task_OK, Task_Invalid);
The system-calls concerned with message-passing return one of these
codes to the calling process.
Task_Crashed...: A task was addressed, which has been disabled
following a stack-overflow condition
Task_NotFound..: The task-table entry indexed by the task-id
passed to the executed system-function is
out of range or currently empty
Task_NoMsg.....: There is no message available (Receive with NoWait)
Task_NotReady..: The receiver is not waiting for a message (Send
with NoWait)
Task_OK........: Fine, that's it
Task_Invalid...: An invalid task-id was passed to the
system-function
TaskStatus = (Running, Ready, Sleeping, Available, Crashed,
Waiting, Sending, Receiving, Suspended);
Return value returned by GetTaskState.
SemReturn = (Sem_NoSpace, Sem_NotFree, Sem_OK, Sem_Invalid);
Some system-calls concerned with semaphore-handling return a value
of type SemReturn.
Sem_NoSpace..: There is no heap space available
Sem_NotFree..: You have tried to delete a semaphore whose task-queue
is not empty
Sem_OK.......: Everything's allright
Sem_Invalid..: You passed an invalid semaphore-handle (NIL)
TaskNoType = Integer;
Higher level representation of the task-id. I decided to redefine
the type integer to be able to change this definition in a later
version without having to think about the program code.
BPtr = ^Boolean;
Definition required by GetTimBusy.
Global Constants
Task_NoSpace
If task creation fails, because there is not enough free dynamic
memory available to allocate a private stack, CreateTask will return
this value instead of the task-id.
Task_NoSlot
If task creation fails, because the maximum number of simultaneously
active tasks is reached, CreateTask will return this value instead
of the task-id.
StateText
StateText is an array indexed by a value of type TaskStatus, which
contains a textual representation of every possible task state.
AnyTask
Don't-care-value, passed to the receive system-function whenever a
process doesn't care from whom it is going to receive a message.
Global Variables
InInt28 (Boolean)
Whenever COMMAND.COM is waiting for input, it permanently issues
an INT 28H (DOS multi-tasking interrupt). MtPopUp for example
catches this interrupt and sets InInt28 to TRUE whilst inside the
interrupt-handler. This informs the kernel know that a task-switch
may safely performed although the DOS critical-flag is set.
Internal Type- and Data-Definitions
Internal Type-Definitions
CSDataType = RECORD
TimBusy : Boolean;
Old08 : Pointer;
ISRStat : Byte;
END;
CSDataType describes a small status-area located in the code
segment of the assembly language module.
"TimBusy", the timer-busy flag, is set to a non-zero value
whenever the interrupt-service routine handling timer-tick
interrupts is currently executing. This prevents reentrance, not
allowed in this case.
If you block the CPU by executing BindCPU, this flag is also set
to simulate a timer-busy condition.
"Old08" contains the interrupt-vector pointing to the original
interrupt-handling procedure.
There are various interrupt-handlers that must not be disturbed by a
task-switch (for example the disk-I/O vector 13H). The byte
addressed by "ISRStat" is used to store a number of interrupt-busy
flags, one for each interrupt that may not be interrupted. By
entering the corresponding interrupt-handling procedure, the flag is
set to 1 and reset on return. At task-switch may only be performed,
if both, the critical-flags and the ISRStat, contain zero.
TaskPtrType = ^TaskDescType;
Pointer to a task-table slot.
SchedRecType = RECORD
First : TaskPtrType;
Last : TaskPtrType;
END;
The anchor of a task-queue. Such task-queues are represented
internally as forward-chained list structures. The scheduler-queues
for example are task-queues that contain all tasks in the system
that are computable or running at the moment. The first element of
the queue is addressed using the pointer "First", the tail of the
queue may be found by following the pointer "Last". This enables
the kernel to quickly append new elements to the end of the queue.
TaskDescType = RECORD
NextTask : TaskPtrType;
TaskNo : TaskNoType;
SaveArea : SaveAreaType;
Status : TaskStatus;
RunLevel : Priority;
Stack : PointerType;
StackSize : Word;
CPU : LongInt;
TimeSlice : Integer;
DelayCount: LongInt;
Senders : SchedRecType;
RecFrom : TaskNoType;
RecBuff : Pointer;
END;
Here you see the structure of a so-called task-descriptor, which
makes up one slot of the task-table.
"NextTask" is used to chain the tasks in a queue together. The
queue is terminated by a NIL value contained in the NextTask field.
The task-id is stored in the field named "TaskNo". This value is
identical to the slot-number of the corresponding task-descriptor.
Quite a number of system-calls need this value to index into the
task-table when manipulating the contents of a particular task-
descriptor.
As you already know, every task possesses a private stack. The
actual top of stack as represented by the registers SS and SP, is
stored in the structure named "SaveArea". The actual size and
location of this stack are stored in fields named "Stack" and
"StackSize". The kernel checks for a stack-overflow condition
whenever a task-switch is performed.
"Status" contains the acutal state of a task. If this field
contains the value "Available", the task-table slot is empty and
may be assigned to a newly created task.
The field indicating a task's priority is named "RunLevel". The
contents of this field is used to select the appropriate
scheduler-queue.
"TimeSlice" is used to keep the number of timer-ticks already used
up during the current timeslice. This value might become negative
due to dynamic scheduling.
The last field concerned with time-sharing is named "DelayCount".
Whenever a task goes to sleep for a while, this field is filled
with the number of ticks the task intends to stay asleep. It is
decremented on every timer-tick until the value zero is reached.
Than the task is re-scheduled.
The remaining fields in the task-descriptor are concerned with
message-passing. "Senders" is the anchor of a task-queue
containing the tasks wishing to send to this process.
"RecFrom" is used to indicate which task a process wants to
receive from on the one hand and to hold the task-id of the sender
after having successfully received a message on the other hand.
The latter function is important when receiving messages using the
don't care code "AnyTask". The receiver will not know in advance
from whom he will receive a message. He may find it out by
questioning the contents of "RecFrom".
"RecBuff" points to the message-buffer.
Semaphore = RECORD
Signals : Word;
WaitQueue : SchedRecType;
END;
Here we have the internal representation of a semaphore. The
signal-count is stored in "Signals" and the tasks kept waiting for
the semaphore to become free, are chained to the anchor WaitQueue.
EventType = RECORD
MemLoc : Pointer;
OrgValue : Byte;
Task : TaskNoType;
END;
We have already heard about events and what they are for.
Here is how they are stored internally.
"MemLoc" points to the memory-location to monitor.
"OrgValue" contains the original contents of the byte at MemLoc^.
"Task" finally is the task-id of the requesting task. It is used
to index into the task-table when re-scheduling the task.
Right before re-appending the task to its scheduler-queue, the
kernel checks whether it is still alive!
TimerType = RECORD
MemLoc : Pointer;
Ticks : Word;
Task : TaskNoType;
Next : TimerPtr;
END;
This is how timers are stored. Whenever a timer is created, the
necessary memory is allocated on the heap. Than the timer is
inserted into the TimerQueue. The single timers contained in this
queue are chained through their "Next" pointers. A NIL-pointer
terminates the timer-queue.
"Ticks" is used to store the number of timer-ticks to wait until
the timer expires.
"MemLoc" points to a memory location whose content is incremented
as soon as the timer expires. Before the kernel actually increments
the byte at "MemLoc", it checks whether the task is still alive.
If not, no action is taken for not to destroy a memory location
that may already belong to another task's private address-space.
"Task" again holds the task-id of the requesting process, which is
used to index into the task-table.
PointerType = RECORD CASE Integer OF
1: (P : Pointer);
2: (POfs : Word;
PSeg : Word);
END;
Split up a pointer in its segment and offset components.
Internal Constants
TicksPerSecond = 18;
This constant is used to describe the number of timer-ticks that
make up a second. You could easily increase the number of
task-switches by using the RTC of an AT-computer for example. If
after the change this constant reflects the new number of
interrupts per second, any program using Seconds() to calculate
its sleeping time, will not have to be adapted.
MaxEvents = 10;
"MaxEvents" determines the maximum slots in the event-table. It is
recommended to use the smallest possible value to keep the amount
of time needed to search this table as small as possible.
Quantum : Byte = 2;
MaxDynQuantum : Byte = 10;
Simply default values that may be changed by means of the
TimeSlice function.
MaxTasks = 50;
This value determines the maximum number of slots in the
task-table.
SafetyMargin = 20;
Whenever the kernel checks for stack-overflow conditions, it
assumes a margin of "SafetyMargin"-bytes from the highest possible
stack-address to be "untouchable" and issues a stack-overflow
message if any byte in this area is used by the corresponding task.
Internal Variables
TaksTable : ARRAY[1..MaxTasks] OF TaskDescType;
The root of all evil - the task-table.
CurrentTask : TaskPtrType;
This pointer is used to hold the address of the task-descriptor
belonging to the currently running task. It may only be
manipulated by the scheduler.
TaskQueues ARRAY[Pri_Nice..Pri_Kernel] OF SchedRecType;
These are the anchors of the various scheduler-queues.
SleepQueue : SchedRecType;
Here your tasks may sleep for a while....
EventTable : ARRAY[1..MaxEvents] OF EventType;
Whenever an EventWait is issued, a slot of this table is allocated
for the requested event. The number of table entries available
reflects the maximum possible number of simultaneously active
events.
ActiveEvents : Byte;
To be able to skip event-checking, the kernel counts the number of
currently active events. Whenever this field contains zero, no
check for events is performed.
TimerQueue : TimerPtr;
This is the head of the timer-queue. If the pointer contains NIL,
no timers are currently active.
InDosFlag : PointerType;
DosCritical : PointerType;
DOS uses two memory-locations to store flags that contain a
non-zero value whenever DOS is not in a consistent state. The
pointers named above are used to hold the addresses of those
memory-locations.
CSDataPtr : ^CSDataType;
This pointer holds the address of a small status area located in the
code-segment of the assembly language module.
Reference Section (alphabetically sorted)
BindCpu
Declaration
PROCEDURE BindCPU;
Purpose
BindCPU is used to bind the CPU to the currently running process
temporarily. No task-switch will be performed an no other
scheduler activities take place until the CPU is released by a
ReleaseCPU call.
Example
...
BindCPU; {gain CPU exclusivly}
GotoXY(1,10); {atomic action}
Write('*');
ReleaseCPU;
...
Remarks
BindCPU blocks the whole kernel! While the CPU is bound, no
event-checks, no timer-countdown and no sleep-queue handling are
performed.
You should only use BindCPU to protect VERY small portions of
code. In most cases, the use of semaphores is recommended
See Also
ReleaseCPU
CancelTimer / CreateSem
Declaration
FUNCTION CancelTimer(MemPtr:Pointer):BOOLEAN;
Purpose
If there exists a timer that will cause the memory location at the
address "MemPtr" to become incremented, it will be removed from the
system. CancelTimer returns TRUE, if a timer could be found,
otherwise FALSE is returned.
Example
VAR Timeout : Boolean;
Ok : Boolean;
...
Timeout := False; {initialize}
IF QueueTimer(@Timeout,Seconds(1)) {create Timer}
THEN BEGIN
REPEAT {wait}
... {some action}
UNTIL Timeout OR Ok;
IF CancelTimer(@Timeout) THEN; {Kill Timer}
... {some more action}
END;
See Also
QueueTimer
Declaration
FUNCTION CreateSem(VAR SemPtr:Pointer):SemReturn;
Purpose
CreateSem allocates a semaphore on the heap and returns a pointer
that functions as a handle in further system-calls referring to
this semaphore. The handle is placed into the variable SemPtr.
The return value of this system-function indicates whether the
action could be completed successfully.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error;
See Also
Typedefinition SemReturn, RemoveSem
CreateTask
Declaration
FUNCTION CreateTask(TaskPointer:Pointer;Level:Priority;
BytesStack:Word):TaskNoType;
Purpose
This function is used to create a subtask.
The address of the procedure to be activated as an independently
running task is to be passed in "TaskPointer".
"Level" describes the desired priority level.
Finally, the parameter "BytesStack" contains the desired size of
the task's private stack in bytes.
CreateTask returns the task-id or a negative value indicating an
error-condition.
Example
PROCEDURE SubTask;
{
I am a task that beeps every second
}
BEGIN {SubTask}
REPEAT { Body }
Sleep(Seconds(1)); { }
Sound(1000); { ACTION!! }
Delay(20); { : }
NoSound; { }
UNTIL False; { Body }
END; {SubTask}
...
...
BEGIN {Main}
IF CreateTask(@SubTask,Pri_User,300) < 0
THEN Error;
...
END. {Main}
Remarks
With respects to programming, a task is realized as a Pascal
procedure without parameters, whose body has to be an infinite
loop. You must NEVER leave the body of a task except by executing
a Terminate system-call.
A task is allowed to have local variable definitions, you will
have to take care however, that the private stack is properly
dimensioned to be able to hold the local data. You will have to
add the amount of memory needed to hold the local variables to
the bytes stackspace the task needs at runtime.
YOU are responsible for passing a valid stack size!!!! This
parameter is not checked!!
See Also
Typedefinitions TaskNoType and Priority, Global Constants,
Terminate
DoShutDown
Declaration
PROCEDURE DoShutDown;
Purpose
Shutdown the Multi-Tasking Subsystem. All interrupt vectors
captured are restored to their original state.
Remarks
This procedure is called internally through the exit-procedure of
the subsystem-unit.
DumpTaskTable
Declaration
PROCEDURE DumpTaskTable;
Purpose
Display the actual state of all active tasks in the system.
Example
Nr. Status Prioritt Slice Delay Stack free CPU
1 Ready Pri_Nice 2 0 205 18
2 Waiting Pri_User 3 0 -1 1
3 Running Pri_Kernel -1 0 221 1
4 Waiting Pri_User 2 0 321 5
Remarks
The current content of your display are overwritten. In case you
wish to realize a status-window, you should replace DumpTaskTable
with a routine of your own.
See Also
Global Constants StateText, GetTaskState
EventWait / GetPID
Declaration
FUNCTION EventWait(MemPtr:Pointer):Boolean;
Purpose
EventWait causes your task to be suspended until the contents of
the memory-location pointed at by "MemPtr" changes.
If no empty event-table entry can be found, EventWait returns
FALSE.
Example
VAR HeadPointer : Word; {Bufferpointer }
TailPointer : Word; { " }
...
IF NOT EventWait(@TailPointer) {wait for a change}
THEN Error {of the pointer}
ELSE DoJob;
...
Declaration
FUNCTION GetPID:TaskNoType;
Purpose
By calling GetPID, a task can find out its task-id.
See Also
Typedefinition TaskNoType
GetTaskState / GetTimBusy
Declaration
FUNCTION GetTaskState(Task:TaskNoType):TaskStatus;
Purpose
GetTaskState returns the current status of the task whose task-id
was passed to the function.
Example
This task displays its own state on the screen:
Writeln(StateText[GetTaskState(GetPID)]);
See Also
Typedefinitions TaskStatus and TaskNoType, Constant StateText
Declaration
FUNCTION GetTimBusy:BPtr;
Purpose
GetTimBusy returns the address of the timer-busy flag which
contains a non-zero value whenever the interrupt-handling
procedure for the INT 8 is active.
Sometimes an external interrupt-handler needs to know, whether
the scheduler is currently busy.
No system-call must be executed whilst the busy-flag is set.
See Also
Typedefinition BPtr
QueueTimer / ReadySuspended
Declaration
FUNCTION QueueTimer(MemPtr:Pointer; Counter:Word):BOOLEAN;
Purpose
Creates a timer that expires after "Counter" ticks and increments
the contents of the memory-location pointed at by "MemPtr".
If there is not enough heap space available to allocate a timer,
QueueTimer will return FALSE, otherwise TRUE is returned.
Example
VAR Timeout : Boolean; {Timeout marker}
...
Timeout := False; {initialize }
IF NOT QueueTimer(@Timeout,Seconds(1)) {Timeout after 1 sec}
THEN Error;
...
IF CancelTimer(@Timeout) {Remove Timer }
THEN ; {if still existent}
...
See Also
CancelTimer
Declaration
FUNCTION ReadySuspended(Task:TaskNoType):BOOLEAN;
Purpose
ReadySuspended is used to re-enqueue a task into its
scheduler-queue wich has been suspended using the Suspend
system-function.
If the action could be completed successfully, the value TRUE is
returned. A return value of FALSE indicates that the task addressed
was not currently suspended.
See Also
Suspend
Receive
Declaration
FUNCTION Receive(FromTask:TaskNoType; MsgBuff:Pointer;
WaitFlag:WaitFlagType):TaskReturn;
Purpose
By executing a Receive system-call, a task can receive a message
from another task in the system.
The first parameter, "FromTask", indicates from which task the
caller would like to receive. If "FromTask" contains "AnyTask",
any message is passed through, otherwise only messages sent by
the process whose id matches "FromTask" are received.
"MsgBuff" points to the start of a message buffer into which the
received message is copied. The kernel does not check whether the
message received fits into the message buffer - it simply
copies!
If the caller passes a "WaitFlag" of value "Wait", it will be
suspended until a message arrives. Otherwise the kernel
immediately returns control to the caller if no message is
currently available.
Example
VAR Puffer : String; {Message buffer}
...
IF Receive(AnyTask,@Puffer,Wait) <> Task_Ok
THEN Error
ELSE Writeln(Puffer);
...
Remarks
Using the don't care task-id "AnyTask", you could realize a task
that spends its life waiting for messages from its environment.
If you had to monitor some sensors, for example, you could have a
separate task for every sensor to be monitored. Whenever some
action has to be taken, the corresponding monitor-task sends a
message to a central workhorse that performs the necessary
adjustments.
See Also
Constant AnyTask, Typedefinition TaskReturn, Send, ReceivedFrom
ReceivedFrom / ReleaseCPU
Declaration
FUNCTION ReceivedFrom:TaskNoType;
Purpose
By calling ReceivedFrom, a task can find out, who was the sender
of the last message received.
See Also
Typedefinition TaskNoType, Constant AnyTask, Receive
Declaration
PROCEDURE ReleaseCPU;
Purpose
Provided you have bound the CPU by executing a BindCPU, you will
have to reenable scheduling through execution of a ReleaseCPU.
Example
...
BindCPU; {disable scheduler}
GotoXY(1,10); {atomic action}
Write('*');
ReleaseCPU; {...set them free...}
...
See Also
BindCPU;
RemoveSem / Sched
Declaration
FUNCTION RemoveSem(VAR SemPtr:Pointer):SemReturn;
Purpose
RemoveSem physically removes a semaphore. The formerly occupied
heap space is returned to the memory pool.
"SemPtr" must be a valid semaphore-handle as returned by the
CreateSem system-call.
RemoveSem returns a value of type SemReturn that indicates
success or failure.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error;
...
IF RemoveSem(Semaphore) <> Sem_Ok
THEN Error;
...
Remarks
The system mostly is not able to prove whether a valid handle is
supplied. It is your job to take care of this.
You cannot remove a semaphore, whose task-queue currently is not
empty.
See Also
Typedefinition SemReturn, CreateSem
Declaration
PROCEDURE Sched;
Purpose
Initiate a task-switch, give up the current timeslice.
"Sched" is used internally by many system-functions. If you want
to have your task give up its timeslice, you should better use
Sleep(1).
Seconds / SemClear
Declaration
FUNCTION Seconds(Sec:Word):LongInt;
Purpose
"Seconds" returns the number of timer-ticks that make up a second.
It is recommended that you use this function whenever you need to
specify a special amount of time. Use of Seconds makes your
application independent of changes to the task-switch rate.
Example
IF QueueTimer(@Timeout,Seconds(1) SHR 1) { timout after }
THEN; { half a second }
See Also
QueueTimer, CancelTimer
Declaration
PROCEDURE SemClear(SemPtr:Pointer);
Purpose
Reset a semaphore's signal-count to zero.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error
ELSE SemClear(Semaphore);
Remarks
Internally SemClear is translated into SemSet(Semaphore,0).
See Also
SemSignal, SemWait, SemClearWait, SemSet
SemClearWait
Declaration
PROCEDURE SemClearWait(SemPtr:Pointer);
Purpose
Reset a semaphore's signal-count to zero and wait until another
task initiates a SemSignal system-call for this semaphore.
In contrast to SemWait, SemClearWait ALWAYS leads to the calling
task beeing suspended.
Example
VAR Ready : Pointer; {Semaphore }
...
PROCEDURE SubTask;
{
I am a task that needs to save the values of some global
variables during initialisation. The main program, however,
changes these variables during the process of its execution.
Therefore, the main program has to wait until I indicate that I
have transferred the values I need to my local data area.
}
BEGIN {SubTask}
... {initialize }
SemSignal(Ready); {O.K. I'm ready }
REPEAT
... {Body }
UNTIL False;
END; {SubTask}
...
...
BEGIN {Main}
...
IF CreateSem(Ready) <> Sem_Ok {Create a semaphore}
THEN Error;
IF CreateTask(@SubTask,Pri_User,300) < 0 {Create a task }
THEN Error
ELSE SemClearWait(Ready); {wait for O.K. }
...
END. {Main}
See Also
SemClear, SemSet, SemSignal, SemWait
SemCut / SemGetSignals
Declaration
FUNCTION SemCut(SemPtr:Pointer;Task:TaskNoType):BOOLEAN;
Purpose
If the task whose id is passed as "Task" currently is waiting in
"SemPtr"'s task-queue, it is removed from this queue.
SemCut returns TRUE, if the action could be completed successfully.
Remarks
SemCut is a somewhat brutal function! It forces an innatural
action and was added for a special application purpose in MtPopUp.
PLEASE do use it with care!! - Better keep away from it.
See Also
SemPaste
Declaration
FUNCTION SemGetSignals(SemPtr:Pointer):Word;
Purpose
Returns the signal-count of the semaphore "SemPtr".
Example
FUNCTION SemBusy(S:Pointer):BOOLEAN;
{
This function checks whether a semaphore is currently busy.
}
BEGIN {SemBusy}
SemBusy := (SemGetSignals(S) = 0); {Signal-Count = 0}
END; {SemBusy}
See Also
SemSignal, SemWait, SemClear, SemClearWait, SemSet
SemPaste / SemSet
Declaration
PROCEDURE SemPaste(SemPtr:Pointer; Task:TaskNoType);
Purpose
SemPaste unconditionally appends "Task" to the task-queue of the
semaphore referred to by "SemPtr". No validity checks are performed!
Remarks
SemCut is a somewhat brutal function! It forces an innatural
action and was added for a special application purpose in MtPopUp.
PLEASE do use it with care!! - Better keep away from it.
Declaration
PROCEDURE SemSet(SemPtr:Pointer; Count:Word);
Purpose
SemSet sets the signal-count of a semaphore to a definite value.
This action does not have any influence on tasks that might be
waiting in the task-queue belonging to this semaphore.
Example
CONST Elements = 100;
VAR Buffer : ARRAY[1..Elements] OF Byte; {Buffer}
Full : Pointer; {No. of used slots}
Empty : Pointer; {No. of empty slots}
...
BEGIN {Main}
...
IF CreateSem(Full) = Sem_Ok
THEN SemClear(Full); {no slot used}
IF CreateSem(Empty) = Sem_Ok
THEN SemSet(Empty,Elements); {all slots free}
...
END. {Main}
See Also
SemSignal, SemWait, SemClear, SemClearWait
SemSignal / SemSoWaiting
Declaration
PROCEDURE SemSignal(SemPtr:Pointer);
Purpose
If the task-queue of the semaphore referred to by "SemPtr" is
currently empty, the signal-count will be incremented. Otherwise
the first task waiting is re-scheduled and the signal-count remains
unchanged.
Example
VAR Critical: Pointer;
...
IF SemCreate(Critical) <> Sem_Ok
THEN Error;
...
SemWait(Critical);
...
{critical section}
...
SemSignal(Critical);
...
See Also
SemWait, SemClearWait, SemClear, SemSet
Declaration
FUNCTION SemSoWaiting(SemPtr:Pointer):BOOLEAN;
Purpose
SemSoWaiting returns TRUE, if there are tasks waiting for
"SemPtr" to become available.
See Also
SemWait, SemSignal, SemClear, SemClearWait, SemSet, SemGetSignals
SemWait / Send
Declaration
PROCEDURE SemWait(SemPtr:Pointer);
Purpose
If a semaphore's signal count currently is greater than zero, it
will be decremented. Otherwise the caller is suspended and
appended to the task-queue of this semaphore.
It remains suspended until another task issues a SemSignal for
the semaphore it is waiting for.
Example
VAR Critical: Pointer;
...
IF SemCreate(Critical) <> Sem_Ok
THEN Error;
...
SemWait(Critical);
...
{critical section}
...
SemSignal(Critical);
...
See Also
SemSignal, SemClearWait, SemClear, SemSet
Declaration
FUNCTION Send(ToTask:TaskNoType; Msg:Pointer; MsgSize:WORD;
WaitFlag:WaitFlagType):TaskReturn;
Purpose
The system-function "Send" lets a task send a message to another
process in the system.
The parameter "ToTask" contains the task-id of the receiver. This
must be the task-id of a currently active task. You must not use
the constant "AnyTask" in this context.
"Msg" points to the start of the message-buffer whose size is
passed in "MsgSize".
Finally a wait-flag has to be supplied, that indicates whether
the sender wishes to wait until the receiver is willing to
receive the message. If you set "WaitFlag" to "NoWait", the
kernel will return control to your task immedeately if the
receiver is not waiting for a message.
Send / Sleep
Example
VAR Puffer : String;
...
IF Receive(AnyTask,@Puffer,Wait) <> Task_Ok
THEN Error
ELSE Writeln(Puffer);
...
Remarks
The message is physically copied to the message buffer of the
receiving process. This is not the best solution from the point
of view of performance, but is provided for the highest possible
independence of sender and receiver. They could theoretically be
located on different machines in a networking environment. If
you better like a pointer to a message to be passed, make your
pointer the message.
See Also
Typedefinition TaskReturn, Receive
Declaration
PROCEDURE Sleep(Ticks:LongInt);
Purpose
"Sleep" lets your task suspend itself for a certain number of
timer-ticks.
Example
Writeln('I''m going to sleep for 3 seconds!');
Sleep(Seconds(3));
Writeln('Hi, here I am again!');
See Also
Seconds
SetPri
Declaration
PROCEDURE SetPri(Pri:Priority);
Purpose
"SetPri" lets a Task change its priority at runtime.
Remarks
The new priority will come into effect at the end of
its current quantum. Therefore the task will remain in
control of the CPU, although it might have lowered its
priority.
Suspend / Terminate
Declaration
FUNCTION Suspend(Task:TaskNoType):BOOLEAN;
Purpose
Brutally suspend a task which is currently computable.
"Task" contains the id of the task to be suspended.
A task that has been suspended by a Suspend system-call can ONLY
be reactivated by a ReadySuspended system-call.
If the task addressed was not computable, Suspend would return
FALSE to the caller. Otherwise TRUE is returned.
See Also
Typedefinition TaskNoType, ReadySuspended
Declaration
PROCEDURE Terminate;
Purpose
A task may terminate itself by executing a "Terminate"-system-call.
The task-descriptor is marked available and the private stack is
returned to the memory pool.
See Also
CreateTask
TimeSlice
Declaration
PROCEDURE TimeSlice(Slice, DynQ:Byte);
Purpose
The width of a timeslice and the +/- maximum number of ticks to
credit/debit during dynamic scheduling, may be changed by issuing
a "TimeSlice"-system-call.
"Slice" describes the size of a timeslice in timer-ticks;
"DynQ" contains the number of ticks to credit/debit during
dynamic scheduling.
"Slice" defaults to 2; "DynQ" defaults to 10.
History
Version 1.30 / November 1988
- Task queues are now realized as doubled chained lists; this
increases speed when removing list elements
- Tasks beeing awakened by an event or returning from a message
passing operation, are now put to the front of their run level
task queue. Thus they will be selected by the scheduler earlier.
- A bug in the delta computation of QueueTimer has been corrected
- The scheduler stack is no more located in the data segment, but
has been moved to the heap
- The formerly local data type "PointerType" has been made public
- The assembly language module has become slightly optimized
- A new procedure "SetPri" has been added
thanks for using my Multi-Tasking Subsystem for Turbo Pascal 5.0.
This product was designed to enable everyone to study programming in a
multi-tasking environment, without having to spend a lot of money to
purchase an operating system like OS/2 or XENIX (and of course the
extension memory necessary to run it).
The Turbo Pascal Multi-Tasking Subsystem is a very inexpensive and
yet powerful tool that adds multi-tasking capabilities to your
Turbo Pascal 5.0 programs.
You will be able to study the programming of parallel processes and
problems of interprocess communication using the programming
environment you are accustomed to.
Although the Multi-Tasking Subsystem initially was designed as a means
of experimenting, it became very soon a powerful and easy to use
product, which I hope you will like as much as I do.
At first, I would like to outline the capabilities of the basic system:
- up to 50 (increased on demand) independently executing
tasks in a single program
- 3 priority levels
- code sharing
- preemptive scheduler, using a dynamic scheduling algorithm
- you may use DOS-functions in your tasks safely. A task is never
interrupted within a DOS-function or a critical interrupt
- size of timeslice and range of dynamic CPU-allocation programmable
- message passing
- semaphores
- programmable timers
- primitive event processing (up to 10 simultaneously active event
requests; increased on demand)
- executes on any PC, XT, AT, PS/2 and full compatible running DOS 2.x
or 3.x
- source code available to registered users
Registered users will get an additional UNIT for FREE(!!) which is
based upon the Multi-Tasking Subsystem and provides you with:
- extended keycodes (Chacter, Scan-Code, Shift-Statusword)
- manipulation of the keyboard buffer (clear, add keycodes at the
end or in front of the actual buffer contents)
- keyboard lock and unlock
- execute a DOS-program as subtask
- all you need to have tasks wait for a special hot-key that awakes
them
- all you need to have tasks pop up over the DOS-program and suspend
the DOS-task while they are executing (another way of writing memory
resident programs)
Well, no product is perfect! If you would like to have a function the
Multi-Tasking Subsystem does not include at present, contact me and
perhaps you will find it in the next version. IF I decide to
realize YOUR idea in a later version of the subsystem, you will
receive a FREE-upgrade! I hope you will understand that I reserve
the right to decide which functions will be added.
In this connection, I would like to thank L. David Baldwin and
TurboPower Software for their fabulous source-level debugger TDebug
PLUS 4.01, that helped me very much during the development of my
Multi-Tasking Subsystem.
Let me stop here! Please take the time to read carefully through the
documentation before you "dive" into the world of multi-tasking.
Best regards,
Christian Philipps
Turbo 5.0 Multi-Tasking Subsystem
User's Manual
(c) Copyright 1988 by
Christian Philipps, Moers
all rights reserved
Version 1.30 / November 1988
Author: Christian Philipps
Dsseldorfer Str. 316
4130 Moers 1
West-Germany
Trademarks Mentioned
TurboPower Software is trademark of TurboPower Software, Scotts Valley
Turbo Professional is trademark of Sunny Hill Software, used under
license to TurboPower Software
Turbo Pascal and TASM are registered trademarks of Borland International
MS-DOS, OS/2 and MASM are trademarks of Microsoft Corporation
A86 is trademark of Eric Isaacson Software, Bloomington
UNIX is trademark of AT & T
IBM PC, XT, AT, PS/2 and PC-DOS are trademarks of International
Business Machines Corporation
License Agreement
The Turbo Pascal Multi-Tasking Subsystem is marketed through the
ShareWare marketing concept.
Therefore I grant you the right to reproduce, distribute and use
copies of this ShareWare version of our Multi-Tasking Subsystem
(including the on disk documentation), on the express condition that
you do not receive any payment, commercial benefit, other
consideration for such reproduction or distribution, or change this
license agreement or the copyright notice.
The Multi-Tasking Subsystem, the additional unit MTPOPUP and the
documentation of both are copyright (c) 1988 by Christian Philipps,
Dsseldorfer Str. 316, 4130 Moers 1, West-Germany.
You have the right to evaluate this ShareWare version for a period of
30 days to find out whether it suits your needs. If you decide to
continue using this software, I expect that you become a registered
user by sending DM 50 or $35 (object code license) / DM 100 or $70
(object code + source code license) to
Christian Philipps
Dsseldorfer Str. 316
4130 Moers 1
West-Germany
As soon as I receive your registration, I will send you your
personal copy of the subsystem plus a FREE unit based upon the basic
subsystem (see introductory letter).
Furthermore you will get technical support through mail, e-mail or
phone during 1 year from the day of receipt.
Users purchasing an oject code license only may receive the source
code of the latest version later at any point of time by sending
another DM 50 or $35 to the adress above.
Disclaimer
I make no warranty of any kind, either express or implied, including
but not limited to implied warranties of merchantability and fitness
for a particular purpose, with respect to this software and the
accompanying documentation.
In no case, I will be liable for any damages (including damages for
loss of business profits, business interruption, loss of business
information, or other pecuniary loss) arising out of the use of or
inability to use this software, even if I have been advised of the
possibility of such damages.
Getting Started
Throughout this documentation I assume that you are familiar with
Turbo Pascal 5.0 in general and with the UNIT concept.
I also assume that you know how to use the advanced features of Turbo
Pascal 5.0, for example type-casting of pointers. If necessary,
please refer to your Turbo Pascal documentation for more information
on any of the compiler's internal functions or the functions contained
in the units Crt, Dos,...
The Multi-Tasking Subsystem mainly consists of four units:
a) CpMulti.TPU
This is the basic Multi-Tasking Subsystem. All further units are
based upon this piece of software.
b) MTPOPUP.TPU (registered users only)
MtPopUp is a unit, based upon the basic subsystem and providing
full control over the system keyboard and supports one DOS-program
to be run as a separate task. Tasks may be kept waiting for a
certain key-combination to be pressed and somewhat resident
programs can be written by executing another COMMAND.COM in the
foreground. Please note that this unit is available only to
registered users, who will receive it in return for their
registration.
c) QUEUE.TPU (Public-Domaine)
This unit contains several functions, which help you to create,
delete and manage forward-chained list-structures. The structure
of a single list element is defined by your application and may be
any data structure. Passing data between separate tasks, you will
shortly find out that list structures, as provided by this unit,
are convenient for implementing pipe-like data structures.
Studying the routines contained in the accompanying source code for
QUEUE, you will learn how to assure the consistency of shared data
at any time during execution.
d) CPMISC.TPU
Miscellaneous service routines, used by QUEUE. The source code of
this Unit is only partially made available.
Let's take a look at the way to USE the Multi-Tasking Subsystem: The
usage of this software is quite straightforward, provided you enable
your compiler to find the necessary units (please refer to your
compiler manual).
You will incorporate the Multi-Tasking library in your programs by
simply including CpMulti in the USES-statement. CpMulti must be the
first unit of the subsystem preceded, however, by the basic units Crt
(or alternatives) and Dos.
Example: USES Crt, Dos, CpMulti, MtPopUp, Queue;
The units CpMulti and MTPOPUP use Turbo's CRT-unit, so problems may
arise, when using an alternative CRT-unit (such as the TurboProfes-
sional unit TPCRT) in your program.
For the special case that you own the fantastic TurboProfessional
package of TurboPower Software, your diskette contains files with
the extension .PRO, which I compiled using the TPCRT-unit. You only
need to rename those files to the extension .TPU.
If you wish to use another alternative Crt-unit that conflicts with
the standard CRT-unit, you will have to order the source code and
recompile the subsystem.
After having set up your USES-statement properly, you will have to
include the compiler switch S-, which prevents the compiler
from generating object code that checks the stack boundaries. This
is VERY important, because each task has its own private stack
that is automatically reserved on the heap at the time of creation.
Without the S- directive, you would get a stack overflow abort
shortly after creating the first subtask.
Whenever a task-switch is performed, the Multi-Tasking Subsystem
checks the private stack of a subtask A task which uses up its
stackspace (minus a certain safety margin) is aborted. The subsystem
issues an error message and marks the task as "Crashed". The memory
occupied by its private stack is not freed and may be examined using a
debugger.
Now let's have a look at the system startup. When your program comes
to execution the initialisition code in CpMulti captures some
interrupt vectors and sets up the task-table. It creates a
null-process with lowest priority (a simple loop) that consumes
CPU-time whenever no other process is currently computable. Finally
the main program is started as task no. 2.
The shutdown again is fully automatic. When your program terminates
or a runtime error brings you to a halt, CpMulti's exit procedure
gains control, disables multi-tasking and restores the interrupt
vectors previously captured.
Only in rare occasions, your system will completely crash. This
could happen, when one of your tasks runs mad and overwrites
valuable system data.
You might start experimenting right now, BUT I recommend studying
the rest of this documentation before. The following sections will
give you a deeper understanding of the concept and the interior of the
Multi-Tasking Subsystem.
Multitasking is too complicated however, to go down to the last.
Those of you, who would like do get a deeper insight into the design
of multi-tasking operating systems should read the book "Operating
Systems, Design and Implementation" by Andrew S. Tanenbaum, Prentice
Hall or scan through the bookshelves of Prentice Hall or Adison
Wesley. The book mentioned above helped me very much in designing the
Multi-Tasking Subsystem and describes a UNIX-like operating system for
personal computers in detail (MINIX + source code available from
Prentice Hall).
The Basic System (CpMulti)
1. Introduction
At the moment, more and more programmers become interested in multi-
tasking environments for personal computers. I suppose, anyone who
intends to stay up to date, will have to deal with systems like OS/2
or UNIX / XENIX /...
Unfortunately, those systems are quite expensive and require lots of
extension memory. Only few wealthy private programmers (have you
ever seen one?) will be able to buy such an operating system already
today.
At the time I started developing the Multi-Tasking Subsystem, my
situation had been as described above. Now I'm in the position to
provide you with an inexpensive means to study the design and
synchronisation of parallel processes. In addition to that, the
Multi-Tasking Subsystem will help you to write programs more complex
and much more sophisticated, especially in areas like data
communication, where quite a lot of actions are to be taken nearly
simultaneously.
Concept
Like UNIX or OS/2, the Multi-Tasking Subsystem was designed as a
time-sharing system. It was not designed for real-time applications.
A time-sharing system tries to divide the available computing power
among all computable processes evenly. Therefore, a process that has
used up its timeslice, is temporarily suspended and another
computable process gains control (preemptive scheduling). This is
accomplished by having a timer-interrupt-service-routine monitor the
system activities about 18 times a second. This routine initiates a
task-switch whenever the running task has used up its quantum.
Although I could have used the AT-RTC (Real-Time Clock), which in fact
would decrease the minimum possible timeslice, I have decided not to
do so for several reasons:
a) The task-switch is the most time consuming action, the kernel has
to perform. A smaller timeslice, increases the number of possible
task-switches, but increases the kernel overhead as well.
b) The Multi-Tasking Subsystem could not run on PC or XT-computers,
because those do not have a real-time clock.
With a timer-interrupt occuring 18 times a second, the smallest
possible timeslice is about 55,5 milliseconds. During the
initialisation sequence the timslice is set to 110 milliseconds.
If you choose a 55,5 millisecond timeslice the amount of computing
power consumed by the mere task-switching process will be about 1,4
percent (12 MHz AT-compatible).
Dynamic Scheduling
Normally each task is temporarily suspended whenever it has used up
its timeslice. If this happes inside a DOS-function, however, the
kernel may not safely interrupt the currently executing task. It has
to wait, until it returns from the DOS-call.
Provided this task has to read thousands of records from a data file
and write them to another file, the kernel will be inside DOS
again, when the kernel tries to suspend it at the following
timer-tick. Again, a task switch is impossible! This procedure might
happen again and again until the task has finished.
As you see, under certain circumstances, a task may exceed its
quantum at an amount that cannot be foreseen by the system-kernel.
On the other hand, there might be a task that periodically, lets say
one time a second, look at a particular value taken from a port to
see, whether actions are to be taken. In this case, it will act upon
the condition encountered, otherwise it will go to sleep and give up
its timeslice. Such a task will use up its timeslice very seldom.
If, hovewer, actions are to be taken, it might be suspended in the
middle of its activities because it has used up its quantum. - Indeed
that's not fair!
What can we do to achieve some kind of compensation? OS/2 brought me
to the idea to implement a somewhat dynamic scheduling that proceeds
as follows: Whenever a task gives up its timeslice, the time not used
is placed to this task's credit. If it has to execute longer at a
later point of time, it will not be preempted until it has used up its
current quantum plus its credit. On the other hand, a task that
exceeds its quantum, is not rescheduled until its debit is "used up".
This sounds quite good, but there is a catch to it though. If we
would credit every single tick not used, the task mentioned above
could possibly run for hours if it has to take some action after
all.
For this reason, the amount of ticks beeing placed to a tasks
credit/debit, is limited. You may choose this limit freely and it
defaults to +/- 10 timer-ticks that is slightly above half of a
second.
Priority
The Multi-Tasking Subsystem supports three levels of priority:
"Pri_Nice" which is the lowest, "Pri_User" which is standard and
"Pri_Kernel" which is highest possible priority. Your Pascal
main-program is started at the level "Pri_User" during initialization.
Let's examine, how the different layers of priority are acted upon:
Inside each layer, the tasks are scheduled in straight round-robin
fashion. Whenever there are computable tasks at a higher level none
of the tasks in a level below that will gain control. That means, the
"idle loop" at level "Pri_Nice" will only be activated, if the
layers "Pri_User" and "Pri_Kernel" are empty.
The programmer has to take this concept into account when designing
his application system. The overall system performance depends very
much on the assignment of priorities. Any task can change its
priority at runtime by means of a special system service.
By the way: Throughout this documentation I use the terms "task" and
"process" interchangably, although some experts will be picking up
stones to throw at me at this very moment... I don't feel guilty
doing so, because those experts seem not to have reached an agreement
yet, about how to define these terms.
Tasks that spend most of their lives waiting for events (waiting for
incoming characters,...), should be given highest priority.
Tasks that spend their lives busily working all the time, should be
given standard pritority.
By doing so, the busy ones will be interrupted whenever an event, a
higher priorized task was waiting for occurs and will be able to work
the rest of the time (which will be most of the time) undisturbed.
The lowest priority level will mostly be occupied only by the idle
loop.
Another Catch To It
DOS does not know about interrupt controlled disk I/O in the
fashion of multi-tasking operating systems! There is a loop inside
your ROM that waits for the disk-controller to issue a completion
interrupt, but that is of no use for us, because a disk I/O
operation must never be interrupted by a task-switch.
Multi-tasking operating systems normally switch to another task,
while the currently running process has to wait for a disk operation
to complete. I didn't know how to achieve this without rewriting
the low-level disk I/O-routines. So for now, we'll wait...
As a consequence, a process that performs lots of disk I/O-operations
and which is running at high priority, will possibly block the whole
system.
In a real multi-tasking operating system however, it is very useful
running I/O-intensive processes at a high priority level for the
reasons stated earlier in the context of mostly waiting tasks.
Inter Process Communication (IPC)
The Multi-Tasking Subsystem contains functions for handling
semaphores and message-passing.
Shared-memory does not require system support, because every task
under DOS has access to any memory location in the whole system.
The programmer will have to use semaphores, however, to avoid race
conditions when accessing commonly used memory areas. The
demonstration program PRO_CON gives examples of how to synchronize
your tasks using semaphores.
Programmable Timers
Programmable timers are a very useful means to take care of timeout-
conditions (for example in communications software).
The Multi-Tasking Subsystem let you define any number (limited by
the available heap) of timers with a resolution of 55,5
milliseconds. If a timer expires, a byte located at the address given
at the time of creation is incremented.
The amount of CPU-time consumed by watching the timers currently
active is negligible and independent of the number of timers defined.
Event Handling
Sometimes it can be useful to have a task waiting for the contents of
a memory location to change. This is what I have chosen to call an
event. Realizing a task that waits for a buffer filled by an
interrupt routine, you can acchieve a very short response time at a
minimum waste of CPU-time by defining an event that reactivates your
task whenever the contents of the buffer's tail-pointer changes.
Starting with release 1.30, the subsystem kernel puts a task,
awakened by an event occuring, at the head of the task-queue
corresponding to its run level. Thus it is put in the position of
beeing able to react on the event very quickly.
3. How Is It Done
Most of the subsystem is written in Turbo Pascal 5.0. Only a small
part of the kernel (about 2 KB) is written in assembly language. If
you purchased a source code license, you will find two versions of
the assembly language module on your product diskette. The first one
assembles using Microsofts Macro Assembler V4.0 or higher or TASM 1.0,
the second one is for programmers using the well known ShareWare
assembler A86 V3.19 or higher, written by Eric Isaacson. Both parts
together form a TPU (Turbo Pascal Unit) that contains the whole
subsystem and may be USEd in your programs.
The Task Table
The central element of the Multi-Tasking Subsystem is called "task
table". For reasons of performance, this table was defined
statically in the data segment. It can hold a maximum of 50 tasks at
the moment, but can easily be expanded (if you own the source code).
Every task in the system occupies one entry in the task table. This
entry holds all pieces of information that describe the task's actual
state of execution.
At any point of time each task is in one of the following possible
states:
Running..: This task is currently executed.
Ready....: This task is computable, but another task currently is in
control of the CPU.
Sleeping.: This task has suspended itself for a certain number of
timer-ticks.
Crashed..: The subsystem has detected a stack overflow for this task.
Waiting..: This task is currently waiting for an event or a
resource (a semaphore for example).
Sending..: This task is waiting for the receiver of a message to
do a receive system call.
Receiving: This task is waiting for another task to send it a message.
Suspended: This task was temporarily disabled by another task in the
system and needs action from another task to be put into
the ready-state again.
If a slot in the task-table currently is not occupied by any task its
state-marker will contain the value "Available".
The following state diagram summarizes all possible task-states and
the state-transitions possible among these states. A state-transition
may only take place in the direction indicated by the arrow symbols.
13
> Running > Crashed
2 1
4
Ready < Waiting <3
6 Sleeping <5
7
8 Sending <
10 Receiving <9
12 Suspended <11
State Diagram, Multi-Tasking Subsystem Version 1.10
Task Management
Internally, the tasks are managed using queues. Dependent on its
state of execution, a task may be located in
a) the scheduler queue of its priority-level ("Running" or "Ready"),
b) the sleep-queue ("Sleeping") or
c) the sender-queue of the receiver-task, if the latter one is not
ready to receive the message and the sender indicates that it
wants to wait until the receiver gets ready ("Sending").
Tasks at the states "Suspended", "Receiving" or "Crashed" are
not located in any queue.
A task that is "Waiting" on an event or a resource may be chained to
the event-table or be located in a semaphore's task-queue.
Whenever a task is created, a task-table entry is allocated to
this task. The slot number of its task-table entry becomes the
task-id (task-number or process-number), which some system-calls
are to be passed as a parameter.
In addition to that, a private stack for the task is allocated and
set up.
The task creation can be completed sucessfully only if a) there is
an empty slot in the task-table and b) the private stack can be
allocated on the heap.
Code-Sharing
The Turbo Pascal compiler generates object code that is mostly
reentrant. Such code may be executed in code-sharing.
The term "Code-sharing" can have different meanings. On the one
hand there may be a number of subroutines which are used by more
than one task in the system. Thus more than one task may be
executing a certain subroutine at the "same" point of time, i. e.
the instruction pointers of those tasks point to a location inside
this subroutine at the same point of time.
Provided the code is reentrant, this will never lead to any
problems. You will have to take care of situations which lead to
more than one task modify global data at the same point of time, of
course, but the type of code-sharing described above will never lead to
a crash or the modification of local variables.
"Code-sharing" may also describe a situation, in which the same part
of code (here: the object code of a procedure - the task) is activated
more than once as a separate process. Each of these tasks is given
its own set of registers, its private stack and its local variables.
The latter are actually allocated on the private stack. Provided the
code is reentrant and provided that you, the programmer, take care of
race conditions, there may be an "unlimited" number of separate tasks
executing the same object code simultaneously.
I have already tried both kinds of code-sharing and did not encounter
any problems so far.
Programmable Timers
Lets now have a closer look at the timers. A timer, which in fact is
a memory location containing a counter beeing decremented at any
timer-tick, needs to be examined whenever a clock-interrupt occurs.
If the kernel realizes that the timer has expired, some action will
be to be taken.
As you see, a small extra amount of computing power is necessary to
countdown timers, act upon their expiration and finally remove them
from the system. Some types of application, communication programs
for example require extensive use of timers. Therefore one should
think about an inexpensive implementation of timers to avoid loss of
performance.
The implementation of timers in the Multi-Tasking Subsystem makes the
kernel overhead constant and independant from the number of timers
presently active. This is achieved by a special queueing method,
taken from the timer implementation of MINIX, a UNIX-like operating
system for the IBM PC. As stated earlier, MINIX has been developed by
Andrew S. Tanenbaum and is currently distributed by Prentice Hall. I
recommend MINIX for everybody, who would like to go deeper into
operating system design, it's fantastic!
Return to the timers: The timer-queue consists of a forward-chained
list of timer-structures. Only the first element of this list is
examined at every timer-tick. If the first timer expires, it will be
removed from the queue after having taken the appropriate action to
signal its expiration to the application. The second element now
becomes the head of the queue, and so on.
When a new timer is inserted into the timer-queue, its counter will be
adjusted to reflect the number of ticks remaining after all previous
elements of the timer-queue have expired.
Example:
Timer-Queue > 3 > 5 > 1
Time to wait: 3 Ticks 8 Ticks 9 Ticks
Event Handling
You might think of lots of events that could be handled by an
application.
Currently only the change of contents with respect to a single
memory location is supported. Practically speaking, your application
may wait (without consuming CPU) until the contents of a particular
memory location changes. This memory location is checked whenever a
task-switch occurs.
What to do with it? Well, I came up with this type of event as I
designed a special application. I had to realize a task that emptied
a ring-buffer which was filled by an interrupt routine. Why not use a
semaphore, you might ask! - Too slow! There was just enough time to
insert the characters received into the ring-buffer. On the other
hand the ring-buffer had to be emptied as quickly as possible, again
for the reasons of speed.
A high-priorized subtask permanently looping, keeping an eagle eye on
the ring-buffer is far from beeing a solution - it simply blocks the
whole system. OK, lets try to have the subtask sleep for a while,
take a look at the buffer and sleep again if no characters have
arrived. Well, in the meantime a buffer-overflow has occured...
I suppose, you now understand why the event has been born. Defining
an event that monitored the buffer's tail-pointer, I kept my
high-priority subtask waiting for the buffer to be filled without
blocking the whole system. Awakend very shortly after a change of the
tail-pointer signalizes incoming characters, it busily processed the
buffer contents and went to sleep again. - Works fine!
Inter Process Communication (IPC)
As mentioned earlier, the Multi-Tasking Subsystem provides for
various methods to exchange data between separate tasks and to
synchronize processes executing simultaneously.
I do not intend to go into detail about the underlying concepts,
because there are lots of books available in the market, which discuss
these topics much better than I could do here in this document. Again
I recommend the study of "Operating Systems Design and Implementation"
written by Andrew S. Tanenbaum.
A. Message Passing
The system-calls supporting message passing are quite
straightforward.
A task that wants to send a message to another task in the system
primarily has to know the task-id of the process it would like to
send to. Then it passes its message-buffer and the receiver-id to
the appropriate system-call. An additional parameter tells the
kernel, whether the sender wants to wait if the receiver is not
ready to receive the message or whether the send-request should be
rejected.
A task is ready to receive, whenever it executes a receive-system-
call. It may specify the task-id of the process it wants to receive
from or pass a don't-care code to the system-call, which causes the
kernel to pass through any message sent to this task.
Again, the task may wait until a message arrives or have the system
reject a receive system-call whenever no message is available.
B. Semaphores
There has been written a lot about this topic in the past. Let's have
a look at the internal represetation only at this point.
A semaphore as defined by the Multi-Tasking Subsystem, consists of a
two-byte signal count, which is set to 1 at the time of creation,
and a task-queue to which the processes waiting for this semaphore
are appended.
The busy-state is indicated by a signal-count of zero; any non-zero
value indicates a free-state. Whenever a wait system-call is executed
and the corresponding semaphore is currently busy, the requesting
task is suspended until another process releases this semaphore
through execution of a signal system-call.
Whenever a signal system-call is executed and the task-queue of the
corresponding semaphore is not empty, the first task of this queue
is made computable, otherwise the signal-count is incremented.
Despite the internal structure, tasks refer to semaphores through
untyped pointers. The task creating a semaphore receives this
pointer on return from the system-call and uses it as a handle
furtheron. The actual number of semaphores in the system is
delimited by the amount of free heap space only.
Critical Sections
You may use the standard functions/procedures supplied with your
compiler freely inside your tasks. BUT - procedures like WriteLn are
not designed to work in multi-tasking environments, that means, they do
not protect their critical sections from beeing entered more than once
at a time.
An example will help to understand what I try to explain: Lets think
of two equaly priorized tasks which spend their lives WriteLn-ing
strings to the terminal screen. The process of bringing a bunch of
characters to the screen is much more complicated than the simple
WriteLn-statement makes believe. Therefore it is likely that
sometimes both tasks will be preempted while in the middle of
performing the screen-write. As a consequence the screen output
becomes clobbered.
Imagine the effects of non-synchronized execution of GetMem and
FreeMem...
Another situation in which you have to block a certain section of code
is a routine, that modifies global data or performs some closely
connected actions that form an undivisible unity (atomic action).
Let me give you an example: Provided you got two tasks executing
simultaneously, which at some point of execution output a character to
the screen at a certain position. This is done by executing a GotoXY,
followed by a Write. If the first task was interrupted after having
executed the GotoXY but before having output the character, the second
task might move the cursor to another location on the screen, which
indeed wouldn't lead to the results desired.
There are two basically different approaches to solve this problem.
First you could force exclusive CPU-access during the execution of
your critical section. This solution is only applicable for very
small portions of code, a GotoXY/Write-sequence for example, because
a CPU-bind blocks any scheduler function including the sleep-queue
and timer management.
In most cases a protection scheme using semaphores is preferable.
You can find an example for this kind of process synchronisation in
the example program PRO_CON.PAS, routines RBuffGet and RBuffPut. A
section of code protected by a semaphore, may only be entered by one
task at a time. Any task trying to enter the critcal section whilst
another process is currently inside is suspended until the latter one
has left the protected area.
Global Type- and Data-Definitions
Global Type-Definitions
Priority = (Pri_Nice, Pri_User, Pri_Kernel);
This type-definition is used to describe the priority-level at
which a task is to be executed.
WaitFlagType = (Wait, NoWait);
Passed as a parameter to the system-calls concerned with
message-passing, this type indicates whether a process intends to
wait for a) a message to be received or b) a message available to
be received.
TaskReturn = (Task_Crashed, Task_NotFound, Task_NoMsg,
Task_NotReady, Task_OK, Task_Invalid);
The system-calls concerned with message-passing return one of these
codes to the calling process.
Task_Crashed...: A task was addressed, which has been disabled
following a stack-overflow condition
Task_NotFound..: The task-table entry indexed by the task-id
passed to the executed system-function is
out of range or currently empty
Task_NoMsg.....: There is no message available (Receive with NoWait)
Task_NotReady..: The receiver is not waiting for a message (Send
with NoWait)
Task_OK........: Fine, that's it
Task_Invalid...: An invalid task-id was passed to the
system-function
TaskStatus = (Running, Ready, Sleeping, Available, Crashed,
Waiting, Sending, Receiving, Suspended);
Return value returned by GetTaskState.
SemReturn = (Sem_NoSpace, Sem_NotFree, Sem_OK, Sem_Invalid);
Some system-calls concerned with semaphore-handling return a value
of type SemReturn.
Sem_NoSpace..: There is no heap space available
Sem_NotFree..: You have tried to delete a semaphore whose task-queue
is not empty
Sem_OK.......: Everything's allright
Sem_Invalid..: You passed an invalid semaphore-handle (NIL)
TaskNoType = Integer;
Higher level representation of the task-id. I decided to redefine
the type integer to be able to change this definition in a later
version without having to think about the program code.
BPtr = ^Boolean;
Definition required by GetTimBusy.
Global Constants
Task_NoSpace
If task creation fails, because there is not enough free dynamic
memory available to allocate a private stack, CreateTask will return
this value instead of the task-id.
Task_NoSlot
If task creation fails, because the maximum number of simultaneously
active tasks is reached, CreateTask will return this value instead
of the task-id.
StateText
StateText is an array indexed by a value of type TaskStatus, which
contains a textual representation of every possible task state.
AnyTask
Don't-care-value, passed to the receive system-function whenever a
process doesn't care from whom it is going to receive a message.
Global Variables
InInt28 (Boolean)
Whenever COMMAND.COM is waiting for input, it permanently issues
an INT 28H (DOS multi-tasking interrupt). MtPopUp for example
catches this interrupt and sets InInt28 to TRUE whilst inside the
interrupt-handler. This informs the kernel know that a task-switch
may safely performed although the DOS critical-flag is set.
Internal Type- and Data-Definitions
Internal Type-Definitions
CSDataType = RECORD
TimBusy : Boolean;
Old08 : Pointer;
ISRStat : Byte;
END;
CSDataType describes a small status-area located in the code
segment of the assembly language module.
"TimBusy", the timer-busy flag, is set to a non-zero value
whenever the interrupt-service routine handling timer-tick
interrupts is currently executing. This prevents reentrance, not
allowed in this case.
If you block the CPU by executing BindCPU, this flag is also set
to simulate a timer-busy condition.
"Old08" contains the interrupt-vector pointing to the original
interrupt-handling procedure.
There are various interrupt-handlers that must not be disturbed by a
task-switch (for example the disk-I/O vector 13H). The byte
addressed by "ISRStat" is used to store a number of interrupt-busy
flags, one for each interrupt that may not be interrupted. By
entering the corresponding interrupt-handling procedure, the flag is
set to 1 and reset on return. At task-switch may only be performed,
if both, the critical-flags and the ISRStat, contain zero.
TaskPtrType = ^TaskDescType;
Pointer to a task-table slot.
SchedRecType = RECORD
First : TaskPtrType;
Last : TaskPtrType;
END;
The anchor of a task-queue. Such task-queues are represented
internally as forward-chained list structures. The scheduler-queues
for example are task-queues that contain all tasks in the system
that are computable or running at the moment. The first element of
the queue is addressed using the pointer "First", the tail of the
queue may be found by following the pointer "Last". This enables
the kernel to quickly append new elements to the end of the queue.
TaskDescType = RECORD
NextTask : TaskPtrType;
TaskNo : TaskNoType;
SaveArea : SaveAreaType;
Status : TaskStatus;
RunLevel : Priority;
Stack : PointerType;
StackSize : Word;
CPU : LongInt;
TimeSlice : Integer;
DelayCount: LongInt;
Senders : SchedRecType;
RecFrom : TaskNoType;
RecBuff : Pointer;
END;
Here you see the structure of a so-called task-descriptor, which
makes up one slot of the task-table.
"NextTask" is used to chain the tasks in a queue together. The
queue is terminated by a NIL value contained in the NextTask field.
The task-id is stored in the field named "TaskNo". This value is
identical to the slot-number of the corresponding task-descriptor.
Quite a number of system-calls need this value to index into the
task-table when manipulating the contents of a particular task-
descriptor.
As you already know, every task possesses a private stack. The
actual top of stack as represented by the registers SS and SP, is
stored in the structure named "SaveArea". The actual size and
location of this stack are stored in fields named "Stack" and
"StackSize". The kernel checks for a stack-overflow condition
whenever a task-switch is performed.
"Status" contains the acutal state of a task. If this field
contains the value "Available", the task-table slot is empty and
may be assigned to a newly created task.
The field indicating a task's priority is named "RunLevel". The
contents of this field is used to select the appropriate
scheduler-queue.
"TimeSlice" is used to keep the number of timer-ticks already used
up during the current timeslice. This value might become negative
due to dynamic scheduling.
The last field concerned with time-sharing is named "DelayCount".
Whenever a task goes to sleep for a while, this field is filled
with the number of ticks the task intends to stay asleep. It is
decremented on every timer-tick until the value zero is reached.
Than the task is re-scheduled.
The remaining fields in the task-descriptor are concerned with
message-passing. "Senders" is the anchor of a task-queue
containing the tasks wishing to send to this process.
"RecFrom" is used to indicate which task a process wants to
receive from on the one hand and to hold the task-id of the sender
after having successfully received a message on the other hand.
The latter function is important when receiving messages using the
don't care code "AnyTask". The receiver will not know in advance
from whom he will receive a message. He may find it out by
questioning the contents of "RecFrom".
"RecBuff" points to the message-buffer.
Semaphore = RECORD
Signals : Word;
WaitQueue : SchedRecType;
END;
Here we have the internal representation of a semaphore. The
signal-count is stored in "Signals" and the tasks kept waiting for
the semaphore to become free, are chained to the anchor WaitQueue.
EventType = RECORD
MemLoc : Pointer;
OrgValue : Byte;
Task : TaskNoType;
END;
We have already heard about events and what they are for.
Here is how they are stored internally.
"MemLoc" points to the memory-location to monitor.
"OrgValue" contains the original contents of the byte at MemLoc^.
"Task" finally is the task-id of the requesting task. It is used
to index into the task-table when re-scheduling the task.
Right before re-appending the task to its scheduler-queue, the
kernel checks whether it is still alive!
TimerType = RECORD
MemLoc : Pointer;
Ticks : Word;
Task : TaskNoType;
Next : TimerPtr;
END;
This is how timers are stored. Whenever a timer is created, the
necessary memory is allocated on the heap. Than the timer is
inserted into the TimerQueue. The single timers contained in this
queue are chained through their "Next" pointers. A NIL-pointer
terminates the timer-queue.
"Ticks" is used to store the number of timer-ticks to wait until
the timer expires.
"MemLoc" points to a memory location whose content is incremented
as soon as the timer expires. Before the kernel actually increments
the byte at "MemLoc", it checks whether the task is still alive.
If not, no action is taken for not to destroy a memory location
that may already belong to another task's private address-space.
"Task" again holds the task-id of the requesting process, which is
used to index into the task-table.
PointerType = RECORD CASE Integer OF
1: (P : Pointer);
2: (POfs : Word;
PSeg : Word);
END;
Split up a pointer in its segment and offset components.
Internal Constants
TicksPerSecond = 18;
This constant is used to describe the number of timer-ticks that
make up a second. You could easily increase the number of
task-switches by using the RTC of an AT-computer for example. If
after the change this constant reflects the new number of
interrupts per second, any program using Seconds() to calculate
its sleeping time, will not have to be adapted.
MaxEvents = 10;
"MaxEvents" determines the maximum slots in the event-table. It is
recommended to use the smallest possible value to keep the amount
of time needed to search this table as small as possible.
Quantum : Byte = 2;
MaxDynQuantum : Byte = 10;
Simply default values that may be changed by means of the
TimeSlice function.
MaxTasks = 50;
This value determines the maximum number of slots in the
task-table.
SafetyMargin = 20;
Whenever the kernel checks for stack-overflow conditions, it
assumes a margin of "SafetyMargin"-bytes from the highest possible
stack-address to be "untouchable" and issues a stack-overflow
message if any byte in this area is used by the corresponding task.
Internal Variables
TaksTable : ARRAY[1..MaxTasks] OF TaskDescType;
The root of all evil - the task-table.
CurrentTask : TaskPtrType;
This pointer is used to hold the address of the task-descriptor
belonging to the currently running task. It may only be
manipulated by the scheduler.
TaskQueues ARRAY[Pri_Nice..Pri_Kernel] OF SchedRecType;
These are the anchors of the various scheduler-queues.
SleepQueue : SchedRecType;
Here your tasks may sleep for a while....
EventTable : ARRAY[1..MaxEvents] OF EventType;
Whenever an EventWait is issued, a slot of this table is allocated
for the requested event. The number of table entries available
reflects the maximum possible number of simultaneously active
events.
ActiveEvents : Byte;
To be able to skip event-checking, the kernel counts the number of
currently active events. Whenever this field contains zero, no
check for events is performed.
TimerQueue : TimerPtr;
This is the head of the timer-queue. If the pointer contains NIL,
no timers are currently active.
InDosFlag : PointerType;
DosCritical : PointerType;
DOS uses two memory-locations to store flags that contain a
non-zero value whenever DOS is not in a consistent state. The
pointers named above are used to hold the addresses of those
memory-locations.
CSDataPtr : ^CSDataType;
This pointer holds the address of a small status area located in the
code-segment of the assembly language module.
Reference Section (alphabetically sorted)
BindCpu
Declaration
PROCEDURE BindCPU;
Purpose
BindCPU is used to bind the CPU to the currently running process
temporarily. No task-switch will be performed an no other
scheduler activities take place until the CPU is released by a
ReleaseCPU call.
Example
...
BindCPU; {gain CPU exclusivly}
GotoXY(1,10); {atomic action}
Write('*');
ReleaseCPU;
...
Remarks
BindCPU blocks the whole kernel! While the CPU is bound, no
event-checks, no timer-countdown and no sleep-queue handling are
performed.
You should only use BindCPU to protect VERY small portions of
code. In most cases, the use of semaphores is recommended
See Also
ReleaseCPU
CancelTimer / CreateSem
Declaration
FUNCTION CancelTimer(MemPtr:Pointer):BOOLEAN;
Purpose
If there exists a timer that will cause the memory location at the
address "MemPtr" to become incremented, it will be removed from the
system. CancelTimer returns TRUE, if a timer could be found,
otherwise FALSE is returned.
Example
VAR Timeout : Boolean;
Ok : Boolean;
...
Timeout := False; {initialize}
IF QueueTimer(@Timeout,Seconds(1)) {create Timer}
THEN BEGIN
REPEAT {wait}
... {some action}
UNTIL Timeout OR Ok;
IF CancelTimer(@Timeout) THEN; {Kill Timer}
... {some more action}
END;
See Also
QueueTimer
Declaration
FUNCTION CreateSem(VAR SemPtr:Pointer):SemReturn;
Purpose
CreateSem allocates a semaphore on the heap and returns a pointer
that functions as a handle in further system-calls referring to
this semaphore. The handle is placed into the variable SemPtr.
The return value of this system-function indicates whether the
action could be completed successfully.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error;
See Also
Typedefinition SemReturn, RemoveSem
CreateTask
Declaration
FUNCTION CreateTask(TaskPointer:Pointer;Level:Priority;
BytesStack:Word):TaskNoType;
Purpose
This function is used to create a subtask.
The address of the procedure to be activated as an independently
running task is to be passed in "TaskPointer".
"Level" describes the desired priority level.
Finally, the parameter "BytesStack" contains the desired size of
the task's private stack in bytes.
CreateTask returns the task-id or a negative value indicating an
error-condition.
Example
PROCEDURE SubTask;
{
I am a task that beeps every second
}
BEGIN {SubTask}
REPEAT { Body }
Sleep(Seconds(1)); { }
Sound(1000); { ACTION!! }
Delay(20); { : }
NoSound; { }
UNTIL False; { Body }
END; {SubTask}
...
...
BEGIN {Main}
IF CreateTask(@SubTask,Pri_User,300) < 0
THEN Error;
...
END. {Main}
Remarks
With respects to programming, a task is realized as a Pascal
procedure without parameters, whose body has to be an infinite
loop. You must NEVER leave the body of a task except by executing
a Terminate system-call.
A task is allowed to have local variable definitions, you will
have to take care however, that the private stack is properly
dimensioned to be able to hold the local data. You will have to
add the amount of memory needed to hold the local variables to
the bytes stackspace the task needs at runtime.
YOU are responsible for passing a valid stack size!!!! This
parameter is not checked!!
See Also
Typedefinitions TaskNoType and Priority, Global Constants,
Terminate
DoShutDown
Declaration
PROCEDURE DoShutDown;
Purpose
Shutdown the Multi-Tasking Subsystem. All interrupt vectors
captured are restored to their original state.
Remarks
This procedure is called internally through the exit-procedure of
the subsystem-unit.
DumpTaskTable
Declaration
PROCEDURE DumpTaskTable;
Purpose
Display the actual state of all active tasks in the system.
Example
Nr. Status Prioritt Slice Delay Stack free CPU
1 Ready Pri_Nice 2 0 205 18
2 Waiting Pri_User 3 0 -1 1
3 Running Pri_Kernel -1 0 221 1
4 Waiting Pri_User 2 0 321 5
Remarks
The current content of your display are overwritten. In case you
wish to realize a status-window, you should replace DumpTaskTable
with a routine of your own.
See Also
Global Constants StateText, GetTaskState
EventWait / GetPID
Declaration
FUNCTION EventWait(MemPtr:Pointer):Boolean;
Purpose
EventWait causes your task to be suspended until the contents of
the memory-location pointed at by "MemPtr" changes.
If no empty event-table entry can be found, EventWait returns
FALSE.
Example
VAR HeadPointer : Word; {Bufferpointer }
TailPointer : Word; { " }
...
IF NOT EventWait(@TailPointer) {wait for a change}
THEN Error {of the pointer}
ELSE DoJob;
...
Declaration
FUNCTION GetPID:TaskNoType;
Purpose
By calling GetPID, a task can find out its task-id.
See Also
Typedefinition TaskNoType
GetTaskState / GetTimBusy
Declaration
FUNCTION GetTaskState(Task:TaskNoType):TaskStatus;
Purpose
GetTaskState returns the current status of the task whose task-id
was passed to the function.
Example
This task displays its own state on the screen:
Writeln(StateText[GetTaskState(GetPID)]);
See Also
Typedefinitions TaskStatus and TaskNoType, Constant StateText
Declaration
FUNCTION GetTimBusy:BPtr;
Purpose
GetTimBusy returns the address of the timer-busy flag which
contains a non-zero value whenever the interrupt-handling
procedure for the INT 8 is active.
Sometimes an external interrupt-handler needs to know, whether
the scheduler is currently busy.
No system-call must be executed whilst the busy-flag is set.
See Also
Typedefinition BPtr
QueueTimer / ReadySuspended
Declaration
FUNCTION QueueTimer(MemPtr:Pointer; Counter:Word):BOOLEAN;
Purpose
Creates a timer that expires after "Counter" ticks and increments
the contents of the memory-location pointed at by "MemPtr".
If there is not enough heap space available to allocate a timer,
QueueTimer will return FALSE, otherwise TRUE is returned.
Example
VAR Timeout : Boolean; {Timeout marker}
...
Timeout := False; {initialize }
IF NOT QueueTimer(@Timeout,Seconds(1)) {Timeout after 1 sec}
THEN Error;
...
IF CancelTimer(@Timeout) {Remove Timer }
THEN ; {if still existent}
...
See Also
CancelTimer
Declaration
FUNCTION ReadySuspended(Task:TaskNoType):BOOLEAN;
Purpose
ReadySuspended is used to re-enqueue a task into its
scheduler-queue wich has been suspended using the Suspend
system-function.
If the action could be completed successfully, the value TRUE is
returned. A return value of FALSE indicates that the task addressed
was not currently suspended.
See Also
Suspend
Receive
Declaration
FUNCTION Receive(FromTask:TaskNoType; MsgBuff:Pointer;
WaitFlag:WaitFlagType):TaskReturn;
Purpose
By executing a Receive system-call, a task can receive a message
from another task in the system.
The first parameter, "FromTask", indicates from which task the
caller would like to receive. If "FromTask" contains "AnyTask",
any message is passed through, otherwise only messages sent by
the process whose id matches "FromTask" are received.
"MsgBuff" points to the start of a message buffer into which the
received message is copied. The kernel does not check whether the
message received fits into the message buffer - it simply
copies!
If the caller passes a "WaitFlag" of value "Wait", it will be
suspended until a message arrives. Otherwise the kernel
immediately returns control to the caller if no message is
currently available.
Example
VAR Puffer : String; {Message buffer}
...
IF Receive(AnyTask,@Puffer,Wait) <> Task_Ok
THEN Error
ELSE Writeln(Puffer);
...
Remarks
Using the don't care task-id "AnyTask", you could realize a task
that spends its life waiting for messages from its environment.
If you had to monitor some sensors, for example, you could have a
separate task for every sensor to be monitored. Whenever some
action has to be taken, the corresponding monitor-task sends a
message to a central workhorse that performs the necessary
adjustments.
See Also
Constant AnyTask, Typedefinition TaskReturn, Send, ReceivedFrom
ReceivedFrom / ReleaseCPU
Declaration
FUNCTION ReceivedFrom:TaskNoType;
Purpose
By calling ReceivedFrom, a task can find out, who was the sender
of the last message received.
See Also
Typedefinition TaskNoType, Constant AnyTask, Receive
Declaration
PROCEDURE ReleaseCPU;
Purpose
Provided you have bound the CPU by executing a BindCPU, you will
have to reenable scheduling through execution of a ReleaseCPU.
Example
...
BindCPU; {disable scheduler}
GotoXY(1,10); {atomic action}
Write('*');
ReleaseCPU; {...set them free...}
...
See Also
BindCPU;
RemoveSem / Sched
Declaration
FUNCTION RemoveSem(VAR SemPtr:Pointer):SemReturn;
Purpose
RemoveSem physically removes a semaphore. The formerly occupied
heap space is returned to the memory pool.
"SemPtr" must be a valid semaphore-handle as returned by the
CreateSem system-call.
RemoveSem returns a value of type SemReturn that indicates
success or failure.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error;
...
IF RemoveSem(Semaphore) <> Sem_Ok
THEN Error;
...
Remarks
The system mostly is not able to prove whether a valid handle is
supplied. It is your job to take care of this.
You cannot remove a semaphore, whose task-queue currently is not
empty.
See Also
Typedefinition SemReturn, CreateSem
Declaration
PROCEDURE Sched;
Purpose
Initiate a task-switch, give up the current timeslice.
"Sched" is used internally by many system-functions. If you want
to have your task give up its timeslice, you should better use
Sleep(1).
Seconds / SemClear
Declaration
FUNCTION Seconds(Sec:Word):LongInt;
Purpose
"Seconds" returns the number of timer-ticks that make up a second.
It is recommended that you use this function whenever you need to
specify a special amount of time. Use of Seconds makes your
application independent of changes to the task-switch rate.
Example
IF QueueTimer(@Timeout,Seconds(1) SHR 1) { timout after }
THEN; { half a second }
See Also
QueueTimer, CancelTimer
Declaration
PROCEDURE SemClear(SemPtr:Pointer);
Purpose
Reset a semaphore's signal-count to zero.
Example
VAR Semaphore : Pointer; {Handle}
...
IF CreateSem(Semaphore) <> Sem_Ok
THEN Error
ELSE SemClear(Semaphore);
Remarks
Internally SemClear is translated into SemSet(Semaphore,0).
See Also
SemSignal, SemWait, SemClearWait, SemSet
SemClearWait
Declaration
PROCEDURE SemClearWait(SemPtr:Pointer);
Purpose
Reset a semaphore's signal-count to zero and wait until another
task initiates a SemSignal system-call for this semaphore.
In contrast to SemWait, SemClearWait ALWAYS leads to the calling
task beeing suspended.
Example
VAR Ready : Pointer; {Semaphore }
...
PROCEDURE SubTask;
{
I am a task that needs to save the values of some global
variables during initialisation. The main program, however,
changes these variables during the process of its execution.
Therefore, the main program has to wait until I indicate that I
have transferred the values I need to my local data area.
}
BEGIN {SubTask}
... {initialize }
SemSignal(Ready); {O.K. I'm ready }
REPEAT
... {Body }
UNTIL False;
END; {SubTask}
...
...
BEGIN {Main}
...
IF CreateSem(Ready) <> Sem_Ok {Create a semaphore}
THEN Error;
IF CreateTask(@SubTask,Pri_User,300) < 0 {Create a task }
THEN Error
ELSE SemClearWait(Ready); {wait for O.K. }
...
END. {Main}
See Also
SemClear, SemSet, SemSignal, SemWait
SemCut / SemGetSignals
Declaration
FUNCTION SemCut(SemPtr:Pointer;Task:TaskNoType):BOOLEAN;
Purpose
If the task whose id is passed as "Task" currently is waiting in
"SemPtr"'s task-queue, it is removed from this queue.
SemCut returns TRUE, if the action could be completed successfully.
Remarks
SemCut is a somewhat brutal function! It forces an innatural
action and was added for a special application purpose in MtPopUp.
PLEASE do use it with care!! - Better keep away from it.
See Also
SemPaste
Declaration
FUNCTION SemGetSignals(SemPtr:Pointer):Word;
Purpose
Returns the signal-count of the semaphore "SemPtr".
Example
FUNCTION SemBusy(S:Pointer):BOOLEAN;
{
This function checks whether a semaphore is currently busy.
}
BEGIN {SemBusy}
SemBusy := (SemGetSignals(S) = 0); {Signal-Count = 0}
END; {SemBusy}
See Also
SemSignal, SemWait, SemClear, SemClearWait, SemSet
SemPaste / SemSet
Declaration
PROCEDURE SemPaste(SemPtr:Pointer; Task:TaskNoType);
Purpose
SemPaste unconditionally appends "Task" to the task-queue of the
semaphore referred to by "SemPtr". No validity checks are performed!
Remarks
SemCut is a somewhat brutal function! It forces an innatural
action and was added for a special application purpose in MtPopUp.
PLEASE do use it with care!! - Better keep away from it.
Declaration
PROCEDURE SemSet(SemPtr:Pointer; Count:Word);
Purpose
SemSet sets the signal-count of a semaphore to a definite value.
This action does not have any influence on tasks that might be
waiting in the task-queue belonging to this semaphore.
Example
CONST Elements = 100;
VAR Buffer : ARRAY[1..Elements] OF Byte; {Buffer}
Full : Pointer; {No. of used slots}
Empty : Pointer; {No. of empty slots}
...
BEGIN {Main}
...
IF CreateSem(Full) = Sem_Ok
THEN SemClear(Full); {no slot used}
IF CreateSem(Empty) = Sem_Ok
THEN SemSet(Empty,Elements); {all slots free}
...
END. {Main}
See Also
SemSignal, SemWait, SemClear, SemClearWait
SemSignal / SemSoWaiting
Declaration
PROCEDURE SemSignal(SemPtr:Pointer);
Purpose
If the task-queue of the semaphore referred to by "SemPtr" is
currently empty, the signal-count will be incremented. Otherwise
the first task waiting is re-scheduled and the signal-count remains
unchanged.
Example
VAR Critical: Pointer;
...
IF SemCreate(Critical) <> Sem_Ok
THEN Error;
...
SemWait(Critical);
...
{critical section}
...
SemSignal(Critical);
...
See Also
SemWait, SemClearWait, SemClear, SemSet
Declaration
FUNCTION SemSoWaiting(SemPtr:Pointer):BOOLEAN;
Purpose
SemSoWaiting returns TRUE, if there are tasks waiting for
"SemPtr" to become available.
See Also
SemWait, SemSignal, SemClear, SemClearWait, SemSet, SemGetSignals
SemWait / Send
Declaration
PROCEDURE SemWait(SemPtr:Pointer);
Purpose
If a semaphore's signal count currently is greater than zero, it
will be decremented. Otherwise the caller is suspended and
appended to the task-queue of this semaphore.
It remains suspended until another task issues a SemSignal for
the semaphore it is waiting for.
Example
VAR Critical: Pointer;
...
IF SemCreate(Critical) <> Sem_Ok
THEN Error;
...
SemWait(Critical);
...
{critical section}
...
SemSignal(Critical);
...
See Also
SemSignal, SemClearWait, SemClear, SemSet
Declaration
FUNCTION Send(ToTask:TaskNoType; Msg:Pointer; MsgSize:WORD;
WaitFlag:WaitFlagType):TaskReturn;
Purpose
The system-function "Send" lets a task send a message to another
process in the system.
The parameter "ToTask" contains the task-id of the receiver. This
must be the task-id of a currently active task. You must not use
the constant "AnyTask" in this context.
"Msg" points to the start of the message-buffer whose size is
passed in "MsgSize".
Finally a wait-flag has to be supplied, that indicates whether
the sender wishes to wait until the receiver is willing to
receive the message. If you set "WaitFlag" to "NoWait", the
kernel will return control to your task immedeately if the
receiver is not waiting for a message.
Send / Sleep
Example
VAR Puffer : String;
...
IF Receive(AnyTask,@Puffer,Wait) <> Task_Ok
THEN Error
ELSE Writeln(Puffer);
...
Remarks
The message is physically copied to the message buffer of the
receiving process. This is not the best solution from the point
of view of performance, but is provided for the highest possible
independence of sender and receiver. They could theoretically be
located on different machines in a networking environment. If
you better like a pointer to a message to be passed, make your
pointer the message.
See Also
Typedefinition TaskReturn, Receive
Declaration
PROCEDURE Sleep(Ticks:LongInt);
Purpose
"Sleep" lets your task suspend itself for a certain number of
timer-ticks.
Example
Writeln('I''m going to sleep for 3 seconds!');
Sleep(Seconds(3));
Writeln('Hi, here I am again!');
See Also
Seconds
SetPri
Declaration
PROCEDURE SetPri(Pri:Priority);
Purpose
"SetPri" lets a Task change its priority at runtime.
Remarks
The new priority will come into effect at the end of
its current quantum. Therefore the task will remain in
control of the CPU, although it might have lowered its
priority.
Suspend / Terminate
Declaration
FUNCTION Suspend(Task:TaskNoType):BOOLEAN;
Purpose
Brutally suspend a task which is currently computable.
"Task" contains the id of the task to be suspended.
A task that has been suspended by a Suspend system-call can ONLY
be reactivated by a ReadySuspended system-call.
If the task addressed was not computable, Suspend would return
FALSE to the caller. Otherwise TRUE is returned.
See Also
Typedefinition TaskNoType, ReadySuspended
Declaration
PROCEDURE Terminate;
Purpose
A task may terminate itself by executing a "Terminate"-system-call.
The task-descriptor is marked available and the private stack is
returned to the memory pool.
See Also
CreateTask
TimeSlice
Declaration
PROCEDURE TimeSlice(Slice, DynQ:Byte);
Purpose
The width of a timeslice and the +/- maximum number of ticks to
credit/debit during dynamic scheduling, may be changed by issuing
a "TimeSlice"-system-call.
"Slice" describes the size of a timeslice in timer-ticks;
"DynQ" contains the number of ticks to credit/debit during
dynamic scheduling.
"Slice" defaults to 2; "DynQ" defaults to 10.
History
Version 1.30 / November 1988
- Task queues are now realized as doubled chained lists; this
increases speed when removing list elements
- Tasks beeing awakened by an event or returning from a message
passing operation, are now put to the front of their run level
task queue. Thus they will be selected by the scheduler earlier.
- A bug in the delta computation of QueueTimer has been corrected
- The scheduler stack is no more located in the data segment, but
has been moved to the heap
- The formerly local data type "PointerType" has been made public
- The assembly language module has become slightly optimized
- A new procedure "SetPri" has been added
December 21, 2017
Add comments