CS644 Thurs., Apr. 29 Handout: Intro to Projects

Xinu I/O system and Device Drivers

Xinu, like Linux/UNIX, has “device-independent” system calls:

open, close, read, write, getc, putc, and control (vs. Linux/UNIX ioctl)

that work with the various devices, some of which don’t need to be opened:

write (CONSOLE, “hi”, 2);  writes to console line (COM2). CONSOLE is opened before main is called.

write(SERIAL0, “hi”, 2);  writes to the COM1 line. This device doesn’t need an open.

write (LPT, “hi”, 2);  (prints “hi” after lp project)

In general, write(int dev, char * buffer, int nbytes) writes nbytes to device dev.

The device numbers are defined in the Xinu stdio.h

#define CONSOLE      0

#define SERIAL0     1

...

#define LPT         9

Very simple, extensible i/o system. Similar to UNIX/Linux, where write is:

write(int fd, char *buffer, int nbytes);

where fd is obtained by open.  However, the standard input (fd = 0), output (fd = 1), and error (fd = 2) are opened by the shell for the user program, so write(1, “hi”, 2)  works immediately in a user program.

Device Drivers

As we’ve seen many times, devices signal events by interrupts while processes request services via system calls.  The interrupt handler code and the system call code in the kernel are considered somewhat different kinds of code, and special rules hold for interrupt handler code.

Each device driver, for a certain device, has both kinds of code: code called by system calls read, write, etc. when they are using this device, and interrupt handler code for when the device interrupts.

For example, tty devices have ttywrite, ttyread, ttyputc, ttygetc, etc, so, functions in the tty device driver. lp devices have lp_write, lp_putc, etc. in the lp driver.

write(CONSOLE, “hi”, 2) calls tty_write(devptr, “hi”, 2), where devptr points to a struct with info about the device.

write(LPT, “hi”, 2) calls lp_write(devptr, “hi”, 2) similarly

putc(LPT, ‘A’) calls lp_putc(devptr, ‘A’)

Each top-level i/o call like write has to look up what device-driver function to call. The “device switch” table named devtab is on pp. 154-155, but our system has a slightly different one, in $xuker/conf.c. This devtab array is another kernel global variable.

The #0 entry of devtab[] is almost the same: the CONSOLE device, with the tty driver.  SERIAL0, our device #1, is different, at least in appearance. Our RING0IN and RING0OUT are devices 10 and 11. Our device 9 is LPT, with lpwrite, lpputc, etc., for the (future) lp.c driver.  To make the system build, there is an lpstubs.c with no-op functions by these names.

This array runs the dispatching of write to ttywrite or lpwrite or whatever. In write, the device number is used to find the relevant struct entry of devtab, and then the dvwrite member of this struct is ttywrite or lpwrite or whatever. See the code for write on pg. 150.

Devtab also provides the “minor device number” that says which instance of a device is being used at the moment. The same device driver can handle several different devices by having the same function pointers in devtab, but a different minor device number for each. This trick is also used in UNIX/Linux. The major device number is the device number such as CONSOLE = 0.

Devtab also provides some hardware addresses and interrupt handler function pointers, but these are not in full use in the x86 version because of a different interrupt handler strategy, so don’t worry about them. Also, they don’t exist in the device switch of UNIX/Linux.

Actually, UNIX/Linux has two device switch tables, one for “char” devices, like tty and lp, and another for “block” devices, like disk drivers and network interfaces. The char device table is the one that looks like Xinu’s devtab. See the first two paragraphs of Love, Chap. 13.

The Xinu TTY Driver

Let’s do putc first, most relevant to the lp project

putc(CONSOLE, ‘A’) ---devtab---> ttyputc(devptr, ‘A’)   See pg. 169.

ttyputc.c is online at $xuker/../tty/ttyputc.c, one line different because of different serial hardware, plus char ps --> STATWORD ps;

First time we’ve seen Xinu kernel code without disable/restore around it—let’s figure out why.

devptr->dvminor picks up minor device #, 0 for CONSOLE.

So iptr points to tty[0], first struct tty in tty array. See pg. 163-164 for struct tty.

See input buffer ibuff, output buffer obuff, echo buffer ebuff, osem, the “output semaphore”, some details, and a pointer ioaddr—for x86, we use i/o port numbers instead, so don’t worry about this.

Current interest is in the output buffer obuff, of fixed length OBUFLEN, and osem.

Data flow: data from ttyputc ---> output buffer obuff ---interrupts--> hardware

So ttyputc is the producer, and the interrupt handler is the consumer, and obuff is the bounded buffer between them.

<picture of ttyputc on top (producer), obuff in middle, interrupt handler at bottom (consumer)>

For full producer-consumer, we use two semaphores, one to block the producer and one to block the consumer.

Here, we can block the producer, ttyputc, but not the consumer, because of the rule that interrupt handlers cannot block.  When ttyputc (called from putc, so system call code) is executing, the system is in process context and can block. That’s why there’s only one semaphore, osem, and ttyputc does block on it.

OK, so what happens when the consumer should block? This happens when the output buffer is empty. We can shut down interrupts, and of course turn them back on when more data shows up.

Now we can understand the code. The wait in ttyputc is the producer-wait of producer-consumer. When this returns, there is space in the obuff, so there are 3 lines of code doing the enqueue into obuff, and then one line of code to enable output interrupts (this has to be different in our case, but has the same effect.)

The code after wait is clearly a critical region, so we see disable/restore around it.

Back to the question of why no disable/restore before or across the wait. 

Each system call (wait, ttyputc) has its own mutex, so doesn’t need mutex held in its calling code.

The contents of devtab do not change while Xinu executes

ch is a local variable, thus process private

Assume iptr->ocrlf is constant, a flag for this device behavior

So the if (...) cannot be involved in a race condition.

The output interrupt handler is ttyoin, on pg. 174. The first two if’s are about echoes and flow control, so we’ll skip over them. The third if is of interest. It’s testing whether or not obuff is empty, and if not, dequeues a char from it and outputs it to the device, and does a signal on osem. Actually it does a more complicated thing than a simple signal per char, but that would work too. If obuff is empty, the code shuts down the output interrupts of the device.

The output to the device is done by cptr->ctbuf = char. This is memory-mapped i/o. We would use outb to the right i/o port.