Here the overview of changes , which i made to VM to support an image-based scheduler.
In order to support an image-based scheduling i introduced a following primitive, which allows to switch the active process explicitly:
“sets an ActiveProcess to new process,
sets an InterruptedProcess to the process which was active
set a ProcessAction to anAction object
| process action |
self export: true.
self hasNewScheduler ifFalse: [ ^ self primitiveFail ].
action := self popStack.
self storePointer: ProcessActionIndex ofObject: self schedulerPointer withValue: action.
process := self popStack.
self transferToProcess: process
As you can see, this primitive takes two arguments. Here the language-side method which using it:
primitiveTransferToProcess: newProcess action: anAction
Sets an activeProcess to new process,
sets an interruptedProcess to the process which was active
set a scheduler’s action ivar to anAction object
<primitive: ‘primitiveTransferToProcess’ module: ”>
The primitive will fail, if image (by some occasion) calls this primitive while having an old scheduler installed.
A new scheduler should have additional instance variables:
You may ask, why i made the action as additional argument while in order to switch the active process we need only single parameter – the process which should be activated. The answer is, that sometimes we need to pass an argument to newly switched process, and second is to ensure atomicity when passing this argument. This functionality is used by scheduler to ensure atomicity of different operations.
Otherwise, we are risking, when passing an action argument to scheduler, because the process which setting it, can be preempted before entering interrupt process, by other process, and action will be overridden and therefore lost:
Processor setAction: [ do something here ].
“we can be preempted here”
Usually, to evaluate the piece of code inside an interrupt process, most of the code is using an #interruptWith: method which implemented as following:
” Transfer control to interrupt process.
A process which is being interrupted is put into interruptedProcess ivar.
activeProcess == interruptProcess ifTrue: [
“We don’t allow nested calls of this method.”
self error: ‘Nested call to #interruptWith: detected!’
^ self primitiveTransferToProcess: interruptProcess action: anAction.
Here the example, how we using this method:
“Give other Processes at the current priority a chance to run.”
self interruptWith: [
| list |
list := quiescentProcessLists at: interruptedProcess priority.
list addLast: interruptedProcess.
interruptedProcess suspendingList: list.
self wakeAtPriority: interruptedProcess priority.
An interrupt process is a special process, held by sheduler and runs an infinite loop, which
– performs an action (if any), passed to interrupt process
– handles external signals
– performs scheduling
at the end of each cycle, it switching back to interruptedProcess (using the above primitive). So, if there in no-one changed the interruptedProcess , it will continue running the interrupted process.
A second primitive is used to fetch pending signals from VM’s semaphoresToSignalA/semaphoresToSignalB buffers directly to language side, so then it could handle signals by itself without the need from VM to even know about existance of such objects as semaphores.
“primitive, fill an array (first argument)with pending signals, stored insemaphoresToSignalA/semaphoresToSignalB buffers.
Returns a number of signals being filled or negative number indicating that array is not big enough to fetch all signals at once.
Primitive fails if first argument is not array.
This primitive is used by interruptProcess to fetch all pending signals from VM and to handle them.
This is mainly all what we need from VM in order to be able to implement own scheduling semantics at language side, without being dependent from VM too much.
The interaction between language side and VM with new scheduler don’t requires from VM to have any knowledge about semaphores. VM operates with only integer values (signals), and scheduler is free to interpret them as it likes to.
The language side passing integer objects to different VM primitives, and VM emits signals using these values. A scheduler then is using a new primitive (primitiveFetchPendingSignals) to fetch the list of pending signals in its interrupt process.
Now, since we lifted the responsibility of interpreting signals from VM side to language side, we could use not only Semaphore to handle the signal, but also any other Object. Scheduler is taking a signal integer value, as an index in its signalHandlers array, then simply sends #handleExternalSignal to an object stored in this array at given index. By default, Object>>handleExternalSignal does nothing, while Semaphore>handleExternalSignal is either awaking the process which is waiting on semaphore or increments an excessSignals value, if there is no processes waiting for it.
This effectively allows us to move scheduling semantics from VM to language side, and free to change it in the future without the need of any changes in VM.
The current Squeak VM provides an API function for plugins (signalSemaphoreWithIndex: ) which could be used by plugins to signal a semaphore stored in external objects table (Smalltalk externalObjects == Smalltalk specialObjectsArray at: 39). This means, that all plugins already operating with integer values (signals) and they can’t really interact with semaphore objects, so we don’t need any changes here, except that instead of directly signaling a semaphore object in external semaphores table by VM, we let a new scheduler to interpret this signal at language side.
In Interpreter, however, there are some places which operating with semaphores directly. So, all what we need now is to review all such places in order to unify this.
Here the list of semaphores used internally by Interpreter:
TheLowSpaceSemaphore (Smalltalk specialObjectsArray at: 18)
TheInterruptSemaphore (Smalltalk specialObjectsArray at: 31)
TheTimerSemaphore (Smalltalk specialObjectsArray at: 30)
TheFinalizationSemaphore (Smalltalk specialObjectsArray at: 42)
Instead of using a special objects indices for signaling semaphores, lets reserve a first four indices in signalHandlers array, kept by scheduler, and use following constants to identify them:
LowSpaceSignal := 1.
InterruptSignal := 2.
TimerSignal := 3.
FinalizationSignal := 4.
Now , lets review each of them one by one, what places need to be changed:
checkForInterrupts method will use:
signalLowSpace ifTrue: [
signalLowSpace := false. “reset flag”
self addPendingSignal: LowSpaceSignal ].
The primitiveLowSpaceSemaphore is now obsolete, since VM don’t needs to signal semaphores directly – it just passing this signal in primitiveFetchPendingSignals to let scheduler handle it.
An addPendingSignal: is a service method, which stores a new signal to semaphoresToSignalA/semaphoresToSignalB buffers, in same manner as signalSemaphoreWithIndex: does, except that its not sends forceInterruptCheck. As you may guess, i changed signalSemaphoreWithIndex: method to a two-liner:
“Record the given semaphore index in the double buffer semaphores array to be signaled at the next convenient moment. Force a real interrupt check as soon as possible.”
self addPendingSignal: index.
Also, in scheduler we add a convenience method:
self performDuringInterrupt: [
signalHandlers at: LowSpaceSignal put: anObject ]
to register an object, who will take care for handling this event. It can be semaphore or something else – we don’t really care at this moment. A #performDuringInterrupt: used to ensure that given enclosed code will run atomically and can’t be preempted.
change in checkForInterrupts method:
“reset interrupt flag”
interruptPending := false.
self addPendingSignal: InterruptSignal ].
again, same as with TheLowSpaceSemaphore, the primitiveInterruptSemaphore is obsolete, and we will add the convenience method to scheduler: setInterruptSignalHandler: anObject
in checkForInterrupts, we will use
self addPendingSignal: TimerSignal
instead of signaling a semaphore:
sema := self splObj: TheTimerSemaphore.
sema = nilObj ifFalse: [self synchronousSignal: sema]
Next, a current primitiveSignalAtMilliseconds takes a two arguments: the millisecond clock value, when timer event should be signaled and a semaphore to store at TheTimerSemaphore index in special objects array.
Since, now we passing a signal value (TimerSignal constant) to language side to handle it, the need in having an object to be signaled is redundant, that’s why i made a new primitive for this:
“Cause the timer to be signaled when the millisecond clock is greater than or
equal to the given tick value. A tick value of zero turns off
| tick |
self export: true.
tick := self popInteger.
ifTrue: [ nextWakeupTick := tick ]
ifFalse: [self unPop: 1]
as you can see its now extremely simple 🙂
A new Delay implementation will make use this new primitive instead of old one.
Again, we add a convenience method setTimerSignalHandler: anObject to new scheduler. In contrast to previous implementation, we don’t need to change the handler object during run time. We set it only once at image startup phase – it will be a Delay class, which on receiving this signal will perform a regular procedure for signaling expired delays, if any. But i don’t want go in detail about this now, since this post dediceted to describe the VM-side changes.
in checkForInterrupts we will use:
pendingFinalizationSignals > 0
pendingFinalizationSignals := 0.
self addPendingSignal: FinalizationSignal.
fortunately, there is no special primitive for setting the finalization semaphore, so this is all what we need to change in VM.
The changes is made in such way, that VM will stay compatible with old image, which don’t use new scheduler, but if it does, then its using different code. You can browse all senders of hasNewScheduler message to see where code takes different route in a presence of new scheduler.
This method simply checks , that a Processor special object having additional slots:
“the old scheduler using just two instance variables”
^ (self lastPointerOf: self schedulerPointer) >= (ProcessActionIndex*BytesPerWord + BaseHeaderSize)
Now, here the list of methods/primitives which is become obsolete and can be removed if we don’t need to support backward compatibility:
- addLastLink: proc toList: aList
- isEmptyList: aLinkedList
- putToSleep: aProcess
- removeFirstLinkOfList: aList
- resume: aProcess
- synchronousSignal: aSemaphore
i marked these methods with
self flag: #obsoleteWithNewScheduler.
to make it easy to find them in future.
In next post i will cover the language-side changes.