[net.micro.amiga] Amiga Multitasking Tutorial

ewhac@well.UUCP (Leo 'Bols Ewhac' Schwab) (04/09/86)

[ #ifdef LINE_EATING_BUG   eat_this_line();  #endif ]

Hello again,

	I posted this to the WELL Amiga conference and Programmer's
Network and so far, response has been favorable.  I haven't had anyone
scream at me for being totally wrong, so I suppose everything works.

	So here's a tutorial on how to deal with multitasking under the
Amiga Exec.  If you find any bugs in the tutorial or the accompanying
source, let me know; I don't want to be labelled a bogon.

					Leo L. Schwab

--------Tutorial follows---------

                        Multitasking on The Amiga


        This tutorial was written by Leo L. Schwab, and was originally
posted on the Programmer's Network and Amiga Conference on the WELL.
Permission is hereby granted to freely distribute this tutorial provided
credit is given to the original author (I want to be famous, you see :-).


Foreword
--------

        John Draper suggested that one way to become well-known is to write
knowledgeable tutorials to help fellow programmers understand the system.
This, then, is my attempt to tell people how to deal with basic multitasking
on the Amiga.

        This tutorial will describe multitasking on the Exec level, not the
DOS level.  As a personal preference, I prefer to avoid the DOS whenever I
can and go straight to the Exec.  This does not cause undue difficulty, and
in fact reduces overhead and headaches.

        This tutorial will deal mostly on the practical level, and will
assume you know the basic concepts behind a multitasking environment.  Thus,
this tutorial will use a cookbook approach to help you through getting your
multiple tasks running and talking to each other.

        So get out your favorite C compiler and follow me.....


Introduction
------------

        The first thing you should do when approacing the Amiga exec is to
forget everything you know about any other kind of multitasking operating
system you're familiar with.  Put it out of your mind.  Ignore it.
Particularly UNIX.

        I say this because trying to use your knowledge of your favorite
multitasking operating system is not going to help you much when you start
dealing with the Amiga exec.  The exec is far and away vastly different from
just about every other multitasking environment, with the possible exception
of XINU (I mention XINU because there's a good book out on it, and many of
the concepts in that book can be applied to the Amiga exec).

        The exec is, in my view, a bare-bones multitasking environment.
There is just enough in the exec to do everything you need, although it may
not do everything you want.  It won't automagically deallocate resources
you've opened, and it's handling of runaway programs is not what many
programmers would like.

        By contrast, however, the exec is small and compact.  It's written
in extremely tight machine code.  Because it's small and quick, and doesn't
try to "do things behind your back," it is a real-time operating system, and
can respond to interrupts and signals rather efficiently.

        So, in exchange for having to do everything yourself, you get a very
efficient multitasking executive.


Getting Started
---------------

        A task, in the exec's eyes, consists of two parts:  A program
segment residing somewhere in memory, and a task control block which is
part of a linked list of tasks managed by the exec.

        The program segment can be anything and anywhere.  One particular
way of getting a program segment might be to declare a function in C:

-----
subtask ()
{
        ...
}
-----

        The task control block is an object that describes your task to the
exec.  In particular, where your stack is in memory, where the stack pointer
is, where the program counter should be, what signals your task has
received, which signals it's waiting on, etc.  Every task must have a task
control block.

        In addition, all tasks require a stack.  Even if you're not doing
any subroutine calls or have any local variables, you are still required to
have a stack so the exec can save your registers and return address if it
should perform a context switch.  The absolute minimum size your stack can
be is 70 bytes (q.v. RKM vol 1, p. 1-17,1-18).  A nice round number for a
stack size is 1K.  Personally, I prefer to specify a stack of 2K just to be
sure I have enough space for my variables and any subroutines I may call.
The stack can be allocated out of the free memory pool using AllocMem().
It is recommended (but not neccesary) that you not declare the memory public
i.e. don't specify the MEMF_PUBLIC flag when calling AllocMem().


Calling A Task Into Existence
-----------------------------

        The exec support library provides a rather nice function for the
purpose of creating tasks, called (oddly enough) CreateTask().

        CreateTask() performs only the most basic of task initialization
steps.  It first allocates a chunk of memory for both the stack and task
control block.  It then initializes the tc_SP{Upper,Lower,Reg} fields in the
structure, and the tc_Node.ln_{Pri,Type,Name} entries in the node structure.
Finally, it calls AddTask() to add your task to the system, and returns you
a pointer to the task control block.  If anything goes wrong during this
Process, it returns a null, and doesn't allocate anything.

        The calling format for CreateTask() is as follows:

struct Task *CreateTask (name, pri, initPC, stacksize)
char *name;
UBYTE pri;
APTR initPC;
ULONG stacksize;

        "name" is a pointer to a string that is the name of your task.  This
is used if another task wants to find your task.

        "pri" is your task's priority.  This is a signed byte from -128 to
+127.  0 is the canonical priority for new tasks.

        "initPC" is the starting value for your program counter.  Generally,
this would be a pointer to a function, or some such thing.

        "stacksize" is how big a chunk of memory you want allocated for your
stack and task control block combined.  CreateTask() allocates (stacksize)
bytes of memory, then uses the first (sizeof (struct Task)) bytes for the
task control block, and the rest is used for the actual stack.  This is
important to remember if you want a specific stack size.

        The source code to CreateTask() can be found in the RKM vol. 2 a few
pages past page E-78, in Appendix F.

        A typical call to CreateTask might look like this:

-----
struct Task *tsk;
extern void function();
if (!(tsk = CreateTask ("Foo", 0, function, 2048)))
        printf ("Couldn't create task\n");
-----

        Note that CreateTask does only the basic initialization of the task
control block.  If you are doing anything special, like exception
processing, or interrupts, you can't use CreateTask; you'll have to cook up
your own.  The source code in the RKM provides a good template for writing
your own custom task creator should you need one.


Getting Rid Of Tasks
--------------------

        If you spawn a subtask, you'll probably want to kill it off
eventually.  This is a bit tricky, but not too terrible.

        Firstly, if your subtask allocated any resources, it must be certain
to close them before you try to remove it.  Remember, the exec does not
perform resource management (at least not to the same level as in other
operating systems); you have to free up everything yourself.  This could
probably be accomplished by sending your subtask a message asking it to kill
itself.  Once it gets this, it goes about closing all the stuff it may have
opened before "exiting."

        If you use CreateTask(), you can use the complementary function
DeleteTask() to get rid of a task you've spawned.  DeleteTask() performs a
RemTask() on the task in question, then frees up the memory it allocated
earlier for the stack and task control block.  A typical call to
DeleteTask() might look like this:

-----
struct Task *tsk;
DeleteTask (tsk);
-----

        "tsk" is the pointer to the task control block you got when you
called CreateTask().

        Now pay attention, because this next bit is important.  If you
intend to use DeleteTask() on a task to get rid of it, *you must never let
that task exit on its own*.

        For example, if you have declared a function to be treated as a
task, you must *never* let the function return.

        The reason for this is because of the action taken by the exec when
a task exhausts its stack (that is, has exited on its own).  Unless you've
specified a special clean-up function when you called AddTask() (which you
can't do with CreateTask()), the default action taken by the exec is to
remove the task from the system task list, by calling RemTask().

        DeleteTask() also calls RemTask().  Calling RemTask() on an already
removed task is deadly.  If you try and do this, the system may or may not
crash.  If it does crash, you may or may not get Guru Meditation.  If you do
get Guru Meditation, it will probably be for something completely unrelated,
most likely a memory problem.  If, on the other hand, the system doesn't
crash, it may crash later when you try to start a different program.
RemTask()ing removed tasks is a sure way to make your life miserable.

        There is, however, nothing wrong with calling RemTask() on a task
that is currently active.  If you get the CPU, you can call RemTask() on any
task in the system, and it will stop running and never be called again.
Once you've RemTask()ed it, it's up to you to deallocate its stack and
resources.

        Since it's a good idea to let tasks deallocate their own resources
(except their stack, that's up to the task doing the RemTask()ing), a
typical pseudo-code sequence might look like this:

-----
if (RECEIVED_KILL_MESSAGE) {
        deallocate_resources ();
        return;
}
-----

        But we've already said that we can't let the task exit on it's own.
So the "obvious" approach would be to do this:

-----
if (RECEIVED_KILL_MESSAGE) {
        deallocate_resources ();
        while (1)
                ;
}
-----

        This is also deadly.  This creates a condition known as
busy-waiting, and it's something to be avoided on the Amiga.  Suppose a low
priority task wanted to kill off a high-priority task by sending it a
message.  If the high-priority task entered the busy-waiting loop outlined
above, the low-priority task would never get the CPU back (Remember, exec
context switching is not the same as UNIX.  Whoever has the highest priority
is the task currently running, regardless of how big a CPU hog it is).
Somehow, we've got to get the task to enter a wait state of some kind.  This
is the way I like to do it:

-----
if (RECEIVED_KILL_MESSAGE) {
        deallocate_resources ();
        Wait (0);       /*  Wait for Godot  :-)  */
}
-----

        When you call Wait(), you specify a mask which is the logical OR of
the signal bits you want to wake up on.  By specifying a mask of zero, no
signal condition will satisfy the mask.  So the task goes to sleep and never
wakes up again.  But the task is still in the system task list.  So at this
point it is safe to call DeleteTask().


Message Passing
---------------

        You might think message passing is a concept seperate from
multitasking.  But multitasking and message passing are so closely
intertwined in the Amiga exec that it would be incomplete to discuss one and
not the other.  As was discussed above, message passing is an effective way
to tell a task to go away.  So it seems appropriate to discuss it.

        To do message passing, you need two things:  A message port, and a
message.

        A message is simply a chunk of memory with data in it.  The first
part of the chunk is a node structure.  This is used to link messages in a
FIFO queue.  The rest of the memory generally contains a pointer to the
reply port, and the size of the message.

        A message port is a list header that the exec links messages on to.
The message port also contains other information, such as which task it
belongs to, which signal bit to assert when a new message arrives, and some
flags.

        The structure definition for messages and message ports can be found
in the RKM vol. 2 p. D-27.


Making Message Ports
--------------------

        The exec support library once again provides us with a convenient
port creation function, called CreatePort().

        CreatePort() first allocates a signal bit (using AllocSignal()),
then allocates memory for the port itself.  It then initializes the node
structure, and the mp_{Flags,SigBit,SigTask} fields.  Finally, depending on
whether or not you gave a name to the port, it either calls AddPort() (if
you gave it a name), or simply calls NewList() for the message list
structure (if you didn't give it a name).  The reason for this dual action
is in case you want to create a truly private message port.  If you don't
call AddPort(), other tasks will not be able to find your port by calling
FindPort().  This is useful if you're sure other parts of the system will
confuse your port with someone else's, or you simply don't want to receive
messages from other tasks that may try to interrogate you (junk mail?).

        The calling convention for CreatePort() is as follows:

struct MsgPort *CreatePort (name, pri);
char *name;
BYTE pri;

        "name" is a pointer to a null-terminated string that is the name of
your port.  This is so that other tasks can use FindPort() to get a pointer
to your port.

        "pri" is a priority.  I asked one of the Amiga people what use
priority on a message port was.  He explained that, since exec keeps
everything in lists sorted according to priority, "Why not ports, too?"
This does have a marginally practical value.  Since ports are sorted
according to priority, when you call FindPort() on a port that has a high
priority, it will still find the port, but it will find it quicker (since
it's closer to the head of the list).  As a rule, I suggest assigning ports
the same priority as the task owning it.


Making Messages
---------------

        All you need to make a message is a chunk of memory, so go get one
(I'll wait....).

        Got it?  Now put a message structure at the front of the block you
allocated.  After that, put anything you need.  A typical way of creating a
message might be like this:

-----
struct InformationPacket {
        struct Message message;
        << insert relevant data declarations here >>
} pack;
-----

        Remember, a message can contain any kind of data you like.  The one
thing exec expects to see is a message structure at the front.  It must be
an instance of a message, not a pointer to one.  Beyond that requirement,
exec doesn't care.

        One field of particular importance in the message structure is the
mn_ReplyPort, which will be discussed later.

        As an illustration, take a look at the IORequest structures (RKM
vol. 2 p. D-23).  See what the first thing is in each of them?  A message
structure.


Sending Messages
----------------

        In order to send a message, you must have a valid pointer to a
message port, and a pointer to a message.  When you have these, you pass
them to the exec function PutMsg().  The calling convention is as follows:

PutMsg (port, msg);
struct MsgPort *port;
struct Message *msg;

        "port" is a pointer to a message port that was obtained either by
calling CreatePort(), or (more likely) by calling FindPort().

        "msg" is a pointer to a message you've constructed, as outlined
above.

        Now pay attention, because this is another important bit.  PutMsg()
does not copy the message you're passing, but simply assigns the right to
use the memory to the task owning the port you're sending the message to.

        Let me rephrase that.  PutMsg() does not make a copy of your message
and pass the copy to the port you're sending to.  It gives the original
message to the port.

        Let's say you've allocated a block of RAM and are using it as a
message.  Then you call PutMsg(), passing a pointer to your block of RAM.
You now no longer own that block of RAM; you have passed ownership (and
therefore the right to use and modify the RAM) to the port you sent it to.

        This means that, once you've passed ownership to the receiving task,
you are *not* allowed to modify the message.  If you *really* know what
you're doing, you can still access the contents of the message, but this can
get you into trouble, as will be illustrated later.


Receiving Messages
------------------

        To receive a message, you must have a properly initialized message
port (generally made with CreatePort()).  If you have properly initialized
the mp_{SigTask,SigBit} fields of the message port structure (CreatePort()
does this for you), you will receive a signal when a message arrives at
your port.

        There are several ways to see if a message has arrived at your port.
The most basic of these is the exec function GetMsg().  It works like this:

msg = GetMsg (port);
struct Message *msg;
struct MsgPort *port;

        "msg" is a pointer to a block of RAM being used as a message.  The
task receiving the message should "know" how the RAM is structured so it can
read the data properly.  If a message is waiting, GetMsg() returns a pointer
to it and removes the message from the port.  If there are no messages on
your port, GetMsg() returns a null.

        "port" is a pointer to your message port that you are expecting
messages to arrive on.

        If you want your task to wait for a message to arrive before you do
anything else, you might try to do it like this:

-----
while (!(msg = GetMsg (port)))
        ;
-----

        This is the very deadly busy-waiting loop again, and you should
avoid it like the plague.  The proper way to wait for a message is like
this:

-----
msg = WaitPort (port);
GetMsg (port);
-----

        WaitPort() first checks to see if you have any messages, and if you
don't, goes to sleep until one arrives.  Note that, although WaitPort() does
return a pointer to a message, it does not remove it from the port.  You
should always follow WaitPort() with a call to GetMsg() to dequeue the
message.

        Another way to wait for messages is to use exec's general-purpose
Wait() function.  It's used like this (when waiting for messages):

-----
Wait (1 << port -> mp_SigBit);
msg = GetMsg (port);
-----

        Note that Wait() does not "buffer" i.e. if two messages arrive at
your port, Wait() will not report both of them.  If you're using Wait() to
wait for messages, the proper way to make sure you get all of them is like
this:

-----
Wait (1 << port -> mp_SigBit);
while (msg = GetMsg (port)) {
        << relevant message handling here >>
}
-----

        It may look like busy-waiting, but it isn't.  The while-loop
continues to execute until all the messages on your port have been received
and dequeued.  Once you've gotten them all, the loop exits and you're free
to check on other things.

        When you have gotten the message, you are free to access and modify
the message as much as you like.  After all, it's now your RAM.

        After you are through using a message, it is a good idea to reply to
it.  Tasks that send you messages often wait for you to reply to them so
they can get on with their work.  This is done with the exec function
ReplyMsg(), which works like this:

ReplyMsg (msg);
struct Message *msg;

        "msg" is a pointer to a message that arrived at your port.

        Note that the message's mn_ReplyPort field must be valid for
ReplyMsg() to work properly.  Once you have replied to a message, you are no
longer allowed to use the data in the message, since you have returned
ownership to the task that sent you the message.

        Note also that, when you reply to a message, it is exactly as if you
said:

-----
PutMsg (msg -> mn_ReplyPort, msg);
-----

        This means that the task receiving the reply will receive it as
though it were an ordinary message.  The receiving task should know what to
do with replies to messages.  It is not a good idea to reply to a reply.
This is a typical way of sending a message and waiting for a reply:

-----
PutMsg (sendport, msg);
WaitPort (replyport);
GetMsg (replyport);     /*  We don't reply to replies, we just get them  */
-----


Traps To Avoid
--------------

        One trap that is easy to fall into is failing to remember that, once
you reply a message, you can no longer rely on the information in the
message.  Here's one incantation of this trap I fell into.

-----Low priority task segment-----
        struct IORequest *msg;
        struct MsgPort *port;

        /*  msg initialized elsewhere (possibly with CreateExtIO())  */
        port = CreatePort ("foo port", 0);
        if (msg = GetMsg (port)) {
                ReplyMsg (msg);
                if (msg -> io_Command == SPECIALVAL)
                        exit (-1);
        }
-----High priority task segment-----
        struct IORequest *msg;
        struct MsgPort *sendport, *replyport;

        replyport = CreatePort ("replies", 1);
        sendport = FindPort ("foo port");
        if (SPECIAL_CASE) {
                msg -> io_Command = SPECIALVAL;
                PutMsg (sendport, msg);
                WaitPort (replyport);
                GetMsg (replyport);
        }
        msg -> io_Command = NORMAL_VAL;
-----

        Here's what happens.  The high priority task detects a special case,
and sends a message to the low priority task with a special value in the
command field.  Then it goes to sleep, waiting for a reply.

        The low priority task wakes up with a message in its port.  It gets
the message, then replies to it right away so the high priority task won't
have to wait too long.

        The moment the low priority task replies, the high priority task
wakes up again (since the WaitPort() just got satisfied), gets the message,
and changes the command field back to its normal value.

        Eventually down the line, the high priority task goes to sleep on
something, and the low priority task starts where it left off.  Remember
that the low priority task had just replied the message and was about the
check the command field for SPECIALVAL.  But it lost control of the CPU when
it replied the message, and the command field got changed back to
NORMAL_VAL.  So the test for SPECIALVAL will fail, and the exit() function
will never get called.

        The way to avoid this trap is to copy *all* the data you intend to
examine into your own private storage before you reply the message.  The fix
to the above code would be this:

-----Low priority task segment-----
        int cmd;

        if (msg = GetMsg (port)) {
                cmd = msg -> io_Command;
                ReplyMsg (msg);
                if (cmd == SPECIALVAL)
                        exit (-1);
        }
-----

        If you intend to modify the message before you reply to it, you
should do all modification before replying the message.

        Remember: when you reply a message, you give up all rights to use
the data in the message.


An Example
----------

        Attached (somewhere) is a piece of source code (in C) that
illustrates the creation of a task, creation of message ports, message
passing, passing and modifying data in the message, and cleaning up.  It
compiles sucessfully under Lattice 3.03 (sorry, I couldn't make it work with
Manx).  Ignore the compiler warnings about improperly typed pointers.

        Note: this program, once compiled and linked (remember the "faster"
argument!), __s_e_e_m_s_ to work OK, provided the program is on
disk.  If you try to run it out of RAMdisk, it will run, but the next
program you try to run crashes the machine.  I'm not sure why this is
happening (I suspect I'm forgetting to do something terribly important), and
would appreciate help in this area.  Other than that, it works.


Bibliography
------------

        ROM Kernel Manual (v1.1), Volume 1
        pp. 1-11 - 1-21, 1-29 - 1-37

        ROM Kernel Manual (v1.1), Volume 2
        pp. A-64, A-65, A-68, A-74, A-75, A-78, A-82, A-85 - A-87, A-93,
            A-94, D-23, D-27, D-29, D-30, Appendix F (past page E-78)

----------------

        I hope you've found this helpful.  If you have any suggestions or
corrections, or just want to flame me, feel free to leave me some mail.

                                        Have fun,
                                        Leo L. Schwab
--------
...!{hplabs,dual,well}!unicom!schwab    (or)    ...!well!ewhac
        "We interrupt this program to annoy you, and to make things
generally irritating for you."

################# Source follows - cut here ##################

/*  :ts=8
 * Code to demonstrate Exec multitasking.
 */

#include <exec/types.h>
#include <exec/io.h>

#define REV		0
#define SUB_PORT	"Subtsk Port"
#define	SUB_REPLY	"Subtsk Reply"

#define CMD_HELLO	CMD_NONSTD
#define	CMD_SUICIDE	(CMD_NONSTD + 1)

struct infopacket	*CreateExtIO();

struct infopacket {
	struct Message msg;
	long command;
} *sub_cmd;

struct MsgPort		*subport, *subreply;
struct Task		*subtask;
void			neeto();


openstuff ()
{
	if (!(subreply = CreatePort (SUB_REPLY, 0L)))
		bomb ("I need a reply port.");

	/*  We can get away with this  */
	if (!(sub_cmd = CreateExtIO (subreply, (long) sizeof (*sub_cmd))))
		bomb ("Can't make subtsk ExtIO.");

	subtask = CreateTask ("Neeto", 1, neeto, 2048L);
	if (!(subport = FindPort (SUB_PORT)))
		bomb ("Can't find SUB_PORT.");
}

closestuff ()
{
	if (subtask) {
		if (subport) {
			sub_cmd -> command = CMD_SUICIDE;
			PutMsg (subport, sub_cmd);
			WaitPort (subreply);
			GetMsg (subreply);
		}
		DeleteTask (subtask);
	}
	if (sub_cmd)
		DeleteExtIO (sub_cmd, (long) sizeof (*sub_cmd));
	if (subreply)
		DeletePort (subreply);
}

bomb (str)
char *str;
{
	printf ("%s\n", str);
	closestuff ();
}



/*
 * The sub-task.
 */

void
neeto ()
{
	struct infopacket *cmd;
	struct MsgPort *prgport = NULL;

	/*  Since this is an independent task, we'll have to do
	 *  everything ourselves.....
	 */
	if (!(prgport = CreatePort (SUB_PORT, 1L)))
		goto die;

	/*
	 * Stand around twiddling our thumbs until the main program
	 * wants us to do something.
	 */
	WaitPort (prgport);
	cmd = GetMsg (prgport);
	if (cmd -> command != CMD_HELLO)
		/*
		 * If we come here, then something is very wrong.  Let the
		 * machine crash (we can't do printf's from a task).
		 */
		bomb ("neeto(): Hello???");
	ReplyMsg (cmd);

	while (1) {
		WaitPort (prgport);

		while (cmd = (struct infopacket *) GetMsg (prgport))
			switch (cmd -> command) {
			case CMD_UPDATE:
				cmd -> command = 0;
				ReplyMsg (cmd);
				break;
			case CMD_SUICIDE:
				goto die;
			default:
				ReplyMsg (cmd);
			}
	}
die:
	if (prgport)
		DeletePort (prgport);
	if (cmd)
		/*  This must be the suicide message, so reply it  */
		ReplyMsg (cmd);

	/*  Wait for Godot  */
	Wait (0L);
}

main ()
{
	int i;

	openstuff ();

	sub_cmd -> command = CMD_HELLO;
	PutMsg (subport, sub_cmd);
	WaitPort (subreply);
	GetMsg (subreply);

	for (i=0; i<10; i++) {
		PutMsg (subport, sub_cmd);
		WaitPort (subreply);
		GetMsg (subreply);
		if (i == 6)
			sub_cmd -> command = CMD_UPDATE;
		printf ("command = %ld\n", sub_cmd -> command);
	}

	closestuff ();	/*  This kills off the subtask  */
}