So it's going to get confusing quickly trying to describe multitasking by trying to show code both from the OS/scheduler and the user/task side. In preemptive, like QDOS, it is independent of the user/task side and will interrupt a program regardless of where it is, with some caveats of critical system functions like I/O (and of course keeping granularity in mind -- i.e. it's not going to context switch after having printed half of a character and I don't believe any OS really does that). In a cooperative system, the user/task side gives control away and the way it was done originally was not by some arbitrary system calls but through a practical means.
Both Windows and the Mac OS were graphical user interfaces before they added multitasking and so had to deal with events (a completely new way of thinking about code compared to the old "print a menu and give me a choice" approach). Thus each had to continually ask the OS what the next event was via a GetNextEvent call. So most proper behaving code looked something like the code below (even before multitasking existed)...I threw out some initialization commands but the cooperative entry point is GetNextEvent:
Code: Select all
...
while (GetNextEvent(everyEvent, &myEvent))
{
switch (myEvent.what)
{
case mouseDown:
windowPart = FindWindow(myEvent.where, &whichWindow);
DoMouseDown(windowPart, whichWindow, &myEvent);
break;
case keyDown:
case autoKey:
{
register char theChar;
theChar = myEvent.message & charCodeMask;
if ((myEvent.modifiers & cmdKey) != 0)
return(DoCommand(MenuKey(theChar)));
else if (noCTRL)
KeyDown(theChar);
else
{ gEvent = myEvent;
TESetSelect((**TEH).teLength,(**TEH).teLength,TEH); }
}
break;
case activateEvt:
if (ours((WindowPtr)myEvent.message))
{
if (myEvent.modifiers & activeFlag)
{ TEActivate(TEH);
ShowControl(vScroll);
DisableItem(myMenus[editM], undoCommand); }
else
{ TEDeactivate(TEH);
HideControl(vScroll); }
}
break;
case updateEvt:
if (ours((WindowPtr) myEvent.message))
UpdateWindow(myWindow);
break;
}
}
...
So the use of GetNextEvent in MacOS (and something similar in Windows 1 and 2) made adding a cooperative kernel simple and likely why both operating systems went that route initially. The cooperation was implicit and thus required little change. Of course, this depended on the application being one that was very interactive, requiring lots of user input (and the thought back then was for user productivity software and graphics stuff so one expected most users to constantly be adding input as opposed to running batch jobs). When I had to port my interpreter, the problem was that context-switching only happens before something is run and I had to add in GetNextEvent() calls in the parsing loop itself (mainly looking for CTRL-S, CTRL-Q, and CTRL-C) to relinquish control and when I didn't it would hang the system.
I just did a couple of videos on it. The first can be found here and I'm posting the second tomorrow:
https://www.youtube.com/watch?v=atnmVFByt-U
The first is more of an overview but in the second I dive down into some C code on both the QL and the Mac OS to show the difference (and somewhat insane way one had to code in a cooperative environment).
I would also not call QDOS a hybrid preemptive/cooperative OS as it's preemptive regardless of how it preempts the code. As long as you the writer of code doesn't have to worry about how you are context-switched out then you aren't cooperating and thus it's not a cooperative OS. A simple test that you can do on a QL is write the following loop in a C program:
This will not freeze the QL up. Note that Digital 'C' SE is an implementation of Small C so there is not something funky going on with its implementation to make it QL-like and, in fact, it would be impossible to take an off-the-shelf compiler and somehow add system calls into every possible keyword.