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:
|
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.
|
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:
|
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:
|
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,
|
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:
|
where <task> is the task to be stopped. To restart a halted task, use RESTART which is used in the form:
|
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:
|
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.
|
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.
|
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:
|
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:
|
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 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.
|
The following words provided for embedded systems have no equivalent because application programs have no direct access to the interrupt control mechanisms:
|
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 SIGNALs 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.
|
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:
|
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:
|
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:
|
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:
|
You notice that the hashes are not displayed any more.
The task is restarted by RESTART. Type:
|
You notice that the hashes are displayed again.
To restart the task from scratch, just kill it and activate it again:
|
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.
0 constant test-code? \ -- n
The test code will be compiled if this constant is true.
#38 constant /tcb.callback \ -- len
Size of the task callback data and code. Used for error checks.
X86 version.
#44 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 int tcb.hthread \ task handle 4 int tcb.up \ user pointer 8 int tcb.pumpxt \ xt of message pump or 0 for none 12 int tcb.status \ status bits 16 int tcb.mesg \ message from another task 20 int tcb.msrc \ TCB of task from which message came 24 int tcb.event \ xt of event handler 28 int tcb.clean \ xt of clean up handler 32 /sem_t field tcb.haltsem \ sem_t (16) for halt/suspend 36 /tcb.callback field tcb.callback \ task callback structure 52 aligned \ force to cell boundary end-struct
The task status cell reserves the low 8 bits for use by VFX Forth. The other bits may be used by your application.
|
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
: perform \ addr --
Execute contents of addr if non-zero. The non-zero contents of
addr are EXECUTEd.
create main \ -- addr ; tcb of main task
The task structure for the first task run, usually the console
or the main application.
: InitTCB \ addr --
Initialise a task control block at addr. X86 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 HALTed or
STOPped, it will be restarted.
: halt \ tcb -- ; mark thread as halted
Stops an INITIATEd task from running until
RESTART is used.
: stop \ -- ; halt oneself
HALTs 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 rhe 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:
|
: 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:
|
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 MultiLin32.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.
$AAAA5555 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.
$5555AAAA 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 i32 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 SIGNALs
that it has finished with the resource.