Primer on Forth Multitasking This multitasking system is cooperative, not preemptive. A task switch occurs only when the currently-running task explicitly gives up control, not at random times. This works fine so long as each task is courteous and relinquishes control every so often. This does not preclude the use of interrupts; it just means that when an interrupt routine finishes executing, it does not cause a task switch at the user level. A task relinquishes control by executing the word PAUSE , hence from now on the act of giving up control will be called pauseing. Tasks are kept in a circular queue and are scheduled in a round-robin fashion. When the current task pauses, the next task in the queue is given a chance to run until it pauses, and so on. Tasks may be in one of two states: awake or asleep. If a task is awake, it will be given control when its turn in the round-robin queue comes along. If a task is asleep, it will remain in the queue, but will be skipped over. It is reasonable to let sleeping processes remain in the queue, because skipping over them is very cheap; it takes only 3 68000 instructions to skip over a sleeping task. All tasks share the same global address space and can execute the same code. Each task has its own private data stack, return stack, and user area. The user area is where the task-specific USER variables are stored. The CPU state that defines a task is the contents of the Data Stack Pointer (SP) (actually A7), the Return Stack Pointer (RP) (actually A6), and the User Pointer (UP) (actually A3). The dictionary, which contains executable forth code, is global and is shared by all tasks. Any VARIABLEs or other data areas within the dictionary, such as those allocated by ALLOT , are global and may be accessed by all tasks. This allows tasks to communicate using shared memory. In the initial state of the Forth system, there is only one task, called MAIN-TASK . This is the task that is executing the Forth interpreter. Other tasks may be subsequently created. There are several steps involved in making a new task: 1) Naming the task. A task is initially created with either "TASK: or TASK: . Each takes two arguments, the size of the private data area to allocate and the name of the new task. The only difference between the two is that "TASK: takes the name of the new task as a string from the stack, whereas TASK: takes the name from the input stream. See to glossary for precise definitions. Naming the task in this way does not immediately cause the allocation of memory for the stack; it just makes a name for the task in the dictionary. Nor is the task entered into the round-robin queue. 2) Forking the task. The next step is to FORK the task. Forking a task causes several things to happen. The memory for the task's private data areas is allocated, the task's user area is initialized with a copy of the user area of the task that is currently running, and the task is entered into the round-robin queue. The task is initially asleep, so it is skipped over when its turn in the queue comes up. The distinction between naming the task and forking it is convenient because of the desire to be able to compile programs under Unix and run them stand-alone. Naming is done at compile time, and forking is done at run time. This has two benefits: a) The private data areas of the tasks do not take up space in the bootable file b) The copying of the user area from the current task to the new task can be deferred until run time. This is useful because the user area contains variables which control whether I/O is to be done using Unix or the PROM monitor. It is convenient to copy the user area when the program is already running in the stand-alone mode, so that the stand-alone copies of the appropriate variables are inherited by the new task. 3) Setting the program that the task it to execute. The task must be told what to do. This is done by setting the task's Interpreter Pointer (IP) to the address of an appropriate bit of Forth code. The Interpreter Pointer in Forth plays a role in Forth that is analogous to the role that the Program Counter plays in a machine language program. The word that sets the Interpreter Pointer is START . For an already active task, START may also be used to make the task start over or even do something entirely different. 4) Waking the task. Now the task may be actually set to work. Waking the task, by using the word WAKE , causes the task to be executed when its turn in the queue comes, instead of being skipped. Once awakened, a task will continue running until it pauses. If a task pauses, it will continue to execute each time that its turn in the queue comes around. If a task does not want to run any more, it may put itself to sleep so that it will be skipped. Tasks may also control each other, so that one task may put another to sleep. Glossary Task Creation and Initialization: "TASK: size name -- A defining word use to create a new task. Name is the address of a packed string which is entered into the dictionary as the name of the new task. Size is the number of bytes to allocate for the task's private data areas. Size must be big enough to include space for a data stack, a return stack, and a user area. DEFAULT-TASK-SIZE is a good choice. When name is subsequently executed, the address of the new task's data area is left on the stack. Implementation note: before the task is FORKed, execution of name causes the address of its parameter field to be left on the stack instead of the address of the data area. FORK fixes name so that it will subsequently leave the address of the data area. TASK: size -- A defining word executed in the form: size TASK: Creates a dictionary entry for name, which is a new task. Tasks thus created behave exactly like those created by "TASK: See: "TASK: DEFAULT-TASK-SIZE -- size Size is an appropriate number to use for the size of a new task's private data areas. Currently it is (hex) 600, which gives (hex) 400 bytes for User Area, and (hex) 100 bytes each for the Return Stack and the Data Stack. If a task is created with a size other than DEFAULT-TASK-SIZE , the difference will be reflected in the size of the Data Stack. TASK-RS-SIZE -- u u is the number of bytes that will be allocated for the Return Stack from the private data area for a task. Currently it is (hex) 100. USER-SIZE -- u u is the number of bytes that will be allocated for the User Area from the private data area for a task. Currently it is (hex) 400. FORK task-pfa -- task-pfa is the number left by executing the created by "TASK: or TASK: . The private data area for that task is allocated, the user area of the currently executing task is copied to the user area of the new task, the task is linked into the round-robin task queue, and that task is put to sleep. is fixed so that instead of leaving its pfa, it will subsequently leave the address of the tasks private data area. Controlling Tasks: START cfa task -- task is the address of the private data area of a task, as left by the of a task that has already been FORKed. cfa is the compilation address of a high-level Forth word that the task is to execute. The data and return stacks of the task are cleared, and the task is set so that it will begin execution of that word when it is awakened and its turn comes. The word that a task executes should never return. It should either contain an endless loop, or should execute STOP when it is done, thus putting itself to sleep. START should only be executed by one task in order to initialize a different task. A task should not try to START itself (this effect may be had from within a task by explicitly clearing the stacks and executing the desired word). A task may START another at any time. STARTing an already-running task will make it immediately stop what it is doing and instead execute the new action (cfa). SET-IP ip task -- Similar to START , except that instead of giving the cfa of the word to execute, the actual address of the task's new interpreter pointer is given. This is useful as an implementation word for the multitasking system, but it is not recommended for use by user programs. SLEEP task -- task is the address of the private data area of a task, as left by the of a task. The task is put to sleep, so that it will be skipped each time that its turn in the round robin queue comes up. This is the appropriate way to suspend the activity of another task. WAKE task -- task is the address of the private data area of a task, as left by the of a task. The task is awakened, so that it will continue execution when its turn in the round robin queue comes up. STOP -- The currently executing task puts itself to sleep and relinquishes control. This is the way for a task to stop itself when it is finished with its job. It is usually not a good idea to execute STOP interactively, since the interactive task will be then go to sleep and will stop listening to the keyboard. PAUSE -- Deferred (PAUSE -- The currently executing task relinquishes control and the round-robin task queue is scanned for the next task that is awake. When other tasks have been given their chance to run, eventually control will be returned to this task, which will begin execution immediately following the PAUSE . Pause is deferred so that multi-tasking may be disabled by setting PAUSE to execute NOOP . To enable multitasking, (PAUSE is installed in PAUSE . (PAUSE is itself the multitasking scheduler. It is only 6 68000 instructions long! SINGLE -- Disable multitasking by installing NOOP in PAUSE . MULTI -- Enable multitasking by installing (PAUSE in PAUSE . For safety's sake, MULTI also ensures that the main task is awake, and that the memory allocator is initialized so that new tasks may be forked. LOCAL task user-variable -- addr task is the address of the private data area of a task, as left by the of a task. user-variable is the address of a user variable in the currently executing task's user area. addr is the address of the other task's copy of that user variable. LOCAL allows one task to poke around in another task's private data space, to see what is going on there. UP@ -- addr addr is the address of the private data area of the currently executing task, which happens to be the starting address of its user area. This allows a task to refer to itself without knowing its name. MAIN-TASK -- addr addr is the address of the private data area of the only task that was around in the beginning. Usually this task is executing the Forth interpreter, but it doesn't necessarily have to be. Implementation Notes: Layout of private storage for a task: Space Size ----- ---- User Area user-size Dictionary variable Parameter Stack variable Return Stack task-rs-size The dictionary and the Parameter Stack share an area equal to the task storage area size minus user-size minus task-rs-size If the task doesn't need any private dictionary space (most don't), then the user area can grow into the private dictionary area. The first 3 locations in the user are used for implementing the round-robin scheduler queue. 0 /n * user tos 1 /n * user entry ( entry address for this task ) 2 /n * user link ( link to next task ) tos contains the value of the stack pointer when the task is not currently executing. link contains a pointer to the next task in the round-robin queue. The pointer points to the user area of the next task, i.e. the address of the next task's tos variable. entry contains the address of a bit of machine code which controls whether or not the task is asleep. Here is what (pause does: Save state: push ip on return stack push rp on data stack store sp in tos To next task: Fetch the up for the next task from link Branch to the address contained in entry If a task is asleep, entry contains the address of the label. This makes it just chain to the next task (3 68000 instructions) If a task is awake, entry contains the address of , which does this: Task-resume: fetch saved sp from tos pop data stack to rp pop return stack to ip next (execute next Forth word)