SpyderTL wrote:My gut tells me that your approach should work well for a single application, but may become an issue when you start trying to run two applications at once.
Lets focus on the multi-threaded issue first. The type of processor you are using makes little or no difference to the way multiple threads work. One thing to keep in mind here is that one processor can only run one program at a time. (Modern processors actually can, but they must be configured and enabled by the OS, so you can safely ignore this functionality for now...)
So, keeping in mind that your CPU can only run one application (or thread) at a time, how does multi-threading work? Before we answer that, let's also assume that we want to support running multiple applications/threads with little or no impact on the application itself. In other words, a running program doesn't need to know that it is running in a multi-threaded environment. As far as it knows, it has complete control of the CPU from the time the application starts to the time that it finishes.
We also don't want to "trust" the application to share the CPU with the other applications, because most programs won't. So how do we pull this off with all of these limitations? The OS needs a way to "freeze" an application and "unfreeze" and application without it noticing anything at all.
As I mentioned above, by default, you will get roughly 18 interrupts a second from the PIT timer. Each time this interrupt is handled, the currently running application is "interrupted" (or "frozen"), and the address of the next instruction is stored on the stack. Then the CPU finds the address of the correct interrupt handler, and sets the IP register to that address, and continues running from there.
That interrupt handler is responsible for "handling" the interrupt, and then setting the IP register back to the original program's next instruction.
So, what if the interrupt handler loaded a different address into IP, instead? The CPU would simply "resume" (or "unfreeze") using that address, instead. So, let's say that we had two programs running. The first program would run just fine until the PIT timer interrupt fires. Then the IP register would be stored on the stack, and the interrupt handler address would be copied to the IP register, and the CPU would start executing the handler. The handler could replace the address on the stack with the "next" instruction address from the second program, and instruct the CPU to resume the application.
The second application would run until the next PIT timer interrupt, which would store the new "next" instruction address to the stack, and call the interrupt handler again. The handler could swap the "next" instruction address for the first application on the stack, and then resume the application again.
Using this flip/flop approach, each application would be paused and resumed around 9 times a second. This would probably by pretty noticeable to the user, so you probably don't want to use the default PIT timer duration of 18 times a second. You can actually reprogram the PIT timer to interrupt the CPU more often, up to about 20,000 times a second, I believe. However, at this rate, your applications will get almost no CPU time, and your interrupt handler will get nearly 100% of your CPU time, so a value in the 1000-2000 interrupts per second is probably more appropriate.
So the only problem left is that both running applications will be using the CPU and its registers. Simply pausing one application and resuming the other will mean that one application will be able to change the registers for the other application. In order to prevent one application from affecting another, the entire CPU state must be stored in memory when the application is "frozen" and reloaded before the application is "unfrozen". If done properly, the application will not even noticed that it has been interrupted.
The actual details about how all of this is accomplished is up to you, but this should give you enough information to get you started. If you run into any problems, just let us know.
As for your Keyboard events, I would start out with just one application that checks the keyboard controller in a loop, and have it HLT if the keyboard buffer is empty. The keyboard event will cause your application to "wake up" after the HLT instruction, and you can check the keyboard buffer again. As I mentioned before, other events will also "wake up" your application, so you will need to check the keyboard buffer to see if it actually has data in it.
Later on, when you need support for multiple applications, I would then replace your interrupt handler with one that can check the keyboard buffer, and copy any data in it into memory. Then I would change the application(s) to check that memory address instead of the keyboard buffer. Eventually, you will need to get creative and figure out how to notify multiple applications of a single keyboard event, so each application may need its own event queue, which will be filled by the OS, and processed by the application the next time it is "awake".
Wow... forgive me if it takes a few rounds of communication for me to process it fully; that's a complex system.
What I'm given to understand is essentially that all the processes form a 'ring' or barrel structure if you will. This draws analogies to how some of the ST registers in x87 operate, but I won't pollute the discussion with that. The process scheduling core or routines rotate this barrel around on the tick of the timer, loading and unloading the CPU environment and registers from the stack.
Needless to say this seems like it involves a lot of the PUSHAD/POPAD instruction calling, at least in the case of i386/IA-32. There are two difficult scenarios I can think of -
1) Program entry or program exit. The opening or closing of a running process would require extra complexity on the part of the scheduler. (I'll use the term "barrels" in this analogy because it fares better with my mind at this early point; thinking of the system as a revolver) If you number the barrels/processes 0-7 (8 total), with 0 starting execution first, and rotating through 7 back to 0, the stack will contain the minimum of program states during that transition, and so the optimum time to add a process would be when 7 goes back to 0, and the optimum time to close a process out would be to simply delay the transition of that process to the next one and clean its traces off the stack immediately. Thus, closing a process involves less latency than adding one in this method.
Of course, this (in the case of the opening of a new process) would hinge on a delay until the "barrel" reaches its top-dead center; the position where it started. The delay system would be easier to implement, and the other scenario, which would be asynchronous or real-time loading, would be more difficult.
2) How to manage the stack once the number of processes rotates through. Once process 0 suspends and process 1 resumes, if I understand correctly the entire CPU state (all registers) will be pushed onto the stack, meaning that process 1 will operate with process 0's state on the stack. Once process 7 suspends to allow process 0 back, all 8 processes' states will be on the stack. Ideally, this is a simple fix. At first, it would be a routine assumption to just roll back the stack pointer either 18 bytes (for x86-16, 8 registers plus IP/EIP, 36 for x86-32) and load out.
The issue with this is that because it is occurring on an interval not controllable by the processes (completely beside the concept of the rings and privilege level), the individual processes wouldn't have a say in when they get switched out. The problem here is that if one of those processes commits a stack operation (pushf, for example) and switches out before it gets to reverse the operation, the stack will be misaligned and the entire process stack is corrupted.
In terms of my interpretation of such a system, is it on the right track? What would your ideas be on how to overcome these issues?
In terms of my priorities for my OS's development, first I want to prove I can build an operating system capable of being roughly "sentient"; that is, it can manage to spawn and run a program in a manner that wasn't specified by its coding; that it could pass the baton to a completely-independent program based on user input alone as opposed to just a linear string of massive subroutines.
Once I have that and if I'm able to make significant inroads in a workable multitasking/scheduling system I'll probably be convinced I have something worth talking about and I'll work on porting it out of the AT hardware and onto generic x86 systems. But that's many months away, even if I get the devil's own API.
I'm still drawing blanks on the whole interrupt system and handlers (as that's the problem I have right in front of me, at least in the short-term), but you have my interest piqued with the multitasking/task/process switching talks and so let's continue that.