device-specific I/O: drivers, implemented as kernel tasks
device-independent I/O: server layer
user-level I/O: user process layer
Minix interrupt handlers
the disk interrupt handler is very simple: check the status
of the device, then call interrupt
other interrupt handlers perform duties that could be performed
by the tasks, except for the added cost of message passing:
on most clock ticks, the clock interrupt handler
updates pending_ticks up to a limit set by the clock task,
and only then sends a message (the clock task can add pending_ticks
to its measure of time, then clear it and update the limit)
the terminal interrupt handler and task both handle all terminals, but
some key events (e.g. releasing of a letter key) can be ignored, so do not
result in sending a message to the terminal task
the terminal interrupt handler never sends messages to the terminal
task, instead setting a variable tty_timeout that is read by the
clock interrupt handler. The clock interrupt handler sends a message
to the terminal task if tty_timeout is set, potentially covering
many characters received in quick succession
although the abstraction (that interrupt handlers do nothing
more than send messages to the corresponding tasks) can be useful,
it can also lead to inefficiency if applied indiscriminately
Minix device drivers
device drivers are linked with the kernel, so can share code
device drivers execute as separate tasks (processes), and (after
initialization) only when they receive a message
user processes send messages to the file system or MM server, and
servers send messages to the tasks
tasks also receive messages from the interrupt handlers
interrupt handler messages carry no data
messages from the servers may carry information about the
(minor) device number, which process is requesting the I/O, how many
bytes are requested, where on the device and where in memory the bytes
are or should be placed
Minix task threading
each task may be interrupted, but interrupt does nothing
while tasks are running, so each task essentially is like a monitor
however, tasks may receive messages, and therefore suspend
the block device drivers will only accept messages from the interrupt
handler while they are waiting for a result, so these tasks are not
re-entrant
the terminal task will accept keyboard input while waiting for a
response from a serial line read, but will not accept further requests
for keyboard input until the first one has been satisfied
Minix task structure
a task must:
initialize its device(s)
loop forever,
receiving a message (usually tasks block here)
carrying out the message (occasionally tasks block here)
sending a response (tasks should never block here)
all the block device drivers (floppy, hard disk, CD, SCSI) share
similar functions, and are implemented with a single, generic main loop
the generic main loop uses an array of function pointers (of type
driver) to execute device-specific operations
Minix block driver
for example, a dev_open message results in a call to
(*dp->dr_open)(dp, &mess)
a call to read or write results in a call to
(*dp->dr_rdwt)(dp, &mess)
/src/kernel/driver.c contains generic versions for
some of these functions, for example do_rdwt, which
calls
if a block device chooses to use do_rdwt, it can implement
these functions to provide the desired functionality
for example, for the RAM disk, dr_schedule can perform
the I/O, and dr_finish is a no-op
in contrast, for the floppy disk, dr_schedule sets up
the operation, and dr_finish waits for it to complete
Block I/O operations
open a device -- check to make sure it is actually there, initialize
it, read the partition table
close a device
read a block, specifying the start location on disk, the number of
bytes, and the buffer address
write a block, specifying the start location on disk, the number of
bytes, and the buffer address
IOCTL a device, getting or setting the partition table to/from memory
do a scattered read or a scattered write, providing an array of blocks
to be read (perhaps optionally) or written
each scattered operation is all reads or all writes
optional reads allow for prefetching, and the device driver can
decide whether prefetching is a good idea (e.g. not beyond the end of
a track)
Block I/O memory allocation
a buffer is statically allocated for DMA
but, to accomodate older PC hardware, the buffer may not cross
a 64K boundary
however, the buffer is allocated by the compiler, so the OS has
no control over whether the buffer crosses a 64K boundary
solution: allocate a buffer of twice the needed size, and only
use the 1/2 the buffer that does not cross such a boundary
Scattered Block I/O implementation
scattered block I/O is logically simply a loop over all the requests
requests must be sorted in block order, for efficiency of merging
with existing requests
the array of requests must be copied from the process's virtual
address space to the task's address space -- this copy is done using
physical addresses for both arrays -- because the requests may suspend
the task
having many blocks to perform I/O for may allow faster overall
latency, since blocks from different requests may be adjacent
Minix RAM disk
RAM disk is block-oriented, just like the real disks, even the
block size is the same
RAM disk is stored in main memory: a 1MB RAM disk takes up 1MB
of main memory
write requests write directly to memory
read requests copy data directly from memory
four different Minix RAM devices:
major device 0: /dev/ram, a true RAM disk on which the root
file system is usually mounted. Memory for this is allocated at boot time
major device 1: /dev/mem, a pseudo RAM disk corresponding
to the physical memory of the PC
major device 2: /dev/kmem, a pseudo RAM disk corresponding
to the kernel data memory of Minix
major device 4: /dev/null, a pseudo RAM disk that throws
all its data away
the first three RAM disks have some overlap (aliasing -- different
names for the same underlying objects)
Minix RAM disk implementation
many functions (e.g. close, cleanup) are no-ops
mem_task calls
m_init (which sets up /dev/mem and /dev/kmem
sizes) and then goes to the shared driver_task function
open checks the device number, then also gives permission to
read/write I/O ports to any process that opens (and therefore has
enough privilege to open) /dev/mem or /dev/kmem
3 ioctls are supported:
set the size of a RAM disk (only FS may do this) -- code on lines 9896
implements first-fit memory allocation algorithm
set the address of the MM or FS part of the process table
get the address of the process table
prepare records the device's minor number and returns the (completely
fake) geometry
finish is a nop -- schedule does all the work
reading or writing of /dev/null is a question of returning
the correct number of bytes read or written (0 read, all written)
Reading or writing a RAM disk block
clear the optional bit -- all operations on a RAM bit complete immediately
check for address legality (within the RAM disk) and truncate
the transfer if necessary
determine the physical address for the transfer, for both the user
buffer and the RAM disk buffer
perform the physical copy
compute the return value, the number of bytes transferred