Everybody Must Play Nicely
The simplest way of sharing the CPU is known as cooperative multitasking. This approach was used in Windows 3.x and earlier, Mac OS Classic, and RISC OS, among others. Each process is allowed to use the CPU for as long as it wants, and then return control to the operating system. This technique typically works well for event-driven systems responding to user input. When you interact with the user interface, the active program handles this interaction and then passes on the control. The downside of this approach is that it requires applications to be well-behaved. An application that’s doing something processor-intensive for a while needs to remember to yield the CPU periodically, or the entire system will freeze.
The solution to this problem is preemptive multitasking, which isn’t a particularly new idea: It has been around in UNIX since the start, and UNIX wasn’t the first operating system to provide it. Preemptive multitasking has become standard in desktop operating systems only in the last decade or so, though. Most desktop operating systems are designed around running one foreground application and quickly switching to others, while multiuser systems allow lots of applications to run at once without impacting each other’s performance.
Preemptive multitasking uses a timer interrupt to return control periodically to the operating system kernel. This interrupt typically is configured to fire every 10 milliseconds, at which point the CPU switches to protected mode (ring 0 on x86) and runs the clock interrupt handler. The kernel then saves the state of the running process and switches to the next one.
The part of the system responsible for determining which process should run next, and for how long, is the scheduler. Remember that the timer interrupt fires every 10 ms in a typical system, but not every process can run for 10 ms. A 2 GHz CPU runs at two billion cycles per second, or two million cycles per millisecond. This means that a process will be allowed to run for twenty million cycles, which is enough time to do a lot of work. A lot of processes won’t have anything to do after this point, and therefore will yield control to the operating system.
There are typically two ways of passing control:
- On UNIX-like systems, you can explicitly yield the CPU with the sleep() system call. If you pass 0 as the argument, the system sleeps for zero seconds, which is interpreted as meaning "Yield the CPU now, but give it back as soon as other processes have had a go."
- Issue a blocking system call such as read(). This call puts the calling process to sleep until some data is available to be read from the file descriptor. (In UNIX-land, that could be a network socket, pipe, etc.) This method is commonly used for GUI programs, which receive events over a pipe or socket, and thus consume no CPU time while they’re waiting for events.