Skip to content
Lesson 19 of 24

Step 1 of 5 · Reading · ~4 min

Read

System Calls & I/O

The Timer & Preemption

A scheduler can only take the CPU away from a process if something forces control back into the kernel. A running user program makes no promises to give the CPU up, so the force is hardware: a timer that interrupts at a fixed rate. Every tick enters the kernel, which can then decide that the running process has had enough. In this lesson your program programs the classic PC timer through its real I/O ports, counts its ticks as time, and uses them to preempt tasks.

The 8253/8254 PIT

The Programmable Interval Timer has an input clock of 1,193,182 Hz (the 14.31818 MHz crystal of the original IBM PC divided by 12). Channel 0 counts down from a 16-bit divisor and raises IRQ 0 each time it reaches zero, so:

interrupt rate = 1193182 / divisor

A divisor of 0 means 65536, giving the default of about 18.2 interrupts per second that DOS machines had. To get roughly 100 Hz the OS needs a divisor of 1193182 / 100 = 11931.82, which must be rounded to 11932. Rounding is not a nuisance but a lasting error: the real rate is 1193182 / 11932 = 99.9985 Hz, so a clock built by counting ticks as exactly 10 ms drifts. Accurate kernels do their time arithmetic in PIT counts.

Programming it through ports

The PIT is controlled with out instructions to I/O ports, not memory:

PortUse
0x40channel 0 data
0x43mode/command register

The command byte is CC AA MMM B:

BitsFieldValue used by kernels
7-6channel0
5-4access3 = low byte then high byte
3-1operating mode2 = rate generator, 3 = square wave
0BCD0

So 0x34 is "channel 0, low byte then high byte, mode 2" and 0x36 is the same in mode 3. After the command, two writes to port 0x40 supply the divisor: low byte first, then high byte. The controller keeps a flip-flop for which byte comes next, and a new command byte resets it to "low". For 11932 (0x2E9C) the sequence is out 0x43, 0x34, out 0x40, 0x9C, out 0x40, 0x2E.

From ticks to preemption

The timer handler runs once per tick. It adds a tick to the global count, subtracts one from the running process's remaining time slice, and when that reaches zero asks the scheduler for another process. This is round robin from lesson 8, with the quantum counted in ticks and the preemption triggered by the interrupt from lesson 17: the process that was running is moved to the back of the queue with a fresh slice. With one runnable task nothing changes hands, only the slice restarts.

Kernels pick the tick rate (Linux calls it HZ: 100, 250 or 1000) as a trade-off: more ticks mean finer scheduling and timers but more interrupt overhead. Modern kernels avoid ticking on idle CPUs ("tickless") and use the local APIC timer or HPET instead of the PIT, but the structure is unchanged.

Worked trace

Divisor 11932 and tasks A (slice 3 ticks), B (2 ticks):

TickRunning beforeSlice leftEvent
1A2
2A1
3A0preempt: A -> B, A goes to the back with 3
4B1
5B0preempt: B -> A

Uptime after 5 ticks at 11932 counts per tick is 5 * 11932 * 1000 / 1193182: 59,660,000 / 1,193,182 is 50.0007, truncated to 50 ms.

Common mistakes

  • Sending the high byte first, or not resetting the byte order flip-flop after a new command.
  • Treating divisor 0 as 0 instead of 65536.
  • Converting ticks to time with the requested frequency instead of the divisor that actually got programmed.
  • Rotating the task when its slice reaches zero but forgetting to reset the slice of the task that was preempted.

Your exercise

The starter parses the port writes and tick commands. You write the PIT register logic and the tick handler.

Up nextKeyboard Input & ScancodesSystem Calls & I/O

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…