Service Routines
Closed this issue · 2 comments
A major limitation of a voluntary preemption OS is the inability to handle interrupts in a reliable manner. If you have multiple top priority tasks, you have to carefully manage delays on them, to make sure they all get time, and ideally you want a strict task hierarchy, with only one task per priority level. So how to do we manage urgent responses, when we have these limitations?
Service routines are micro-tasks that run every time the OS gets control. This means any time a task yields, all service routines will be run before the CPU is turned over to the next task. Like ISRs in a traditional RTOS, service routines should be very small and very fast. They can be used to check inputs or timers, to serve a similar role to ISRs, or they can handle I/O tasks that you don't want to block but that take some CPU time to complete. An example use might be handling network I/O in a system where multiple tasks need to generate and receive network traffic. A service routine could be used to route incoming traffic to the right tasks and handle things like task priority in outgoing traffic. Service routines are intended to be used to extend the OS, rather than provide a different kind of task.
There is one major difference between tasks and service routines: Service routines are intended to run to completion then return, rather than yielding, so that context does not need to be retained. (There may be some exceptions...read on...)
Design questions that need consideration: Should service routines be functions or objects? Functions are lighter weight, but services routines that need to retain context might be better as objects, since functions would need external data storage.
How about this: Service routines must be directly callable. The default is functions to minimize weight, but if you really need a service routine that retains context, rather than using global variables, you are encouraged to use a generator that runs all of the loop code before yielding. I don't think Python will care about the difference. A callable is a callable. (But, service routine generators should never return, or a StopIteration exception will be thrown, and I am not going to add exception handling for stuff like this, because space is a premium.)
Service Routines must be functions, which take no arguments and return nothing.
Service Routines cannot block and should be as short and fast as possible.
Normal service routines store no state.
Service Routines can use global MessageQueues for communication with tasks.
If a Service Routine has a reference to a task, it can use the task's "deliver()" method to send messages directly. Note that this method is intended to be used by the OS. Since Service Routines are user extensions of the OS, this is a valid use for this method*.
If Task notifications are implemented, Service Routines should be able to trigger them, given a reference to the task.
(*Maybe we need to add another means of passing arguments into tasks, so that this method can be moved out of the normal documentation and relegated to the exclusive role of kernel space API. Also, the addition of Service Routines will mean that the kernel space API needs to be documented.)
If a stateful Service Routine is necessary, a wrapped generator can be used. Note that these will use more resources than normal Service Routines and thus should be used very sparingly, but a stateful Service Routine is probably better than storing state globally that is not needed globally.
To use a generator as a Service Routine:
- Create a generator function. It can take arguments, if the Service Routine needs things like dynamic references to MessageQueues or a list of tasks to communicate with. Any initialization the service routine needs to do should be done before the infinite loop. This generator should never return, or a StopIteration exception will result in a kernel crash**.
- Start a generator instance. This will not be done automatically (as it would be with a task).
- Wrap the generator instance in a lambda function with next(). Something like,
net_service_routine = lambda: next(net_sr_gen)
. - Register the reference to the lambda function as a Service Routine.
(**It might be tempting to suggest that the OS use some exception handling to catch and gracefully terminate misbehaving tasks and service routines. There are two strong reasons not to do this. The first is resources. try/except statements and exception handling code will take up more flash space and memory, which are at a premium on many CircuitPython devices. The second is that invisibly handling exceptions like this will hide them from the user and make debugging more difficult. It is better for the OS to be intolerant to errors, so that user code doesn't rely on it to make up for bugs and faults.)
Service routines added. sample.py now has a simple test, which on my laptop runs the service routine some 20k+ times, in the 0.5 seconds the two sample task block before doing their work.
Let's do issue #4 . To keep service routines very light weight, we do need a very light weight communication system. Even if we use MessageQueue
s for bigger communication, task notifications would provide a good method for service routines to tell tasks which one needs to check a shared MessageQueue
.