The core of the system
The switcher keeps track of processes and threads, allowing them to be created and destroyed. It also allows one thread to yield control to another, ensuring that the context of the first thread is swapped out and replaced by that of the second, if necessary.
An application does not talk to the switcher directly. One of its threads calls in to one of the subsystems, then that subsystem decides whether to talk to the switcher, usually because it knows that it cannot complete the call immediately, and is perhaps waiting for some hardware (e.g. a network interface or a disc drive) to respond.
Under the control of the subsystem which a thread has called in to, the thread offers to yield control through a ContextSwitcher_Yield call, which might block until the switcher grants control back to that thread. In the meantime, the switcher may have granted control to several other threads in turn.
Before yielding, the controlling subsystem must set up any interrupt routines and internal structures to determine the earliest moment when the switcher can return control. In particular, it must reserve a word [Edit: zero-initialised?] to be used as a release handle, and pass its address to ContextSwitcher_Yield, which the switcher will fill in. When the interrupt comes in, and the subsystem has determined that the thread's call into it can complete, it passes the release handle to ContextSwitcher_Resume, allowing the switcher to mark the thread ready to continue.
When the switcher detects that all threads are suspended, the kernel will be idle, so it should try to clear the transient-callback list. This will allow interrupts to call ContextSwitcher_Resume indirectly by setting up transient callbacks.
Here's some code to do this, taken from The Callback Hack for network filing systems:
usermode_donothing
SUBS ip, lr, #0 ; ip := lr; set C - sneaky
TEQ pc, pc
TEQNEP pc, #0 ; ->USR26, Z still clear, IF cleared
MSREQ CPSR_c, #2_00010000 ; ->USR32, I32/F32/T cleared
MOV r0, #0
MOV r1, #1
SWI XOS_Byte
SWI XOS_EnterOS
[ {CONFIG}=26
MOVS pc, ip
|
MOV pc, ip
]
Only one SWI is defined for accessing the context switcher, and it is only to be used by subsystems as modules, not by applications, as a rendezvous point. All other subroutines are SVC calls, and are provided to the caller through the SWI routine.
| Type | Routine |
|---|---|
| SWI operation | ContextSwitcher_RegisterSubsystem |
| SVC subroutine | ContextSwitcher_Yield |
| SVC subroutine | ContextSwitcher_Resume |
| SVC subroutine | ContextSwitcher_LookupContext |
| SVC subroutine | ContextSwitcher_Exit |
| SVC subroutine | ContextSwitcher_Fork |
| SVC subroutine | ContextSwitcher_Spawn |
| SVC subroutine | ContextSwitcher_Finish |
Hook a new subsystem into the context-switching architecture
A subsystem is registering its interest in context switching. This operation allows the switcher and the new subsystem to exchange subroutine entry points, and the subsystem will be allocated a free subsystem index to be used in future reference with the switcher.
On entry, the first R3 words of the block pointed to by R2 should be the entry addresses of the corresponding first R3 subroutines of the following sequence:
Unused entries must contain 0, or be beyond the specified size of the table.
In return, R2 will point to a table of R3 switcher subroutines, in order:
This table is persistent, and shared by other systems. It must not be altered.
Indicate that the current thread is waiting for I/O
The calling subsystem is yielding control on behalf of the process that called it. The call may therefore not return immediately.
When it does return, the release handle can be considered invalid.
The subsystem reference is returned by Subsystem_Terminate, so that subsystem can locate its state pertaining to a blocked thread that has been aborted.
[Edit:
Another possibility is that the switcher will simply allow the terminated thread to return from this call with some indication that it has been terminated. The subsystem would detect this, and release its resources, but not return to the application. Instead, it would make a non-returning call back to the switcher, which could then delete the thread's stack and select another thread.
]
Place a waiting thread in a scheduling queue
The calling subsystem wants to resume one of its callers' threads. The thread is placed on a queue identified by the priority level. This call may be used to alter the priority of a pending thread, or set it back to the blocked state (priority -1).
Like other SVC subroutines, this routine must not be called in an interrupt mode. The interrupt routine should arrange for an SVC call to be made to a subsystem routine as soon as possible after the interrupt is finished, e.g. by OS_AddCallBack. This ensures that the ContextSwitcher_Yield call will have had time to write its release handle into the supplied word — the callback will not occur between the word being allocated and the handle being written.
Identify the subcontext for the current thread
The calling subsystem wants to locate its subcontext within the current context.
Duplicate the current process
The calling subsystem wants to cause the calling context to fork.
The call will return twice, once to the original thread with the process identifier of the new thread, and once to the new thread with a null identifier.
Terminate the current process
The calling subsystem wants to cause the calling context to exit.
Start new thread in current process
The calling subsystem wants to start a new thread in the current process. A block of initial register values must be provided, including the entry address in R15. The code will be entered in USR mode, and should return according to the managing subsystem's specification, e.g. with ThreadManager_Finish, which will then call ContextSwitcher_Finish on the thread's behalf.
Terminate the current thread
The current thread is terminated. If this is the last thread in the process, ContextSwitcher_Exit is called with an exit code of zero.
[Edit:
Presumably, if a thread is terminating itself, it can call the appropriate SWI after releasing its stack.
]
[Edit:
If a thread tries to terminate itself using a non-zero identifier, the attempt could be ignored. That would allow the exec code in the thread manager to easily terminate all threads except the one running, so that it can be used to initiate the new process image.
]
Question: What about terminating a particular thread?
Okay, yes. This call should take an argument giving the process-scoped thread identifier. [Edit: Now added.]
We don't want to do something similar for processes, though, do we? Process termination comes from within the process, while thread termination can come from anywhere in the controlling process, i.e. another thread.
But we should still have a way to force a process to abort, like kill -9.