next up previous
Next: About this document

Comp 242 Spring 2002

Distributed Terminal Drivers (Due Tue Mar 26)

In this project, you will add distributed terminal drivers to your distributed operating system. You will implement the Xinu getc(descrp) and putc(descrp,ch) system calls for terminal devices and additional devbind(descrp,kid) and devunbind(decrp) calls to support dynamic binding of distributed terminals. Each Xinu kernel is associated with its own keyboard and display devices, which are simulated by two separate Unix processes. The kernel executes a separate Unix process, and the associated device processes sends it software interrupts instead of hardware interrupts. For instance, when a character is typed in by the user, the keyboard process generates and sends a signal to its kernel process. The terminal drivers are distributed in that a Xinu thread on one kernel can use the device attached to a remote kernel. You need not worry about the details of simulating devices using processes. We have provided the necessary library routines that perform these tasks. When a kernel starts, it must initialize its two devices by simply calling the routine InitDevices().

As in the single-kernel Xinu discussed in Comer's book, a device is named by a device descriptor. Device descriptors in your multi-kernel OS will be shared by all kernels, just as port numbers are (in fact, in your implementation, device descriptors will be bound to port numbers.) So on kernel m and n, descriptor i refers to the same device, allowing us, for instance, to build a system in which the console is rebound by a single process in the system.

You can assume that in a distributed OS with N kernels, there are exactly N device descriptors. Each kernel i, when it starts, binds descriptor i to kernel i. This is the default binding for the descriptor, and a thread on any kernel can change this binding. You will implement a call, devbind(descrp,kid) which binds the the device descriptor, descrp, to the terminal connected to kernel, kid. Thus, if a thread executes:

devbind (1, 2);

and later some thread executes:

putc (1, 'a');
the character is printed to the terminal attached to kernel 2. Note that neither of the two threads may be executing on kernel 2. A bound device can be unbound by the devunbind(descrp) call. The reason for allowing dynamic binding and unbinding of devices is that we may want to develop distributed virtual terminals that can be mapped to different physical terminals much as, in Unix, the standard input and output can be mapped to different physical files without changing the process doing the I/O.

You do not need to worry about echoing characters or dealing with characters in anything but raw mode. You should read about watermarks, but do not have to implement them in the assignment.

As discussed in class and in Comer's book, a device driver consists of two halves: an upper level routine that is the interface used by programmers and a lower level routine that is used to interface with the device itself. The getc and putc routines are the upper level routines used by a program. You are responsible for implementing these routines. In addition you are responsible for writing the lower level routines that will respond to interrupts from the keyboard and display device. These interrupts will come to your program in the form of UNIX signals. The UNIX signal SIGUSR1 (defined in /usr/include/signal.h) will be sent to your device driver whenever a character is made available by the keyboard device. The signal SIGUSR2 will be sent to your device driver whenever the display device is free to read a character for display. The upper and lower halves of the input and output device drivers should coordinate using a shared buffer object.

In Xinu, shared memory and semaphores are used to define the buffer object. Since the upper-half routines on one kernel and can communicate with lower-half routines on a remote kernel, you cannot follow the Xinu approach and should, instead, use the distributed IPC primitives from your previous assignment to implement a distributed buffer object. One constraint is that you should not implement additional mechanisms for queuing processes: processes waiting for I/O should be blocked on (Xinu) ports rather in special queues created for this assignment. You can assume that certain ports are bound by predefined OS processes. The devbind and unbind calls will essentially allocate and deallocate ports. You will probably need more than N ports to support N devices - you can increase the size of the port table according to your needs.

Once devices are initialized using InitDevices, the kernel should use a ttyinit() routine written by you to initialize the buffers and other device specific structures, arrange for the signals SIGUSR1 and SIGUSR2 to be handled by your interrupt handlers, create predefined OS processes and bind ports, and perform any other tty-specific initialization. InitDisplayDevices should be called before you start initializing Xinu, open sockets or do anything else. It should be the first call in main().

You can assume that terminals are the only I/O devices connected to a kernel and the I/O routines getc and putc always apply to terminal devices. Thus, you do not need to worry about mapping getc and putc to ttygetc and ttyputc through a table.

Lower half routines normally interact with a serial line device (such as tty) through input/output registers (e.g. RBUF in Chapter 2 of Comer's book) and a control and status register (such as RCSR). On page 170 of Comer's book he talks about using the Serial Line Unit (SLU) registers to effect these interactions. The driver routines set bits in the control and status register to enable or disable devices. For example, when a character is output to the display device, the ttyputc routine enables the display device interrupts (see SLUENABLE on p. 169 in Comer's book). Because your driver routines will be interacting with device processes using software interrupts, you must call a supplied routine called "EnableDisplayDevice()" that enables the output device whenever a character is available for output.

In addition you need a means for your lower half routines to transfer characters to and from the device processes (like XBUF/RBUF registers in an SLU device). To transmit characters to and from the devices in the assignment you should use the routine "IOTransfer(op, pch)" which takes an operation code and a character pointer. You should define two opcodes

#define IOREAD 1
#define IOWRITE 2
To transmit a character to the display device use "IOTransfer(IOWRITE, &ch)" "IOTransfer(IOREAD, &ch)". Note: These routines must only be used by the lower half routines of your device driver.

The following is a list of routines provided to you that you must use in implementing the lower and upper half driver routines.

InitDevices() -- initialize the devices -- done once.

EnableDisplayDevice() -- to enable the display device when a character is available for output -- like enabling output interrupts in Xinu but this must be done on each character.

IOTransfer(op, pch) -- to transfer characters to/from the device.

CleanupDevices() -- clean up at the end.

The library code is defined in the files device.c and defns.h in the directory http://www.cs.unc.edu/ dewan/242/f97/code/assignment4. You should compile and link device.c with your program. Depending on the compiler you are using, you may have to change the declarations of some of the parameters of the procedures in this file. It does compile with gcc. You may also have to make sure that names of the library routines do not conflict with the routines you have written.

Because the keyboard and display device processes will be reading and writing characters to and from the terminal, a kernel must not perform any input (such as gets, scanf) or output (printf) operations. To enforce this, InitDevices() closes the stdin and stdout of your kernel process. The consequence is that all debug output by your program must be written to stderr (e.g fprintf(stderr, "")). Output printed to stdout will not be shown so when you cannot figure out why a debugging statement is not being written check to make sure it is being written to stderr. You should invoke each kernel in a separate window so that different device processes do not perform I/O in the same window.

This project allows multiple processes executing on different machines to perform I/O on the same device. As in Unix, this means that the output of different processes may get intermixed. Also an input character is not delivered to each process waiting for input on that device, it simply goes to the next process waiting for input. Shared descriptors should not complicate your implementation, in particular, you should not have to worry about keeping distributed tables consistent - the underlying port mechanism should already be doing that since shared descriptors are built on top of shared ports and binding a descriptor should correspond to allocating a port. It is all right if your devbind fails because the port you want to allocate is already reserved.

As part of your solution, you should do the following:

Describe how distributed lower and upper half device routines synchronize with each other.

Describe if you allow binding of multiple descriptors to the same kernel. If not, give a semantics- or implementation-based reason for doing so.

Use your OS to implement a special terminal whose descriptor is MASTER_CONSOLE (a constant). Every time a kernel creates a new process, it should print the kernel and process id of the new process on the MASTER_CONSOLE. Initially the MASTER_CONSOLE is bound to a default kernel (which depends on the value of this constant). This device should accept a user command to move the console to another kernel. To move the console, a user enters a move command of the form:

#m <kernel_id>

You can assume that kernel id will be a single digit and that exactly one blank will be entered before the kernel id. Thus, if a user enters

#m 4
at the master console, then the device moves to (the terminal attached to) kernel 4 and subsequent output to the MASTER_CONSOLE is directed there.

Use the OS to also support simple teleconferencing allowing users at one or more terminals to talk to each other. The teleconference is controlled from the MASTER_CONSOLE. We define another command, the join command, to add a kernel to the conference:

#j <kernel_id>

Thus, if a user enters

#j 4
#j 5
at the master console, then (the terminals attached to) kernel 4 and 5 are added to the conference. Any input entered at a terminal in the conference is echoed to all terminals in the conference. It is upto you whether you allow MASTER_CONSOLE to join the conference. In case you do, the # character before the command name allows you to distinguish commands from teleconference input.

Good luck!




next up previous
Next: About this document



Prasun Dewan
Tue Mar 5 11:37:41 EST 2002