ยง36.8โ€“36.10Case Study: A Simple IDE Disk Driver โ€ฆ Summary

Part III OSTEP pp. 427โ€“430 ยท ~6 min read

Weโ€™ve covered the mechanisms in the abstract; now a real device โ€” the IDE disk โ€” makes them concrete.

36.8 Case Study: A Simple IDE Disk Driver

An IDE disk exposes four kinds of register (control, command-block, status, error), read and written with x86 in/out at fixed I/O addresses. The status register at 0x1F7 packs the driveโ€™s state into eight bits:

The IDE status register (0x1F7): one byte, eight state bits. BUSY and DRQ drive the protocol; ERROR flags a fault.

bit 7BUSYbit 6READYbit 5FAULTbit 4SEEKbit 3DRQbit 2CORRbit 1IDDEXbit 0ERROR

The command-block registers (also I/O addresses) carry the request:

Figure 36.5: the IDE command-block registers, reached with x86 in/out at fixed I/O addresses.
addresswhat it does
Data0x1F0the data port โ€” read/write the sector bytes
Error / Features0x1F1error details (read when the STATUS ERROR bit is set)
Sector Count0x1F2how many sectors to transfer
LBA low / mid / hi0x1F3โ€“0x1F5the logical block address (which sectors) โ€” 3 bytes
Drive / head0x1F6drive select (master/slave) + top LBA bits
Command / Status0x1F7WRITE a command here to start; READ it for status
Control0x3F6reset the drive; enable/disable interrupts
Dotted-underlined cells have explanations โ€” click one.

The protocol strings these together โ€” wait ready, write the parameters, start the command, and let an interrupt signal completion:

The IDE read/write protocol, as the xv6 driver runs it.
OS driver(xv6 ide.c)
IDE disk
1 โ€” Wait until ready (ide_wait_ready)
2 โ€” Write parameters, then start (ide_start_request)
3 โ€” Complete via interrupt (ide_intr)
step 1 / 8 ยท time flows downward

A poll: spin until the drive can accept a command. (This brief poll is fine even in an interrupt-driven driver โ€” the drive is ready almost at once.)

The xv6 driver implements exactly this in four functions (Fig 36.6):

// spin until the drive is ready to accept a command
static int ide_wait_ready() {
    while (((int r = inb(0x1f7)) & IDE_BSY) || !(r & IDE_DRDY))
        ; // loop until not-busy and ready
}

// issue a request: write the registers (and data, if a write)
static void ide_start_request(struct buf *b) {
    ide_wait_ready();
    outb(0x3f6, 0);                       // enable interrupt
    outb(0x1f2, 1);                       // one sector
    outb(0x1f3, b->sector & 0xff);        // LBA low ...
    outb(0x1f4, (b->sector >> 8) & 0xff); // ... mid ...
    outb(0x1f5, (b->sector >> 16) & 0xff);// ... hi
    outb(0x1f6, 0xe0 | ((b->dev & 1) << 4) | ((b->sector >> 24) & 0x0f));
    if (b->flags & B_DIRTY) {
        outb(0x1f7, IDE_CMD_WRITE);       // WRITE command
        outsl(0x1f0, b->data, 512/4);     // ...stream the data out
    } else {
        outb(0x1f7, IDE_CMD_READ);        // READ command (no data yet)
    }
}

// queue a request; sleep until it completes
void ide_rw(struct buf *b) {
    acquire(&ide_lock);
    /* ...append b to ide_queue... */
    if (ide_queue == b)          // if the queue was empty,
        ide_start_request(b);    //   issue it now
    while ((b->flags & (B_VALID|B_DIRTY)) != B_VALID)
        sleep(b, &ide_lock);     // block until the interrupt marks it done
    release(&ide_lock);
}

// the interrupt handler: finish the request, wake the waiter, start the next
void ide_intr() {
    acquire(&ide_lock);
    if (!(b->flags & B_DIRTY) && ide_wait_ready() >= 0)
        insl(0x1f0, b->data, 512/4);  // if a READ, pull the data in
    b->flags |= B_VALID; b->flags &= ~B_DIRTY;
    wakeup(b);                          // wake the process in ide_rw
    if ((ide_queue = b->qnext) != 0)    // if more queued,
        ide_start_request(ide_queue);   //   launch the next
    release(&ide_lock);
}

Notice the shape: ide_rw queues and sleeps (using the concurrency primitives from Part II โ€” a lock plus sleep/wakeup); ide_intr runs on the hardware interrupt, does the finishing work, and wakes the sleeper. Interrupts and DMA-style overlap, made real.

36.9 Historical Notes

Interrupts and DMA are ancient. Interrupt vectoring appears on the early-1950s UNIVAC; DMA-like designs on the DYSEAC (a โ€œmobileโ€ machine haulable in a trailer) and IBM SAGE; vectored interrupts perhaps on the Lincoln Labs TX-2. The exact โ€œwho first?โ€ is murky and maybe beside the point โ€” the ideas are obvious consequences of fast CPUs and slow devices: of course youโ€™d let the CPU do other work while a slow I/O is pending. If youโ€™d been there, you might have thought of them too.

36.10 Summary

You now know how an OS interacts with a device: two efficiency techniques โ€” the interrupt (overlap computation with I/O) and DMA (offload data copying) โ€” and two ways to reach device registers โ€” explicit I/O instructions and memory-mapped I/O. And the device driver encapsulates each deviceโ€™s low-level specifics so the rest of the OS stays device-neutral. Next: the device this driver has been commanding all along โ€” the hard disk drive.

Check yourself: the IDE driver

1.Before issuing a command, ide_wait_ready() spins on the status register. What is it waiting for?

2.In ide_start_request, what does writing the command register (0x1F7 = READ or WRITE) accomplish?

3.What does ide_rw() do, and how does it use Part II's concurrency tools?

4.What does the interrupt handler ide_intr() do when the disk finishes?

5.How does the driver detect and diagnose an error?

6.The chapter summary lists two efficiency techniques and two register-access methods. What are they?

6 questions