Processes

Overview

A (control) process defines and implements an independent thread of control, or thread of execution, as part of a control program. Such program can comprise one or more processes, each focusing on its realm of control, defined goals, and duties. Processes should be as self-contained as possible, with as narrow as feasible interfaces to each other (high cohesion, low coupling).

Processes are the dynamic building blocks of a control program, much like Oberon modules are static building blocks regarding program structure. Oberon modules provide an ideal conceptual and implementation substrate to realise processes.

A process gets invoked by a scheduling mechanism, and it can, but is not required to, hold state between invocations.

Cooperative Scheduling

Oberon RTK uses cooperative scheduling, where processes are given full control of their processor, or processor core, until they “voluntarily” yield this control back to the scheduler. There are substantial benefits with the cooperative approach, including being very simple and transparent, avoiding complex schemes to arbitrate access to shared data and devices, and allowing for simple context switches with little overhead.

With cooperative scheduling, a running process can be sure to never be interrupted (but see the following sections about interrupts and error handling), which is relevant regarding shared data and devices. If a process concludes its work on shared resources before it yields control back to the scheduler, no lock-out mechanism such as semaphores is required.

Control systems with cooperative scheduling can be powerful. NASA landed their Apollo spaceships on the moon with cooperatively scheduled software for navigation, attitude, and burn control. Due to their relative simplicity, such system are robust and transparent. However, cooperative scheduling puts the responsibility onto the programmer to fulfil the timing requirements.

Interrupts

We can distinguish between two needs, or uses, of interrupts. First, interrupts can react to genuine external events in the controlled system (environment), such as detection of a required measurement, or react to exceptional situations in the control system, such as failure states. Second, interrupts can be used to implement purely internal regular and recurring mechanisms, for example to empty a buffer towards a peripheral device.

Oberon RTK will try to avoid the second type, or use it closely coupled to processes. Systems employing many interrupts can become conceptually cumbersome quickly, and they are hard to test and debug.

Interrupts and interrupt handlers can muddy the simple concept of cooperative scheduling somewhat, and depending on their interaction with the processes this must be dealt with accordingly, for example using lock-out mechanisms. However, such lock-out mechanisms could delay the timely handling of an interrupt, which runs against the basic idea and even the usefulness of an interrupt.

Run-time Error Handling

A process could at any point be reset due to the run-time error handling. This may or may not be an issue, depending on the use case. We’ll discuss this topic when revisiting error handling and recovery.

Shared Resources

As outlined above, sharing resources such as data or peripheral devices is usually straightforward with cooperative scheduling. If, however, a process has not concluded all its work on a shared resource when yielding control back to the scheduler, a reserve and lock-out mechanism must be employed, for example a semaphore.

An example would be a shared peripheral device, where one process wants to transmit more data than the device’s buffer can hold. It then has to yield control to wait for the buffer to be empty again – busy waiting is not permitted –, but cannot allow another process to access the device in the meantime.

Note that semaphores and similar mechanisms require careful considerations regarding deadlocks.

Process Implementation

In a cooperatively scheduled system, processes can be implemented using

  1. Coroutines
  2. Tasks

Coroutines

One coroutine represent the complete thread of execution of a process, and control between coroutines is transferred explicitly. A coroutine is created by passing one procedure, which contains all the process’ code. While control processes basically could transfer execution control explicitly among themselves, this does not result in modular control programs with high cohesion and low coupling. Therefore, a scheduler transfers control to processes, which transfer control back to the scheduler when yielding.

Each coroutine has its own stack, which has pros and cons. On the pro side, the process’ state can be held in its stack, which is preserved between process invocations by the scheduler. On the con side, it might be difficult to calculate the needed size of a process’ stack, which must accommodate the deepest procedure call chain. This can result in stack space that is rarely used, and thus “wasted” memory, which can be an issue in memory-constrained systems.

Oberon RTK provides test tools to measure the stack memory used by each process. Also, the two stack pointers of the Cortex-M0 and Cortex-M33 processors can be put to good use: interrupt handlers can run in a different stack (the main stack) than the process stacks, reducing the need for reserves in the process stacks.

Tasks

Tasks employ one or more procedures (handlers), which get invoked by the scheduler. Each such procedure always runs to completion, and yielding control back to the scheduler is implicit.

The procedures of all tasks run using the same stack. If the stack grows “downwards” in memory, the scheduler sits atop, and calls all task procedures. Hence, each task handler can make use of the full size of the stack (apart from any local variables of the scheduler itself). Compared to coroutines, no memory is “wasted” for stack reserves of single processes.

However, the task needs to explicitly retain any process state between handler calls, which means more legwork by the programmer. On the plus side, the process state is open and transparent, which can be useful for error recovery involving restoring the state and continuing with the corresponding handler.