First off: an OS is nothing magical. It's code like yours or mine. I've written one (a small one), you can too. Waste of time though, since Linux already exists and it does more than you or I could write.
You have a CPU. It runs instructions. It doesn't know about processes. It doesn't even know about functions. Or lines of code. Or the screen. It just does one instruction after another, forever.
Cores are CPUs. You think have a 4-core CPU, you actually have 4 CPUs in one chip.
The CPU is hard-wired so it starts by running code from the BIOS chip on your motherboard and that chip is hard-wired so the CPU can directly run code from it without loading. Well, one of them is. The other 3 are hard-wired to do nothing. That's how you can still run MS-DOS (maybe) and waste 3/4 of the CPUs you paid for.
The BIOS code looks at all your hard drives, your CD drive, your USB ports, finds a signature that says "hey this drive has an OS on it", loads that and jumps to the address it just loaded. The old-school MBR system is just the first 512 bytes of any drive, ending with hex codes 55 AA to say that it's bootable. Since it's only 510 bytes you normally put here a program that loads more bytes. The more modern
ChatGPT system is more complex and the BIOS loads all the bytes you want, not just the first 510. No point going into detail here.
With either MBR or GPT, once the BIOS loads these bytes and then jumps to their address, the computer is all yours to do as you wish. Usually what it just loaded is another loader which loads the operating system, but that's a historical accident, not something necessary. (Well, it's necessary in MBR if your operating system is more than 510 bytes big, which is the historical accident)
You have 1 CPU running whatever code you like, and 3 CPUs still asleep. For the time being, let's see what you can do with just 1 CPU.
What code do you like? It can be anything. Use your imagination. But let's say you want to make something like Linux but more basic. Linux has a whole lot of parts. You asked about one specifically. You asked about the scheduling.
There's a little magic trick to scheduling that makes it all understandable. The magic trick is that CPUs are extremely forgetful. See, the CPU contains a bunch of registers - which is to say very small memory circuits, the hardware equivalent of variables - different ones depending on which kind of CPU you have, but on x86 you have registers like eax, ecx, edx, ebx (yeah the order is ACDB, don't ask what Intel were smoking), esp, eip, cs, ds, es, and a bunch more. Some of them are general-purpose; some have very specific purposes. And those are the only things the CPU can remember. No, it can't remember the current process ID. No, it can't remember what it was doing 2 instructions ago. No, it can't remember whether it's been running this loop for 50000 iterations or whether the loop counter started at 50000. It's Last Thursdayism on a microscopic scale.
So if you want to swap threads all you have to do is save all the data in the registers into memory (you could call that place
struct thread) and load them from somewhere else in memory (a different
struct thread) back into the registers. Now the CPU continues running the second thread just as if it had been doing it all along. This is called a context switch. In fact, you can already do this in your own program without OS help - on Linux there's a supported library function called
Keeping track of which thread is currently running is your problem, not the CPU's.
But how does it happen without the thread calling
swapcontext? Enter: interrupts. One of the ways that hardware talks to the CPU is by signalling an "interrupt request" which tells the CPU to stop what it's doing and run some OS code - it makes the CPU do an automatic context switch.
The details vary depending on the CPU type. x86 CPUs have quite complicated interrupts, so let's talk about ARM which is much simpler. When an ARM CPU is running some code ("user code") and gets an interrupt request, it only does a few simple steps:
- it notes the current instruction address (in ARM terms that's
- it activates the "interrupt mode" bit in the CPU status register
- it stores the current instruction address in register
R14_irq which is only used in interrupt mode
- it jumps to the instruction at address
0x00000018 (which is hard-wired)
Of course, there's also a
interrupt instruction which does the opposite. Of course, the operating system will have made sure that address 0x00000018 has OS code, so the operating system now has control of the CPU and can run whatever code it likes.
Clearly it's not a full context switch. Only the current instruction address is saved and updated. But that's enough for the OS code to do the rest of it. It isn't possible to context switch the register R14_irq properly, because the old value in that register got overwritten. Nobody notices, though, because user code can't touch that register, and OS code deliberately doesn't touch it except when it's processing an interrupt.
One hardware device that creates interrupts is a timer, like an alarm clock. The OS sets the alarm clock before it starts running code from a process. When the alarm clock rings, it makes an interrupt and then the CPU is running OS code once again, even if the process got into an infinite loop.
An OS can run multiple processes on one CPU by cycling through them and setting short alarms (few milliseconds, or less). Of course, the OS also knows which processes don't want to run (which is most of them, most of the time) and skips over those.
Other than timers, interrupts are also used for all sorts of hardware, especially inputs like mice and keyboards so that the OS can do something immediately when you press a key.
Multiple processors. What about the other 3 CPUs? Actually that one's easy - you use the main CPU to send them some specific signal (just like you send signals to disk drives or keyboards) that tells them to turn on and what code address to start from.