CS 134

Handing I/O

  • Hedgehog speaking

    In the OS/161 codebase and the homework, it talked about interrupts. I sort of have a bit of an idea of what they are, but it's pretty fuzzy. Can you explain them to me?

  • PinkRobot speaking

    Totally!

Before There Were Interrupts

Here's a diagram of the hardware of an early computer system (modern ones are much the same, although the I/O devices would be different—your phone has a touch screen, not a tape drive).

Basic Computer Hardware

Basic Computer Hardware
Image credit: Silberschatz and Galvin, Operating System Concepts

The key aspect is that the CPU can talk to both memory (where it gets its instructions and data) and I/O devices (like disks, tape drives, and printers). All the hardware is connected together over a common communications channel known as the system bus.

  • Rabbit speaking

    In early computers there was a single shared bus and only one device could talk at a time. Today's machines have multiple separate busses, typically PCIe for high-speed devices like graphics cards and NVMe SSDs, and USB for slower devices like keyboards and mice, with memory being connected to the CPU over its own separate bus (or busses).

  • PinkRobot speaking

    True, but that doesn't really matter for our discussion. We can imagine a single shared bus for simplicity.

In early computers (without much, if any, of an operating system), the easiest way to work with an I/O device was to ask it to do something and then wait for it to be done.

Python pseudo-code for this would look like

def read_from_disk(diskno, sectorno):
    disk_controller.send_command(cmd="read", disk=diskno, sector=sectorno)
    while not disk_controller.is_done():
        pass
    return disk_controller.get_data()

This approach is known as busy waiting, because the program is essentially repeatedly asking, “Are we there yet?” until the disk controller says, “Yes”. Below is a timing diagram of this process.

Busy Waiting

Busy Waiting

Looking at the busy waiting diagram, what are some issues with this approach?

Polling Is Inefficient

The problem with busy waiting is that it's inefficient. The CPU is doing nothing while waiting for the disk controller to finish, which is a waste of CPU time that could be used to do other work.

Also, if the CPU constantly polls the I/O device, it would dominate the system bus and perhaps distract the disk controller from doing its work. So the CPU generally needs to wait a short time between polls. But now there can be a delay between when the disk controller finishes and when the CPU notices it's done. Depending on the system, it's also possible for the disk controller to be idle until the CPU notices that it's finished, thus wasting its time, too.

  • Duck speaking

    It would be better for the disk controller to tell the CPU when it's done, rather than the CPU constantly asking.

  • PinkRobot speaking

    Exactly, and that's what interrupts are for.

Interrupts

An interrupt is a signal to the CPU that an event has occurred that needs immediate attention. The CPU can then stop what it's doing, save its state, and jump to a special piece of code called an interrupt handler that deals with the event.

A Clock/Timer Interrupt

Let's set aside the disk device for a moment and imagine a different device: a clock. The clock device is going to help the computer keep track of the passage of time by sending a tick interrupt to the CPU every second.

When the CPU receives the tick, it stops what it's doing, saves its state, and jumps to the interrupt handler for the clock. The interrupt handler can then update the system time and do anything else that needs to be done every second.

Clock Tick

Clock Tick Interrupt

A tick interrupt is one of the simplest kinds of interrupt. We didn't do anything to cause it, and there's nothing we need to ask the clock when we get it (the clock could tell us the time, but with a tick every second, we can just keep track of the time ourselves).

  • Cat speaking

    In OS/161, there is a clock device that sends a tick interrupt to the CPU at 100 Hz, or 100 times a second.

  • PinkRobot speaking

    That's right. OS/161 uses this interrupt to implement timesharing, where each process gets a slice of CPU time before the next process gets a turn.

A Disk Interrupt

Let's go back to the disk device. With interrupts, instead of our program busy waiting for the disk controller to finish, once the request is sent, the CPU can go off and do other work (i.e., the kernel can schedule another process to run). Let's call the original program “Program A” and the second “Program B.”

When the data is ready, the disk controller sends an interrupt to the CPU. The CPU stops what it's doing, saves its state, and jumps to the interrupt handler for the disk. The interrupt handler can then read the data from the disk controller.

We now have a choice, because we have two programs that are both able to run, Program A, whose data is now ready, and Program B, which was running when the interrupt happened. Which program to run next is a scheduling decision.

What would you choose to do in this situation?

Give a brief explanation of your choice. What trade-offs can you see between the options?

Trade-offs

For I/O-related interrupts like our disk example, here are some of the trade-offs:

  • We want interrupt handling to be quick, not least because while we're handling one interrupt, we won't be noticing any others. So we want to resume normal execution as soon as possible.
  • When a higher-priority process becomes able to run, we want it to resume running quickly.
  • We want to avoid starvation, where a process never gets to run because there's always something more important to do.

Process prioritization and the decision of what to run next is the job of the process scheduler (which we'll discuss later). In timesharing systems (including Linux, macOS, Windows, and OS/161), the scheduler is invoked regularly by a timer interrupt, so there is little advantage to specifically calling the scheduler from an I/O interrupt handler.

To make interrupt handling fast, a common approach is to have the interrupt handler be as minimal as possible, grabbing data and stashing it away. If the data needs additional processing, an internal task inside the kernel (known as a tasklet in Linux) can be scheduled to do the work.

  • Hedgehog speaking

    What if two devices send interrupts at the same time?

  • PinkRobot speaking

    The CPU can only handle one interrupt at a time, so it has to choose which one to handle first. The choice is usually based on the priority of the interrupting device. The CPU can be programmed to ignore interrupts from devices with lower priority while it's handling an interrupt from a higher-priority device.

  • BlueRobot speaking

    But that's a detail we'll mostly gloss over in this class. We'll generally just use a binary on/off approach to interrupts rather than worrying about priorities.

Polling vs. Interrupts

  • Dog speaking

    So interrupts are best and polling is a discardable relic of the past?

  • PinkRobot speaking

    Not quite…

  • BlueRobot speaking

    Sometimes, it turns out that polling is actually more efficient than interrupts.

Suppose, for example, that we're downloading an OS update that is 1 GB in size and it takes 100 seconds. If we get an interrupt for every network packet, there will be 1 million interrupts, or about 10,000 per second. That's a lot of interrupts! It might be more efficient to poll the network card every 10 ms to collect all the packets that have come in since the last poll. (We will need the network card to be able to buffer packets for us, but that's a reasonable assumption.)

  • Hedgehog speaking

    Interrupts seem more complicated. Making programmers use them seems like a lot of work.

  • PinkRobot speaking

    Remember, we're talking here about code inside the operating system, not in user programs. For user programs, the operating system provides an idealized machine that is synchronous, so the fact that the program was stopped and then restarted is invisible to the program.

  • Hedgehog speaking

    Oh, right… Phew!

  • Duck speaking

    But in this class, we're writing the operating system kernel code…

  • Hedgehog speaking

    Oh, right… Oh, dear!

Reminder: Application Code and I/O

The idealized machine that the operating system provides in the form of processes is synchronous. From a user program's perspective, a call to read just waits for the data to be ready, and then returns it. The fact that behind the scenes the program was stopped and then restarted is invisible to the program.

Deeper Dive: I/O and User Programs

That said, some user programs themselves have other work they could do while waiting for I/O. For such programs, there are a few options:

  • Non-Blocking I/O: The program can ask not to be suspended while I/O happens. It can then go off and do other work and check back later to see if the I/O is done. On Unix-like systems,
    • The O_NONBLOCK flag for the open and fcntl system calls sets a file descriptor to non-blocking mode (although this mode only makes a difference for “slow” devices like terminals and network connections).
    • The program can use the select or poll system calls to wait for I/O on multiple file descriptors at once.
  • Asychronous I/O: The program can ask the kernel to notify it when I/O is done. This approach mirrors the interrupt-driven approach described above, except that instead of I/O devices sending interrupts to the CPU, the kernel is sending notifications to the program. On Unix-like systems,
    • The aio_read and aio_write functions can be used to perform asynchronous I/O.
    • Like interrupts, the program can choose to receive a signal when the I/O is done (although other notification mechanisms are available).
    • More can be found about this in the aio man page in Section 7 of the Linux manual pages.

These aspects are an ever-evolving landscape. For example, unsatisfied by the aio interface, the Linux community has been working on a new interface called io_uring that is more efficient and flexible. It's a good example of how the kernel evolves to meet the needs of applications programmers.

  • Goat speaking

    Meh. That stuff about aio_read won't be on the test, right?

  • PinkRobot speaking

    These are advanced facilities. It's good to know they exist, but we're not going to make you use or implement them in this class.

(When logged in, completion status appears here.)