Design

Architecture and interface definitions

Archi­tecture

The system will be divided up as follows:

  • application processes, which are regular programs,

  • several subsystems, which perform tasks on behalf of the processes, and may hold state per process, and

  • a context switcher, which maintains each process's state as it gains and loses control of the processor.

Core subsystems include:

Other subsystems could include:

  • a socket/​Internet module;

  • a module to maintain pipes between processes;

  • a module to switch environment handlers as processes are switched;

  • a module to preserve the CWD setting, environment variables, etc;

  • a GUI.

Question: Should this architecture hide or expose environment handlers?

Answer:

The switcher will want to detect an exiting process through the exit handler, and the memory manager will want to detect memory faults through those handlers, so perhaps they should set up their own. If language interpreters set up theirs, they should fall through to the architecture's handlers.

So, we should hide the handlers by providing our own, but allow modules to intercept them.

On the other hand, we want to have the first stab at dealing with, say, page faults. Only if we fail, do we want to offer it to the application (which will probably report an error and call OS_Exit). Hmm…


Each application runs as one or more processes. All application activities will revolve around the switcher via the various subsystems, but will not normally talk directly to the switcher. The switcher will maintain a context for each process (i.e. at least one per running application), and one or more threads per process as part of that context.

Question: Why should the application not talk directly to the switcher?

Answer:

It could do, but all the operations would be ?spawn thread?, ?fork?, ?exit?, ?wait on handle(s)?, etc. That would then mean that the switcher would have to be aware of other mechanisms like event handles. It would be nice to decouple it from such things, in case event handles aren't the way forward.


Question: Why does the switcher have to know about threads?

Answer:

If it only recorded one thread (i.e. one supervisor stack) per context, an application couldn't yield with one thread and then resume with another.


Each subsystem will maintain private state (a subcontext) for each process context, while the switcher will co-ordinate the subcontexts of each subsystem as part of the process context. When a subsystem needs to access its state, it consults the switcher for a state reference which the subsystem gave to the switcher when the context was created.

A subsystem can indicate to the switcher several changes it wants to make to a process:

  • The subsystem, on behalf of the current process, is yielding control to the switcher (Context​Switcher_​Yield). For example, the application has initiated a blocking operation.

    When this call returns, the switcher is returning control to the subsystem/thread. At this point, if the subsystem doesn't want this thread to be pre-empted by another of its own threads, it should cancel any pending attempt by those threads to regain control. (This is how the WIMP could ensure that only one thread is ever accessing the resource it controls, the screen, at once.)

  • The subsystem wants to regain control for a process, at a particular priority (Context​Switcher_​Resume), e.g. when a blocking operation terminates.

  • The subsystem wants to cancel an attempt to regain control (Context​Switcher_​Resume).

  • The subsystem wants to duplicate (fork) a process (Context​Switcher_​Fork).

  • The subsystem wants to destroy a process (Context​Switcher_​Exit).

  • The subsystem wants to start a new thread in the current process (Context​Switcher_​Spawn).

  • The subsystem wants to terminate a thread (Context​Switcher_​Finish).

Most subsystems will only use the first two operations. The third (actually, a variation on the second) would only be used by a GUI that didn't want to pre-empt itself. The others will allow a dedicated subsystem (e.g. a thread manager) to present these operations to applications.

A subsystem can also register with the switcher its interest in certain events:

  • A new process is starting, with its context as a copy of an existing one, i.e. forking (Subsystem_​Fork). The subsystem should make a copy of its subcontext.

  • A process and its context are being destroyed (Subsystem_​Exit). The subsystem should destroy its subcontext.

  • A thread, blocked while in the subsystem, is terminating (Subsystem_​Terminate).

  • A process is yielding control (Subsystem_​Switch​Out), e.g. when it performs a blocking socket operation.

  • A process is gaining control (Subsystem_​Switch​In), e.g. when a blocking operation is completed.

  • A page fault has occurred.

Only a memory-management subsystem is likely to use the yield/gain/fault notifications. Other subsystems will only need to know that a particular context is loaded when the they are invoked, in which case, they just ask the switcher for their subcontext.

Only a thread-management subsystem is likely to use the thread-terminate notification.


Subsystem identifiers/​indices

Each instantiation of a subsystem has a system-wide unique identifier, a subsystem index, allocated when the subsystem registered with the context switcher (through Context​Switcher_​Register​Subsystem). The subsystem uses it in all correspondence with the switcher.


Subcontext pointers

The switcher maintains state per process (i.e. the process's context), including its own internal details, plus optional state per subsystem (i.e. subcontexts). A subcontext is identified by its address, and a subsystem is responsible for maintaining its subcontext, and is informed when contexts are forked or exited — hence each of its subcontexts must be forked or exited by the appropriate subsystem. An operation is provided to allow a subsystem to obtain its subcontext pointer for the current context, Context​Switcher_​Lookup​Context, and it should be a very fast operation.


Processes and threads

Each process has a system-wide unique identifier allocated when the process begins. The process comprises state held about it by each subsystem and the switcher, including its memory mapping and threads. Thread identifiers are unique only within their process. A system-wide thread identifier is therefore a combination of process identifier and thread identifier.


Implement­ation of threads and processes

The system maintains several processes, each with one or more threads. Each thread is held as a copy of its supervisor stack — to switch threads, the old thread's stack is copied (or paged?) away, and the new thread's stack replaces it.

Question: Isn't that inefficient?

Answer:

Maybe, but such stacks won't be very big anyway, and maybe some page-swapping can be done. What happens with the current WIMP?


Question: Will this stop multiprocessor support?

Answer:

Perhaps. But you might be able to get two processors to use the same virtual address space for the SVC stack, I suppose. You'd have to be able to do something similar with application memory anyway, wouldn't you? – Each processor would have its own, independent, logical-physical memory mapping, right?


An application starts a new thread by setting up an appropriate application-space stack, and calling Thread​Manager_​Spawn with the necessary registers. The thread manager can then use Context​Switcher_​Spawn to create a new stack record, which will eventually be switched in.

To terminate the thread, the application must release its application stack memory, and call Thread​Manager_​Finish. This should happen, whether the thread calls an explicit thread-termination function within the application, or the thread simply returns from its original function. The thread manager then calls Context​Switcher_​Finish to delete the corresponding SVC stack record, allowing another thread to resume.

An application duplicates itself by calling Thread​Manager_​Fork, which causes the thread manager to call Context​Switcher_​Fork. Each of the process's threads are duplicated, as is its state within the switcher and within each subsystem (by a call to Subsystem_​Fork). There are now two threads in the call, and one will return immediately, while the other in due course.

Question: What happens to duplicated threads blocked on the same call?

Answer:

Dunno. We could get the child thread to return immediately with an error (rather like EINTR).

This could be of importance when a process with WIMP-registered thread (i.e. task) is forked. The task status of the parent thread remains, but the child has to assume that it is not a task, or it has to update all its WIMP handles, which are supposed to be globally unique.


When the last thread in a process terminates (always by Thread​Manager_​Finish), or the process calls Thread​Manager_​Exit (or OS_Exit, which the thread manager should set up to call Thread​Manager_​Exit?), the process has finished. The thread manager then calls Context​Switcher_​Exit, which calls Subsystem_​Exit on each of the subsystems, but does not return.