The multitasker supplied with VFX Forth is derived from the multitasker provided with the MPE Forth cross compilers, v6.1 onwards. Using a multitasking system can greatly simplify complex tasks by breaking them down into manageable chunks. This chapter leads you through:
The multitasker source code is in the file Lib/Lin32/MultiLin32. Note that the full version of this file with all switches set except for test code is compiled as part of the second stage build, but is not present by default in the kernel version of VFX Forth.
The configuration of the multitasker is controlled by constants that control what facilities are compiled:
0 constant event-handler? \ true for event handler
0 constant message-handler? \ true for message handler
0 constant semaphores? \ true for semaphores
0 constant test-code? \ true for test code
The multitasker needs to be initialised before use. At compile time you must define the tasks that your system requires and at run-time, all the tasks must be initialised.
Before use the multitasker must be initialised by the word
INIT-MULTI
, which initialises the primary task MAIN
,
and enables the multi-tasker.
To disable the multitasker, use SINGLE
.
To enable the multitasker, use MULTI
, which starts the
scheduler so new tasks can be added.
Tasks are very straightforward to write, but the way tasks
are scheduled needs to be understood. This implementation
uses the Linux pthreads API, and so tasks are pre-emptively
scheduled. This is different from the cooperative scheduler used
by embedded systems. Despite this, the word PAUSE
which
yields a timeslice is retained for compatibility, and
PAUSE
is where the MPE event handling is incorporated.
: ACT1ON1 \ -- ; An example task
TASK0-IO \ select the console as the I/O device
DUP IP-HANDLE ! OP-HANDLE !
BEGIN \ Start an endless loop
[CHAR] * EMIT \ Produce a character )
1000 ms \ Wait 1 second
PAUSE \ Needed!
AGAIN \ Go round again
;
TASK TASK1 \ -- tcb ; name task, get space for it
The task name created by TASK
is used as the task
identifier by all words that control tasks.
An area of memory known as the USER
area is set aside
for each task. This is often called thread local
storage. This memory contains user variables which contain
task specific data. For example, the current number conversion
radix BASE
is normally a user variable as it can vary
from task to task.
A user variable is defined in the form:
n USER <name>
where n is the nth byte in the user area. The word
+USER
can be used to add a user variable of a given
size:
<size> +USER <name>
The use of +USER
avoids any need to know the offset at
which the variable starts.
A user variable is used in the same way as a normal variable.
By stating its name, its address is placed on the stack,
which can then be fetched using @
and stored by !
.
Tasks can be controlled in the following ways:
A task is started by activating it. To activate a task, use
INITIATE
,
' <action> <task> INITIATE
where ' <action>
gives the xt of the word to be run
and <task>
is the task identifier. The task identifier
is used to control the task. Tasks defined by
TASK <name>
return a task identifier when <name>
is executed.
A task may be temporarily suspended. A task may also halt
itself. To temporarily stop a task, use HALT
.
HALT
is used in the form:
<task> HALT
where <task>
is the task to be stopped. To restart a
halted task, use RESTART
which is used in the form:
<task> RESTART
where <task>
is the task to restart.
To stop the current task (i.e. stop itself) use *\fo{STOP( -- ).
Terminating a task halts it, performs an optional clean up action, and calls the operating system thread end function. A thread must terminate itself, which leads to some complexities. However, it does give the task an opportunity to release any resources it may have allocated (especially memory) at start up or during its execution. To terminate a task use:
<task> TERMINATE
Before the operating system thread end function is called, the terminating task will execute its clean up code. The XT of the clean up code is held in the task control block. If no clean up action is required, zero is used.
... ['] CleanUp MyTask AtTaskExit ...
If you want a task to be a BEGIN ... UNTIL
loop rather
than an endless loop, this is perfectly legal,
as returning from a thread will call the clean up code and
then the ExitThread
function. However, you must define
an exit code before you return from the task. Note that on
entry to the task there will already be a 0 on the stack.
: MyTask \ 0 -- exitcode
<initialisation> \ initialise task resources
begin \ round and round until done
<actions> MyDone @
until
drop 0 \ paranoid, return 0 as success
;
Unlike MPE's embedded systems, under Linux you cannot
predict how long a task will take to start after INITIATE
or shut down after TERMINATE
.
An essential feature of the multitasker is the ability to send and receive messages between tasks. For cross compiler compatiblility, the operating system mechanisms are not used.
To send a message to another task, use the word SEND-MESSAGE
,
used in the form:
message task SEND-MESSAGE
where message is a 32-bit message and task is the identifier of the receiving task. The message can be data, an address or any other type of information but its meaning must be known to the receiving task.
To receive a message, use GET-MESSAGE
. GET-MESSAGE
suspends the task until a message arrives. When a message is
received the task is re-activated and the sending task and
the data are returned.
Events are analogous to interrupts. Whereas interrupts happen on hardware signals, events happen under software control.
An event is a normal Forth word. An event is associated to a task so that when the event is triggered, the task is resumed. Therefore, an event is usually used as initialisation for a task. Note that an event handler must have NO net stack effect.
Events are initialised in a similiar way to tasks. They are assigned in the form:
ASSIGN EVENT1 task TO-EVENT
where EVENT1
is your event handler and task
is
the task that it is to be associated with.
There are two ways of triggering an event:
SET-EVENT
SET-EVENT
is a word that sets an event flag for a task. Once
the event flag is set, the tasker will execute the event
before it switches to the task's main-line code. The task is
also restarted.
A bit can be set in a task's status word that indicates to the multitasker that an event has taken place. This method can be used to trigger an event from a hardware interrupt or a device driver. Refer to `The multitasker internals' section later in the chapter for details on the status cell. This mechanism can be used to signal that some event has taken place, and that consequent processing should start.
To stop an event handler being run, use CLEAR-EVENT
.
Sometimes the multitasker has to be inhibited so that other
tasks are not run during critical operations that would
otherwise cause the scheduler to operate. This is achieved
using the words SINGLE
and MULTI
. Note that these
do *\{not} stop the Linux scheduler, only the MPE extensions.
If a full critical section is required, see the semaphore
source to find out how to use the Windows critical section API.
SINGLE -- ; inhibit tasker
MULTI -- ; restart tasker
The following words provided for embedded systems have no equivalent because application programs have no direct access to the interrupt control mechanisms:
DI EI SAVE-INT RESTORE-INT [I I]
A SEMAPHORE
is a structure used for signalling between
tasks, and for controlling resource usage. It has two fields,
a counter (cell) and an owner (taskid, cell). The counter
field is used as a count of the number of times the resource
may be used, and the owner field contains the TCB of the task
that last gained access. This field can be used for priority
arbitration and deadlock detection/arbitration.
This design of a semaphore can be used either to lock a
resource such as a comms channel or disc drive during access
by one task, or as a counted semaphore controlling access to
a buffer. In the second case the counter field contains the
number of times the resource can be used. Semaphores are
accessed using SIGNAL
and REQUEST
.
SIGNAL
increments the counter field of a semaphore,
indicating either that another item has been allocated to
the resource, or it is available for use again, 0 indicating
that it is in use by a task.
REQUEST
waits until the counter field of a semaphore
is non-zero, and then decrements the counter field by one.
This allows the semaphore to be used as a COUNTED semaphore.
For example a character buffer may be used where the semaphore
counter shows the number of available characters. Alternatively
the semaphore may be used purely to share resources. The
semaphore is initialised to one. The first task to REQUEST
it gains access, and all other tasks must wait until the
accessing task SIGNAL
s that it has finished with the
resource.
A multitasker tries to simulate many processors with just one processor. It works by rapidly switching between each task. On each task switch it saves the current state of the processor, and restores the state that the next task needs. The Forth multitasker creates a task control block for each task. The task control block (TCB) is a data structure which contains information relevant to a task.
The following example is a simple demonstration of the multitasker. Its role is to display a hash `#' every so often, but leaving the foreground Forth console running. To use the multitasker you must compile the file LIB\MULTIWIN32.FTH into your system. Note that the file has already been compiled by the Studio IDE in VfxForth.exe, but is not present in VfxBase.exe.
The following code defines a simple task called TASK1
.
It displays a '$' character every second.
VARIABLE DELAY \ time delay between #'s in milliseconds
1000 DELAY ! \ initialise time delay
: ACTION1 \ -- ; task to display #'s
TASK0-IO \ select the console as the I/O device
DUP IP-HANDLE ! OP-HANDLE !
[CHAR] $ EMIT \ Display a dollar
BEGIN \ Start continuous loop
[CHAR] # EMIT \ Display a hash
DELAY @ MS \ Reschedule Delay times
PAUSE \ At least one per loop
AGAIN \ Back to the start ...
;
The use of PAUSE
in this example is not actually required
as MS
periodically calls PAUSE
.
Before any tasks can be activated, the multitasker must be initialised. This is done with the following code:
INIT-MULTI
The word INIT-MULTI
initialises all the multitasker's
data structures and starts multitasking. This word need only
be executed once in a multitasking system and is usually
executed at start up.
Note that on entry to a task, the stack depth will be 1. This happens because Linux requires a return value when a task terminates, and a value of zero is provided by the task initialisation code.
To run the example task, type:
TASK TASK1
ASSIGN ACTION1 TASK1 INITIATE
This will activate ACTION1
as the action of task
TASK1
. Immediately you will see a dollar and a hash
displayed. If you press <return> a few times, you notice
that the Forth interpreter is still running. After a few
seconds another hash character will appear. This is the
example task working in the background.
The example task can be controlled in several ways:
Changing the variable DELAY
can change the rate of
production of hashes. Try:
2000 DELAY !
This changes the number of milliseconds between displaying hashes to 2000 milliseconds. Therefore the rate of displaying hashes halves.
Typing the task name followed by HALT
halts the task:
TASK1 HALT
You notice that the hashes are not displayed any more.
The task is restarted by RESTART
. Type:
TASK1 RESTART
You notice that the hashes are displayed again.
To restart the task from scratch, just kill it and activate it again:
TASK1 TERMINATE
ASSIGN ACTION1 TASK1 INITIATE
You notice the dollar and the hash are displayed, followed by more hashes.
1 constant event-handler? \ -- n
The event handling code will be compiled if this constant is true.
1 constant message-handler? \ -- n
The message handling code will be compiled if this constant is true.
1 constant semaphores? \ -- n
The semaphore code will be compiled if this constant is true.
1 constant test-code? \ -- n
The test code will be compiled if this constant is true.
56 constant /pthread_attr_t \ -- len
The structure is now an opaque type - see
Kernel/x64Lin/Tests/lintest64.c.
#50 constant /tcb.callback \ -- len
Size of the task callback data and code. Used for error checks.
X64 version.
xxx constant /tcb.callback \ -- len
Size of the task callback data and code. Used for error checks.
ARM version.
struct /TCB \ -- size
Returns the size of a TCB structure, which controls the task.
int tcb.link \ link to next task ; MUST BE FIRST 0 offset int tcb.hthread \ task handle 8 int tcb.up \ user pointer 16 int tcb.pumpxt \ xt of message pump or 0 for none 24 int tcb.status \ status bits 32 int tcb.mesg \ message from another task 40 int tcb.msrc \ TCB of task from which message came 48 int tcb.event \ xt of event handler 56 int tcb.clean \ xt of clean up handler 64 /sem_t field tcb.haltsem \ sem_t (32) for halt/suspend 72 /tcb.callback field tcb.callback \ task callback structure 104 aligned \ force to cell boundary 154 end-struct \ 160
The task status cell reserves the low 8 bits for use by VFX Forth. The other bits may be used by your application.
Bit When set When Reset
0 RFU RFU
1 Message pending but not read No messages
2 Event triggered No events
3 Event handler has been run No events - reset by user
4..7 RFU RFU
8..31 User defined User defined
cell +USER ThreadExit? \ -- addr
Holds a non-zero value to cause the thread to exit.
cell +USER ThreadTCB \ -- addr
Holds a pointer to the thread's TCB.
cell +USER ThreadSync \ -- addr
Holds bit patterns used for intertask synchronisation.
See later section.
: AtTaskExit \ xt tcb -- ; set task exit action
Sets the given task's cleanup action. Use in the form:
' <action> <task> AtTaskExit
create main \ -- addr ; tcb of main task
The task structure for the first task run, usually the console
or the main application.
: InitTCB \ tcb --
Initialise a task control block at addr. X64 version.
: InitTCB \ addr --
Initialise a task control block at addr. ARM version.
: task \ -- ; -- addr ; define task, returns TCB address
Use in the form TASK <name>
, creates a new task data
structure called <name>
which returns the address of
the data structure when executed.
: Self \ -- tcb|0 ; returns TCB of current task
Returns the task id (TCB) of the current task. If called
outside a task, zero is returned.
: his \ task uservar -- addr
Given a task id and a USER
variable, returns the
address of that variable in the given task. This word is
used to set up USER
variables in other tasks. Note
that the task must be running.
0 value multi? \ -- flag
Returns true if the tasker is enabled.
: single \ -- ; disable scheduler
Disables the Forth portions of the scheduler, but does not
disable Linux scheduling.
: multi \ -- ; enable scheduler
Enables the Forth portions of the scheduler, but does not
disable Linux scheduling.
defer pause \ --
PAUSE
is the software entry to the pre-emptive scheduler,
and should be called regularly by all tasks.
The phrase sched_yield drop
occurs at the end of the
default action (PAUSE)
. If the task needs more than
this and does not use one of the existing message loop words
such as IDLE
, place the XT of the message pump word
in offset TCB.PUMPXT
of the Task Control Block and that XT
will be called once every time PAUSE
is called.
Because of the way Linux works, PAUSE
also controls
task closure. A task that does not call PAUSE
cannot
be safely terminated except by the task itself, or by a call
to the API function kill()
. A task that calls PAUSE
in a loop without calling any delay mechanism will cause
CPU hogging.
: (pause) \ -- ; the scheduler itself
The action of PAUSE
after the multitasker has been
compiled. If SINGLE
has been set, no action is taken.
If PAUSE
was not called from a task and MULTI
is
set, the action is sched_yield.
: restart \ tcb -- ; mark task TCB as running
If the task has been initiated but is now HALT
ed or
STOP
ped, it will be restarted.
: halt \ tcb -- ; mark thread as halted
Stops an INITIATE
d task from running until
RESTART
is used.
: stop \ -- ; halt oneself
HALT
s the current task.
: running? \ tcb -- u
Returns the task's semaphore value, where non-zero indicates
that it is running. Returns 0 on error.
: set-event \ task -- ; set event trigger in task TCB
Sets the event trigger bit in the task. When PAUSE
is
next executed by that task, its event handler will be run.
: event? \ task -- flag ; true if task had event
Returns true if the task has received an event trigger,
but has not yet run the event handler.
: clr-event-run \ -- ; reset own EVENT_RUN flag
Clear the EVENT_RUN
flag of the current task. This is
usually done if the task has to be put back to sleep after
the event handler has been run.
: to-event \ xt task -- ; define action of a task
Used in the form below to define a task's event handler:
assign <action> <task> to-event
: msg? \ task -- flag ; true if task has message
Returns true if the given task has received a message.
: send-message \ mesg task -- ; send message to task (wakes it up)
Sends a message to a task, waking it up if it was asleep.
Interpretation of a message is the resposibility of the
receiving task. If the receiving task has unprocessed
messages, the sending task blocks.
: get-message \ -- mesg task ; wait for any message
Waits until a message has been received from another task.
Interpretation of a message is the resposibility of the
receiving task. See MSG?
which tells you if a message
is available.
: wait-event/msg \ -- ; wait for message or event trigger
Wait until a message or event occurs.
: to-task \ xt task -- ; set action of task
Used in the form below to define a task's action:
assign <action> <task> to-task
: to-pump \ xt task -- ; set message loop of task
Used in the form below to define the action of the message pump:
assign <action> <task> to-pump
: initiate \ xt task -- ; start task from scratch
Initialises a task running the given xt. All required O/S
resources are allocated by this word.
: terminate \ task -- ; stop task, and remove from list
Causes the specified task to die. You should not make
assumptions as to how long this will take. Unlike the
embedded systems implementations, this word is very
operating system dependent. The task may still be alive
on return from this call.
N.B. Do not use self terminate
to cause a task to end.
Use the following instead:
: Suicide \ -- ; terminate current task
pause termThread 0 pthread_exit
;
... Suicide
: start: \ task -- ; exits from caller
START:
is used inside a colon definition. The code
before START:
is the task's initialisation, performed
by the current task. The code after START:
up to the
closing ;
is the action of the task. For example:
TASK FOO
: RUN-FOO
...
FOO START:
...
begin ... pause again )
;
All tasks must run in an endless loop, except for
initialisation code. There are exceptions to
this, and these are discussed in the section on terminating
a task. When RUN-FOO
is executed, the code after
START:
is set up as the action of task FOO
and
started. RUN-FOO
then exits. If you want to perform
additional actions after starting the task, you should use
IINITIATE
to start the task.
: TaskState \ task -- state
Returns true if the task has started and zero if the thread
has finished.
: init-multi \ -- ; initialisation of multi-tasking
Initialise the Forth multitasker to a state where only the
task MAIN
is known to be running. INIT-MULTI
is
added to the cold chain and is also called during
compilation of MultiLin64.fth. This word must
be run from MAIN
.
: term-multi \ --
Performed in the exit chain when the program terminates.
closes all active tasks except SELF
. This allows
all task clean-up actions to be performed before the
program itself finishes.
: .task \ task -- ; display task name
Given a task, e.g. as returned by SELF
, display its
name or address.
: .tasks \ -- ; display active tasks
Display a list of all the active Forth tasks.
$AAAA:5555:AAAA:5555 constant TaskReady \ -- n
At task initiation, USER
variable THREADSYNC
is
set to zero. Set THREADSYNC
to this value to indicate
that the task is willing to synchronise with another task.
$5555:AAAA:5555:AAAA constant TaskReadied \ -- n
A synchronising task sets another task's THREADSYNC
to this value to indicate that synchronisation is
complete.
: WaitForSync \ --
Perform the slave synchronisation sequence.
: [Sync \ task -- task
Used by a master task in the form:
[Sync ... Sync]
to synchronise and pass data to another task, usually
when USER
variables must be initialised. The slave
task must execute WAITFORSYNC
.
: Sync] \ task --
Used by a master task in the form:
[Sync ... Sync]
to indicate the end of synchronisation.
struct /semaphore \ -- len
Structure used for Linux x64 semaphores.
: semaphore \ -- ; -- addr [child]
A SEMAPHORE
is an extended variable used for signalling
between tasks and for resource allocation. The counter field
is used as a count of the number of times the resource may be
used, and the arbiter field contains the TCB of the task
that last gained access. This field can be used for priority
arbitration and deadlock detection/arbitration.
: InitSem \ semaphore --
Initialise the semaphore. This must be done before using it.
: ShutSem \ semaphore --
Delete the critical section associated with the smaphore.
: LockSem \ semaphore --
Lock the semaphore.
: UnlockSem \ semaphore --
Unlock the semaphore.
: signal \ sem -- ; increment counter field of semaphore,
SIGNAL
increments the counter field of a semaphore,
indicating either that another item has been allocated to
the resource, or that it is available for use again, 0
indicating in use by a task.
: request \ sem -- ; get access to resource, wait if count = 0
REQUEST
waits until the counter field of a semaphore
is non-zero, and then decrements the counter field by one.
This allows the semaphore to be used as a counted
semaphore. For example a character buffer may be used where
the semaphore counter shows the number of available
characters. Alternatively the semaphore may be used purely
to share resources. The semaphore is initialised to one.
The first task to REQUEST
it gains access, and all
other tasks must wait until the accessing task SIGNAL
s
that it has finished with the resource.