A rant on why things should be done differently
While considering various extensions to RISC OS, in both kernel and support libraries, I've found that a rethink of the common underlying mechanisms associated with multitasking is in order. Here are some scenarios expressing those considerations.
The reactor pattern is a way of managing multiple I/O events in a single-threaded environment. A program designed this way sets up a reactor, and makes it available to the components used to build it, e.g. GUI, network middleware, other device controllers (e.g. audio). These all have different I/O requirements on the kernel, and if they tried to use traditional blocking calls unilaterally, each would interfere with the concurrent running of the others (in the same thread). (And non-blocking calls would have to be used either continuously, which is highly inefficient, or periodically, which is unresponsive.) Instead, the components register their interests in I/O events (such as data arriving from the network) or timing events (e.g. a particular moment in time, or a few seconds from the present time) with the shared reactor, and multiplexes their requirements into a single kernel call. When that returns, it demultiplexes the results, and up-calls the components for which relevant events have occurred. (This is not unlike the co-operative multitasking in RISC OS, but it occurs between components in a single thread in a single program.) A portable reactor interface allows components to be written to co-operate with each other in a portable manner, because even platform-specific versions of a component can all have the same, portable interface.
I wrote a ?reactor?
as a C library for use on UNIX systems. This is easy: almost
all I/O in a UNIX process is through a ?file? descriptor –
which includes network sockets, device descriptors and pipes to
other processes – and you can monitor several descriptors at
once with a single select() or pselect()
call, so the process can be put to sleep completely while the
kernel efficiently watches for interesting I/O on behalf of the
process. (There are also poll() and
epoll_wait calls which offer improvements over
select().)
I also adapted the reactor library to work portably on
Windows. Windows provides
MsgWaitOnMultipleObjectsEx()
(and some related functions) to do the same sort of job as
select(), although it uses ?system handles? rather
than file descriptors — these are the ?multiple objects? one
waits on with the call. (In fact, the use of handles, including
semaphores and such, allows the reactor to behave as a
proactor, whereby the kernel informs the process that an
I/O operation is complete, rather than that an anticipated
operation would not block. Something similar in PipedreamOS might
be advantageous.)
I figured I could do a simultaneous port to RISC OS, but I find it's too awkward. Multiplexing of events outside the WIMP is not achieved in a single, standard way — e.g. for sockets, it seems to have been bolted on afterwards. And the network is just one of several subsystems that might benefit from event multiplexing, e.g. a floppy drive, USB devices, etc. Will these be designed with multiplexing in mind, or will it be an after-thought? How will these subsystems interact in a program that uses more than one, particularly if the components used to access individual systems are libraries independent of each other? Is it going to make my reactor impossibly complex to build and maintain?
Some further info on reactors and proactors:
[Edit:
Hmm, maybe there's a way…
]
The WIMP system of RISC OS is an unnecessary conflation of context switching and GUI, leading to poor interaction with other subsystems that need to be aware of context switching.
For example, subsystems with blocking operations (such as sockets) must use mechanisms to keep in touch with the WIMP, which are non-uniform, somewhat ad-hoc and complex for the developer, and incapable of dealing with pre-emption (should that be available). Other blocking subsystems are unaware of the WIMP, because they (rightly) should not be GUI-aware, even if they should be context-aware, so their use puts responsiveness at risk (e.g. when a floppy-disc access occurs). Memory management, as another example, is tied in to the WIMP implementation, and so cannot be replaced and improved by itself — a shared-memory system would need to be informed of when a context is switched, for it to switch the memory mappings correspondingly.
The conclusions I draw from these considerations are that:
There needs to be a single task-switching or context-switching component in the kernel, to which other subsystems defer when blocking, forking or switching per-subsystem state, etc.
This component should have a sufficiently rich interface to cope with future subsystems, and allow tasks to sleep efficiently when idle/blocked.
The GUI should be one of the subsystems, not the main component. Other subsystems would be shared memory, sockets, program environment…
Shared libraries and a reactor implementation should be easier after this infrastructure is in place. Improved handling of sockets and clean-up of resources may similarly follow.
There is also an opportunity to support (perhaps limited, i.e. voluntary) pre-emption. The ability to do real-time applications without resorting to module code may follow from this, and that should encourage portability.