CS444 Class 24
Hw4—questions?
Next Wed: last class: teacher evals, review.
Finish Chap 5, then start Chap 6 Deadlocks
Goals of i/o software, pg. 283.
device independence, helped by uniform naming, also known as the filesystem “namespace”, a syntax of string names for files and also devices represented by “special files” like /dev/lp.
synchronous vs. asynchronous—we’ve covered this before:
but “output done” can have two meanings, at least for user output using an OS:
Memory-mapped I/O (not to be
confused with memory-mapped files!!)
We are familiar with I/O instructions of the x86, not memory-mapped i/o. For example,
inb %dx, %al with 0x3f8 in dx
This instruction reads one byte
from i/o port 0x3f8, the COM1 UART’s receiver register, into CPU register
Compare this to the memory-reading instruction
movb 0x3f8, %al
This instruction reads one byte
from memory location 0x3f8 into CPU register
The x86 architecture specifies two spaces, a 32-bit memory space and a 16-bit i/o port space, and it is always clear from the instructions which one is being used.
When the reads or writes go over the bus, the memory address or i/o port number ride on the address bus, which of course has to be big enough for the 32-bit memory addresses, and is half idle in the case of an i/o port number. In addition to the address lines, there is a control line called M/IO# which says which kind of address it is.
Thus it is easy for devices to know that an i/o port # is current on the address bus and they should check further to see if it’s asking for them.
On the other hand we’re using 33 lines somewhat inefficiently—we could carve off a little bit of the memory 32-bit space for i/o addresses and drop the M/IO# line. That’s the idea of memory-mapped i/o.
Aside: Memory-mapped i/o was invented by the genius Gordon Bell, who masterminded the PDP11. He also engineered the Unibus backplane, with a standardized bus protocol that allowed new boards to be plugged in. Then UNIX came along with its plug-in device drivers, at no charge, just sign this nondisclosure. The result was dynamite for University researchers and others who wanted to use computers for measurement and control as well as general computing. Our department’s first computer was a PDP11 with 256K of memory and 10M of disk, bought in 1978 for about $30K, soon upgraded with a giant 80M disk. We taught several classes at once on this system, using version 6 and then version 7 of the original UNIX sequence.
In memory-mapped i/o, a certain
region of memory addresses are turned into i/o register addresses. Under
UNIX/Linux or other sealed-up
Tan ex. “test port_4” in assembler, where port_4 is some particular address in the hardware register region. One nice thing about memory-mapped i/o is that you can use C directly for accessing device registers.
Char * printer_status_reg = 0xffff10cc
Char * printer_data_reg = 0xffff10ce;
while ( *printer_status_reg != READY) /*loop until
ready */
*printer_data_reg =
p[i]; /* send one char to printer */
With memory-mapped i/o, you can write a whole device driver in C, except for that little assembler envelope around the interrupt handler. Of course we put little routines around inb and outb, so we can write mostly in C in spite of the need to use special i/o instructions.
The x86 actually does memory-mapped i/o as well as i/o instructions. The PC system architecture reserves the upper part of the first 1M of memory space for this. The video memory is in this area.
DMA—can skip coverage in Tan. Used for network interfaces as well as disk-like devices.
pg 344: Programmed i/o—polling device status, no interrupts.
Note polling loop on pg. 285 is using memory-mapped i/o—be sure you know the x86 equivalent. Also move the semicolon for the empty while body to it own line!
pg. 346: Interrupt-driven i/o—covered in detail already. Note again memory-mapped i/o in example. Don’t try to interpret this code exactly, as it is not exactly right.
pg. 347: I/O using DMA (can skip): a whole block of data gets moved to or from a device and memory, with just one interrupt. As with simpler devices, the data gets copied from the user to a kernel data area before being sent to the device, and is received first in kernel memory, then copied back to user. The kernel blocks the user process as needed in read or write. The interrupt handler calls wakeup.
pg. 351: Device Drivers—one for each kind of device.
pg. 348: Layers pic. Fig. 5-11. Note that “interrupt handlers” are actually parts of device drivers, not a separate kind of software. Each device driver has an upper half and a lower half, where the upper half runs in system calls read or write, and the lower half is the device’s interrupt handler. Between them is a shared data buffer, itself in kernel data. Producer-consumer relationship here. In other words, there are two layers here that belong to device drivers, the upper system call code and the lower interrupt handlers.
pg. 349 Steps in interrupt handler—mostly review, but some comments re real OS’s. More detailed than list on pg. 93.
Steps of process switch—should be separated from interrupt handler description. It’s called from there and many other places. Note that it is called at the end of the interrupt handler, as we are doing in the tick handler for hw4, for the same reason: the main body of the interrupt handler is supposed to finish very fast, to be responsive to its hardware and not prevent the rest of the system from progressing.
Process switch steps:
our scheduler() in hw4, but add MMU actions for real OS
1. Setup MMU/TLB to map in new process’s VA space. Also, flush TLB.
2. Save old process’s CPU registers in its process table entry (missing from pg. 349)
3. Load newly chosen process’s CPU registers from its process table entry
4. Now the new process is running (no need to explicitly “start it” as listed in Tan’s point 10.
More on untrusted interrupt handlers (optional). A trusted interrupt handler is one that is written by the main kernel developers. An untrusted interrupt handler is one written by an outside group. There is a way to dynamically load additional device drivers into the kernel, and this is how an untrusted interrupt handler can become part of the kernel. Without special handling (steps 2 and 3 above in the interrupt handling list), an interrupt handler has easy access to all kernel code and data, and possibly user code and data (in 32-bit Linux for ex.), which it may use maliciously. To protect the system from an untrusted interrupt handler, the OS can bottle it up by loading the MMU with (say) a special page directory with only one page table to map just the needed code and data for this interrupt handler. Of course at the end of the interrupt handler the OS needs to restore the MMU setup of the process that was interrupted—this step is missing in the above list.
At Tan., pg 353, Device-indep. i/o—implementation
At syscall level with UNIX, we see write(fd, buffer, nbytes) where the first argument can correspond to many different devices or files, specified at open time. fd = open(“/dev/lp”....) for example.
Similarly, in the hw, we have write(dev, buffer, nbytes), where dev can correspond to different devices: 0 and 1 for serial devices, and we can add device 2 for LPR. Then we have in ioconf.c:
struct
device devtab[] = {
{0,
ttyinit, ttyread, ttywrite, ttycontrol, 0x3f8,(int)&ttytab[0]}, /* TTY0 */
{1,
ttyinit, ttyread, ttywrite, ttycontrol, 0x2f8,(int)&ttytab[1]},/* TTY1*/
{2,
lpinit, lpread, lpwrite, lpcontrol, ... }
};
This is the core data structure to a device-independent i/o system, the device switch table. Classic UNIX has “cdevsw” for character devices and “bdevsw” for block devices that have similarly tabulated function pointers for device drivers. It allows a new device driver to be added without changing the kernel source except for this one file, plus the additional device driver code.
Linux reimplemented the same basic idea, though uses a hash table rather than an array, allowing expansion at runtime. See pg. 776 for a representation of the Linux char device switch.
Execution of
device-independent i/o calls.
1. The user program calls read or write or … with a buffer in user data to supply or receive the data.
2. The device-independent i/o layer contains the “sysread” and “syswrite” or equivalent functions called from the system call dispatch code. In these functions, the appropriate device driver function is looked up in the device switch table and called, causing execution to arrive in the right device driver.
To be more exact, execution arrives in the upper half of the device driver, in “ttyread” or “ttywrite” for a serial device, or “lpread” or “lpwrite” for the LPR device. There it enqueues or dequeues data between the user and kernel data areas, and calls sleep if appropriate, waiting for the actual i/o to be reported by interrupts. In some cases, the upper half has to “start” the i/o in the device.
3. The interrupt handler handles the actual i/o with the hardware device, and calls wakeup when needed to get the upper half going again.
4. The hardware is at the bottom.
In the UNIX/Linux case, covered pp. 775-776, the device number used to look up the right row in the device switch table (or hash table) is called the major device number. We can see these two numbers by doing “ls –l” on the special file:
ls
–l /dev/lp
crw-rw-rw-
1 root 4, 1 <data> /dev/lp
Here the major device number is 4, so cdevsw[4].write will be accessed by a write to this device, and will be “lp_write” or some similar name in the lp device driver.
pg 294 Buffering. There are so many ways described here it’s confusing. Concentrate on case (c), the most common case, where the kernel has a data buffer, and the user has another one.
Android Notes
Android, being Linux, uses device nodes. For example:
ls –l /dev/binder
crw-rw-rw---- 1 root
10, 53 <date> /dev/binder
This shows the device underlying the Binder IPC service that we discussed earlier. It’s a char device, shown by the leading “c”, and thus has system calls open, close, read, write, ioctl, …. Of these, open and ioctl are the ones in common use. The messages are all handled by ioctl, the miscellaneous-services call, rather than by read or write, possibly to indicate that you can’t send arbitrary blocks of data through this system.
Other device nodes: /dev/graphics/fb0 for frame buffer, /dev/event0 for keyboard.
Also, you can use remote gdb to debug C programs (“native code”, running on the Linux level). After setting up things with “adb” and starting the appropriate gdb on the development host, you can use the familiar “target remote” command to connect to the Android device over TCP:
(gdb) target remote :5039 to connect to the Android device using TCP port 5039