15-412: Project # 2 Two Simplified Terminal Drivers for Threads

advertisement
15-412: Project # 2
Two Simplified Terminal Drivers for Threads
Times of Interest




Class, Friday, September 14, 2001 – Assignment distributed
11:59PM, Wednesday, September 19, 2001 – Group change deadline
11:59PM, Friday, September 21, 2001 – Checkpoint
11:59PM, Friday, September 28, 2001 – Assignment submission deadline
Educational Objectives
Through this assignment, we hope that you will accomplish the following:
 Gain practical experience programming concurrent systems and using threads
 Gain practical experience using semaphores as a synchronization primitive
 Gain practical experience using the Mesa monitor paradigm to simplify complex
synchronization problems.
Project Overview
The project requires you to do three things:



Implement a terminal driver using Mesa monitors.
Implement semaphores using the synchronization primitives available within the supplied
thread library
Implement a second terminal driver using your semaphore primitives.
Since the focus of this project is synchronization and coordination, and we have two weeks, not
two months, we will use a software library to simulate a simple terminal device instead of using
the real thing. The library is design to give you the flavor of developing a device driver, without
the frustration.
We will also use a simplified thread library instead of pthreads. The provided library is basically
a subset of pthreads. We hope that by avoiding an overly large API, you will be better able to
focus on the project. It also allows us to control what tools are available to you, which makes it
less likely that you’ll accidentally trivialize the assignment.
1
Miscellany
A few pieces of important information about the project that are otherwise unrelated are
discussed in this section:
Groups
You are assigned to the same group you selected for project #1. If you would like to
change groups, please contact staff-412@cs on or before Wednesday, September 19,
2001.
Environment





Your solutions must be implemented in ANSI C
Your solutions must run in the Andrew/Solaris environment
Your use of threads should be limited to the package that we provide and to
only those features that we describe in this document
Your terminal driver must be support the simulated terminal hardware that we
provide via our terminal library.
You may only use those synchronization primitives that are provided by our
thread library or you create using our thread library.
Form
1. The monitor version of the terminal driver should compile into a single object
file called montty.o.
2. The implementation of semaphores should be contained in a file called
sem.c.
3. The monitor version of the terminal driver should compile into a single object
file called semtty.o.
Grading
Grading for this project will be much like that of project #1. We will consider the
completeness, correctness, design and implementation of your solution. Because of
the nature of this assignment, design is more important than it was in project #1.
Projects that do not correctly follow the required paradigms will be severely
penalized.
It is the case that performance is more important in this project. Projects that perform
badly due to synchronization issues will encounter severe design or implementation
penalties, as appropriate.
2
Paradigm and performance problems aside, we will be considering the design and
implementation of your solution to help you improve and are not likely to impose
penalties, except where they are unquestionably warranted.
Checkpoint
To help you fight the demons of procrastination, we will be checking your progress a
week after we hand out this assignment. By the end of Friday, September 21st.
In class, please hand in a written copy of your design that shows the shared buffers
and other data structures, and briefly describes how access to them is coordinated in
each of the monitors-based and semaphore-based solutions. Please be careful to
identify the critical sections. The design description should be no more than 3 pages,
including diagrams. Please understand -- it is fine if it is much shorter.
By midnight, please create a subdirectory named CHECKPOINT within your group
directory and copy a snapshot of your monitor terminal driver source files and a
Makefile.
This snapshot of the driver, when compiled, should be capable of echoing characters
typed into any terminal. This snapshot will need to use at least WriteDataRegister,
ReadDataRegister, TransmitInterrupt and ReceiveInterrupt.
You will not be graded on this checkpoint, but failure to submit a design that
clearly evidences careful thought and/or failure to submit code that substantially
achieves the goals will result in a penalty of up to 15%
What is a Terminal Driver?
Device drivers are modules within operating systems that encapsulate the messy details of the
hardware of I/O devices. Encapsulation is particularly necessary for devices because there is a
great variety of them. It is easier to provide many small modules that make all devices of a
particular type look the same, than to add support for each new device in many places within the
operating system. I/O devices are particularly messy because they operate at very different
speeds. This makes the protocols of hardware interaction baroque as well as diverse, and exposes
the OS device driver to some insidious concurrency constraints.
Device drivers are often structured into a ``top half'' and a ``bottom half.'' The top half deals with
reacting to the programs that request service from the I/O device, e.g, to read and write system
calls issued by the program. The bottom half deals with acting on and reacting to the hardware,
e.g, by writing device registers and servicing interrupts from the actual device. The top and
bottom halves coordinate their work by sharing data structures.
3
To work correctly, this coordination must use some form of synchronization in controlling access
to those shared data structures. Also, device drivers usually must implement some sort of
synchronization with user programs in order to keep different requests from different programs
from interfering with each other.
The Terminal Hardware
The terminals in this project are simulated on top of xterms running on your display (or, if you
are not on an X display, directly on your terminal.) The terminals support both input and output,
with all data being sent and/or received one character at a time.
The terminal controller hardware implements two data registers and triggers two interrupt lines,
for each terminal it supports. The hardware supports up to a maximum of MAX_NUM_TERMINALS, a
value defined in the header file mentioned below.
Each terminal's hardware displays characters written onto the Output Data Register for that
terminal, but does not do so instantaneously. While the hardware is busy transmitting the
character, the output register must not be used. When the transmission is complete, the hardware
raises a transmit interrupt.
Characters typed onto a terminal are deposited in the Input Data Register for that terminal. When
the hardware places a new character in this register, it raises an interrupt. In real-life hardware,
further input characters from the same terminal are typically placed on the input data register
regardless of whether the previous value of the input register has been read by the device driver.
If the previous value hasn't yet been read by the driver, that is, if the driver failed to respond
sufficiently ably to the previous input interrupt, the character would be typically lost without
having ever been seen by the device driver. In our emulated hardware, however, this is not so. To
help you debug, your driver will be convinced to dump core when an arriving character would
overwrite a character that hasn't yet been read from the input data register.
A class of devices implemented with the same hardware and device driver is often called a major
device. Each instance of that class is often called a minor device. The major device number
typically identifies the code that should service any of the actual (minor) devices. In this project,
we only have one major device, but we have up to MAX_NUM_TERMINALS minor devices.
4
The Hardware Emulation API
The API for the simulated terminals is proved as part of the proj2 library and requires the
inclusion of hardware.h:

void WriteDataRegister(int minor, char c)
This hardware operation places the character c in the output data register of the terminal identified
by the device minor number, minor.

char ReadDataRegister(int minor)
This hardware operation reads (and returns) the current contents of the input data register of the
terminal identified by minor.

int DeviceInit(int minor)
This hardware operation initializes the terminal identified by minor. It must be called once and
only once before calling any of the other hardware procedures on the terminal identified by
minor.

int DeviceInputSpeed(int minor, int msecs)
This hardware operation may be used to set the average character reception speed of the terminal
identified by minor. The parameter msecs specifies the average number of milliseconds of
delay between the time at which a character is typed on the terminal and the time at which the
character is available on the input data register. The actual delay is not predictable (depends on
physics) but its statistical distribution is. DeviceInputSpeed returns the previous value of the
reception speed. If msecs is equal to NO_CHANGE, this function returns the speed value without
changing it. This function is not required for any reason, but you may find it useful in testing – by
varying device speeds, you may be able to ferret out schedule-depended bugs.

int DeviceOutputSpeed(int minor, int msecs)
This hardware operation may be used to set the average character transmission speed of the
terminal controller. The parameter msecs specifies the average number of milliseconds of delay
between the time at which a character is written to the output data register and the time at which
the output data register again becomes available for writing another character.
DeviceOutputSpeed returns the previous value of the transmission speed. If msecs is equal
to NO_CHANGE, this function returns the speed value without changing it. This function is not
required for any reason, but you may find it useful in testing – by varying device speeds, you may
be able to ferret out schedule-depended bugs.
5
Required Terminal Driver Procedures
The device driver you write will need to service interrupts from the hardware as well as requests
from the programs (threads) executing in the system. The procedures in this section must be
written by you, and are called either from the interrupt dispatcher or from a user thread that
requests access to the device.
Interrupt handlers
As mentioned above, when the transmission of a character to a terminal completes, the
terminal controller hardware signals a transmit interrupt. Similarly, when the receipt of a
new character from a keyboard completes, the terminal controller hardware signals a
receive interrupt. In your terminal driver, you must write a separate procedure to handle
each of these types of interrupts. Specifically, your terminal driver must provide the
following interrupt handlers:

void ReceiveInterrupt(int minor)
This procedure is called by the hardware once for each character typed into the keyboard,
after that character has been placed in the input data register of the terminal identified by
minor. The character that caused the interrupt should be read from the input data
register using the ReadDataRegister() operation. Your receive interrupt handler
should not block for very long periods because it is probably not reentrant, and further
receive interrupts will be blocked until the current invocation of the handler returns.

void TransmitInterrupt(int minor)
This procedure is called by the hardware once for each character written to the output
data register, after the character has been completely transmitted to the terminal identified
with minor. After executing a WriteDataRegister() operation, you must assume
that the output data register for that minor device is busy with the transmission until you
receive the corresponding transmit interrupt and your TransmitInterrupt()
procedure is called with the same minor number.
Terminal Driver API
User threads communicate with the terminal using procedures similar to the read and
write system calls in Unix. This section describes the functions that you should
implement. You should include the definitions of these functions found in tty.h.
These functions identify the terminal devices using a terminal number. The hardware
library identifies the terminal devices using a device minor number. Your software
should perform the mapping from minor number to terminal number. A one-to-one
mapping is suggested.
6

int WriteTerminal(int term, char * buf, int buflen)
This call should write to terminal term buflen characters from the buffer that starts at
address buf. The characters must be transmitted one at a time to the terminal by your
terminal driver. Your driver must block the calling thread until the transmission of the
last character of the buffer is completed. This function should return the number of
characters written (buflen), or -1 in case of any error.

int ReadTerminal(int term, char * buf, int buflen)
This call should read characters from terminal term, placing each into the buffer
beginning at address buf, until either buflen characters have been read or a newline
('\n') has been read. The characters must be received by your terminal driver, one at a
time. This function should return the number of characters read, or -1 in case of any error.
Note that the ReadTerminal procedure should not place a null character at the end of
the buffer.

int InitTerminal(int term)
This procedure will be called once and only once by applications before
any calls to use the terminal term are issued. InitTerminal must
initialize the terminal controller hardware by calling the DeviceInit
operation.
Terminal Behavior
Character Processing
Terminal drivers typically do much more than transfer characters from memory to the
terminal hardware. They also process characters in a myriad modes before (on
reading) or after (on writing) the application program sees them (see man termio if
you want to get a taste of the many settable parameters.) In this project we are not
going to ask you to implement a complete set of these operations, but we do ask for
some. Specifically, you must carry out the following character processing:

Any line feed ('\n') output to the terminal by the user program must be
transmitted to the device as the sequence of the two characters '\r' (carriage
return) and '\n', in that order. In C, the character '\n' is called “newline'', and has
the same ASCII code as the device carriage return (which returns the cursor to the
beginning of the same line,) but the meaning in C of “newline'' is, in the domain
of the device, a device carriage return followed by a device line feed (which
moves the cursor one line down without changing its column.) The C Language
changes the meaning of '\n' for programmer convenience, and in so doing
confuses the issue a bit.
On input, the terminal hardware provides a carriage return ('\r' in C) when you
type ”Enter” at the keyboard. This carriage return should be converted to a single
7
“newline” ('\n'.) This is again necessary because the meaning of '\n' in C is the
same as the terminal meanings of both '\n' and '\r'.

All characters input from the terminal must be ``echoed'' back to the
terminal. This allows the user typing on the keyboard to see each character as it
is typed. In order for the terminal to appear responsive, the echoed characters
should be transmitted back to the terminal at the earliest opportunity, regardless
of what the user threads are doing or have done in the recent past. In particular, no
application output should go to the terminal between the time a character is typed
at the terminal and the time the input character is echoed to the terminal. Please
remember this. There are always some 412 students that claim to not have read
this paragraph.

Special processing is required when you receive either a “backspace”
character ('\b') or a “delete” character ('\177') from the terminal. When either
of these two characters is typed, you should delete the last character from the
current input line (if any.) The current input line consists of those characters you
have received from the terminal that have not yet been terminated as a line of
input by the receipt of a newline character ('\n' after the processing described
above.) If the current input line is empty, you should ignore the backspace or
delete character. If the current input line is nonempty, you should delete the last
character in the line. In no case should you include the backspace or delete
character itself in the input line that is returned to a user program calling the
ReadTerminal procedure.
In processing a backspace or delete character as described above, if you delete a
character from the current input line (because the current line was not empty,) you
should also echo the three-character sequence of ``backspace'' ('\b') followed by
“space” (' ') followed by “backspace” ('\b'.) In no case should you echo the delete
or backspace character itself to the terminal. This is necessary because the “C''
meaning of '\b' and '\177' is to delete a character, which the terminal hardware can
only do with the sequence above. This sequence will result in the last character on
the screen appearing to be “erased.”. If the current input line was already empty
when the backspace or delete character was received, you should ignore the
backspace or delete character and do not echo anything for this character (some of
you may want to ring the bell, as some terminals do: you do this by sending '\007'
to the terminal.)
Line-oriented Input
You will notice that the specification of ReadTerminal() considers the input as
consisting of lines of text delimited with `\n', but that, at the same time, the terminal
hardware gives you a single character every time it raises the read interrupt. Something
similar can be said for WriteTerminal() and the write interrupt.
8
This implies that your terminal driver must implement the illusion (abstraction) of a lineoriented terminal on top of a character-oriented hardware device. It is usually true, as it is
in this case, that the diverse pieces of the operating system implement abstractions that
are not directly or completely supported by the hardware.
Implementing this line-oriented behavior in the terminal driver presents some
complications that may not be obvious at first. Consider the following application code:
int len1, len2, len3;
char buf1[2];
char buf2[10];
char buf3[10];
char buf4[10];
len1 = ReadTerminal(0,
len2 = ReadTerminal(0,
len3 = ReadTerminal(0,
len1 = ReadTerminal(0,
&buf1,
&buf2,
&buf3,
&buf4,
2);
10);
10);
10);
and suppose the user types the following on the keyboard of terminal 0:
Hello\b\b\b\bi\nUniverse\b\b\b\b\b\b\b\bWorld\nGood bye
From the specification of ReadTerminal(), and the character processing that the
driver is to perform, you should be able to deduce that, after the application code
executes:
len1 = 2
buf1 = "Hi"
len2 = 1
buf2 = "\n"
len3 = 6
buf3 = "World\n''
and that the application will be blocked on the last ReadTerminal() until the user
types a `\n' or `\r'
Think about why this is the case. You may come to realize that you need some buffering
inside your terminal driver.
Sharing Discipline
When used from different threads, the terminal functions can be called concurrently.
Furthermore, program-driven output also occurs concurrently with the output from the
echoing of input characters. We expect you to decide what behavior is reasonable in the
presence of concurrency, but here we set some minimum standards.
9

When a mix of more than one WriteTerminal occurs concurrently to the same
terminal, their outputs must not be interleaved on the screen. Output to two different
terminals may be interleaved.

The echoed input characters must be displayed at the earliest opportunity, as
previously mentioned. They can (indeed, some times must) be mingled, on the screen,
with the output from one or more WriteTerminals.

Multi-character sequences resulting from the character processing described above
must be displayed atomically, even if, as in the case of the echo stream, the stream
itself can be interleaved. This is to preserve the display effect of these sequences.

If there is more than one ReadTerminal waiting to read characters from a
terminal, the contents of each of their buffers must be formed by characters typed
sequentially at the keyboard. That is, input characters should go to a single
ReadTerminal until that ReadTerminal returns, and then on to the other one.
Concurrent ReadTerminals reading from the same terminal should not alternate
the reading of the input data register for that terminal.

Your device driver should implement line-oriented terminals. For this project, this
means that no data is returned to a ReadTerminal call until a newline has been
read. This does not necessarily imply that the whole line is returned to the
ReadTerminal: only as many characters as requested should be returned, and the
rest remain available for the next ReadTerminal(), even if it is issued by a
different user thread.
Driver Implementation Constraints
Device drivers are critical parts of the code of the operating system: they are very stressed by
concurrency, and operate at a very low level. Notably, they are often invoked from hardware
interrupt handlers, which typically preempt any other system activity, including important OS
functions. They are also restricted, for reasons that will become clear later in the course, to using
small portions of well-defined “pinned'' memory.
Because of this, your device driver should:

Not block for an indeterminate amount of time when running from an interrupt handler.
In particular, it should never, when called from an interrupt handler, block on a resource
being held by some other blocked thread.

Allocate and use a fixed amount of memory for its internal data structures. In the event
that a sequence of terminal operations should require the driver to use more memory, the
last operation of the sequence should be either not carried out or blocked, depending on
whether the operation is executed from within an interrupt handler, or not. In particular, it
10
is all right to drop input characters if they are coming in faster, on the average, than they
can be consumed by the applications. However, it is also unreasonable to require
applications to have a pending ReadTerminal at the time every single character
arrives. You must use buffers to deal with bursts and temporarily “absent'' applications,
but the buffers must be of finite size.
When adding one character to one such buffer would overflow the buffer, and the driver
cannot block waiting for the buffer to drain (e.g, if within an interrupt handler) you may
choose a course of action. You may drop the character as stated above, and you might,
depending on the situation, try to output a bell (`\007') to the terminal. The bell is not
required and may not be possible in all cases, but we expect you to implement reasonable
behavior for these cases.
User Programs
In this project, the user programs are threads which call WriteTerminal, ReadTerminal,
DeviceInputSpeed, DeviceOutputSpeed and SynchronousMulticast() at will, but only after
one of these threads (and only one) has called InitTerminal once (and only once) for each
terminal used.
The one thread that starts all of the user threads (e.g, in the test programs) is the boot thread,
which is started by the hardware at start-up time. This thread must not call the exit() routine
but may return before the user threads spawned by it complete their work. Any thread spawned
by the boot thread, or the boot thread itself, can call InitTerminal, but only one can do so.
When the boot thread is started, neither the terminal hardware nor your terminal driver are
active.
The boot thread's entry point is the procedure
int SystemBoot(int argc, char ** argv)
which is to be provided when testing your terminal driver, but which is not itself part of the
driver. The argc and argv arguments to this procedure are just like the arguments to the main
procedure of a C program.
Please note that you should not provide a main function anywhere, neither in your terminal
device driver nor in your test programs that use the terminal driver.
Please also note that the SystemBoot() procedure is not part of the terminal driver, but is
instead the part of the system that uses the terminal driver. Thus, any code related to your device
driver, including the initialization of any data structures you define, should not depend on any
particular SystemBoot() function - your terminal driver should be able to work with any mix of
application threads. In particular, when we test your driver, we will replace the SystemBoot()
function with one of our own. Because of this, you should put the code for any SystemBoot()
you use in a source file separate form the files you use to hold the code for your device driver.
11
We provide several sample SystemBoot() files in /afs/andrew/scs/cs/15412/pub/proj2/sample.
The Thread Package
The thread package you will use is similar to a subset of the Solaris Threads library. In order to
use the library you must include their prototypes as below:

#include <proj2.h>
You will also need to explicitly link the thread library when you link your program. For example
to link foo.o into an executable file foo, you could do the following:

cc -o foo foo.o -lproj2 -lthread
The library provides the following functions:

int thr_spawn(void * (*func)(void *), void * arg, thread_t *tid)
thr_spawn() starts a new thread, with an initial call to procedure proc. proc is a procedure whose
prototype is void * proc(void *), a procedure that takes a generic pointer as an argument and
returns a generic pointer. The argument can be specified via thr_spawn(‘)s arg argument. This
argument should be casted within proc to the appropriate type. Upon success, thr_spawn() returns
0 and sets tid to the identifier of the new thread. Upon failure, thr_spawn() returns an error code
that can be printed in English with perror().

int thr_gettid(void)
Returns the thread id of the calling thread;

int thr_msleep(long msecs)
Sleeps the calling thread for msecs milliseconds. This may or may not be useful during
debugging, but it should not be used in the final solution. Returns 0 after successfully sleeping,
and a negative error code if the operation could not be carried out.

int thr_perror(int err, char *msg, ...)
Prints a description of the error err returned by a thread library call, preceded by the characters
that would result from calling printf(msg, ...)

int mutex_create(mutex_t *mx)
mutex_create() initializes mutex mx. mx is allocated by the caller by declaring it as mutex_t mx.
mutex_create() returns 0 on success, and an error code upon failure.

int mutex_acquire(mutex_t *mx)
mutex_acquire() acquires mutex mx atomically, i.e, if mx is not locked it locks it atomically.
mutex_create() must have been called previously. Returns 0 on success, and an error code upon
failure.

int mutex_release(mutex_t *mx)
mutex_release() releases mutex mx atomically. If the mutex is not being held by the calling thread
it returns an error. mutex_create() must have been called previously to initialize the mutex. If
12
there are other threads waiting to lock the mutex (blocked by mutex_acquire()) it yields control to
one of them. Returns 0 on success, and an error code upon failure.

int mutex_destroy(mutex_t *mx)
mutex_destroy() deactivates the mutex pointed to by mx. The mutex must not be currently locked.
After a mutex is deactivated calls to mutex_acquire() and mutex_release() will have undefined
results. mutex_destroy() does not deallocate the memory pointed to by mx: any deallocation, if
necessary, must be done elsewhere. Returns 0 upon success, and an error code upon failure.

int cond_create(cond_t *cv)
cond_create() initializes conditional variable cv. cv is allocated by the caller by declaring it as
cond_t cv. Returns 0 on success, and an error code upon failure.

int cond_wait(cond_t *cv, mutex_t *mx)
cond_wait() affects the condition variable wait operation. It blocks until the same variable is
signaled using cond_signal(), but only one of possibly several waiting threads will be unblocked at
that point. The mutex pointed to by mx must be held by the calling thread. As in monitors,
cond_wait() unlocks mx before blocking to allow entry into other procedures protected by mx (i.e,
other entry procedures in the same monitor.) As in monitors, cond_wait() locks mx before
returning after having blocked to prevent other thread's entry into other entry procedures protected
by mx. Returns 0 upon success, and an error code upon failure.

int cond_signal(cond_t *cv)
cond_signal() affects the condition variable signal operation. It causes one thread currently
waiting for cv to be ready for execution. The waiting thread will not wake up until the mutex
associated with it when it called cond_wait() is released, presumably by the thread that calls
cond_signal(). Returns 0 upon success and an error code upon any failure.

int cond_destroy(cond_t *)
cond_destroy() deactivates the condition variable pointed to by cv. Any subsequent calls to
cond_signal() or cond_wait() have undefined results. The memory pointed to by cv is not
deallocated. If necessary, that space must be reclaimed elsewhere. Returns 0 upon success, and an
error code if anything goes wrong.
Implementation Using The Mesa Monitor Paradigm
As you know from lecture, monitors are synchronization primitives that are similar to “objects”
in the “object-oriented” sense, with ``private'' variables that can only be accessed through public
“methods” called entry procedures provided by the monitor. Monitors of course have the specific
features of mutual exclusion and condition variables.
Although neither the C language nor our thread library provides specific support for monitors, it
is possible to follow the monitor paradigm as we design and implement the terminal driver. The
most significant difference between using the monitor paradigm as we will and using it in an
environment that provides specific support for monitors is that we will have to be more selfdisciplined – neither the compiler nor the runtime environment can enforce the constraints
present of a monitor.
13
In particular, in order to use the monitor paradigm to provide mutually exclusive access to a
shared variable or collection of shared variables, you should use the thread library as follows:

A monitor is a collection of C language procedures and all shared variables controlled by
those procedures.

You should put all of the code for a monitor into a single file of C Language source code.
You should also put an initialization procedure for your monitor in that source file.
Nothing other than the monitor should be included in that same file. In other words, there
should be one file per monitor and this file should contain, exactly, the monitor.

In that initialization procedure, you should create one mutex variable to control mutual
exclusion for access to the monitor. You should not create any other mutex variables to
be used by this monitor. This means that your monitor uses only this one single mutex.

You should create any condition variables to be used within this monitor in the
initialization procedure. You should generally not create any new condition variables for
outside of the initialization procedure. This implies that your monitor uses only a finite,
pre-allocated set of condition variables.

Some of the C Language procedures in this file will be entry procedures of the monitor
and others will be called only from within the monitor. Each entry procedure should be
defined as extern and each internal procedure should be defined as static.

At the beginning of each entry procedure, you should acquire the mutex controlling
mutual exclusion to the monitor.

At the end of each entry procedure, you should release the mutex controlling mutual
exclusion to the monitor. Please remember that the mutex should be release anywhere
that a procedure returns – whether normally or abnormally.

You must not acquire or release this mutex at any other place in your code.

You may not use any other mutexes anywhere in your program.

Condition variables are per-monitor resources. They should only be used within a single
monitor.

Each call to cond_wait() must use the mutex that controls the monitor from within which
you are calling cond_wait().

The “Monitors version” of your assignment should use exactly one monitor. It should not
create other monitors, nor should it nest monitors. Think, “simple”.
14
Implementing Semaphores
The second part of this assignment is to implement semaphores. The implementation of
semaphores will involve a critical section. Please use the monitor paradigm, as described above,
to ensure mutually exclusive access to this critical section. In other words, a monitor should
encapsulate your semaphores implementation.
Your implementation of semaphores should be contained within the files sem.c and sem.h.
You should use typedef to define the type sem_t. This type will probably be a struct.
Your monitor should include the following entry procedures:

int sem_create(sem_t * sem, int value)
Creates a new semaphore (returned in sem) and initializes its value to value. Returns 0 on
success and -1 on any error.

int sem_destroy(sem_t sem)
Destroys the semaphore identified by sem. Before this procedure is called any of the other
procedures may be called. After this procedure is called any semaphore procedure calls have
undefined results. Returns 0 on success and -1 on any error. You should be careful to deallocate
(using free()) any memory (if any) allocated by malloc() inside sem_create.

int sem_P(sem_t sem)
Performs a semaphore P operation on the semaphore identified by sem. Returns 0 on success and
-1 on any error. Note that the text book for this course refers to this semaphore operation as
``wait''. We use the more common ``P'' to avoid confusion with the ``wait'' operation of monitor
condition variables.

int sem_V(sem_t sem)
Performs a semaphore V operation on the semaphore identified by sem. Returns 0 on success and
-1 on any error.
A Few Important Notes
This solution is all about paradigm, please observe the following restrictions:

Your terminal driver should be completely interrupt driven – it should not use any
threads. The only place you should create threads is within SystemBoot. This specifically
policy specifically rejects the sue of an “Echo Thread”.

Your semaphore should be implemented within a monitor.

“Working is [not] good enough!” We are concerned about how you accomplished
synchronization, not that you did accomplish synchronization.
15
Running Your Terminal Drivers
When you finally have a terminal driver compiled and you are ready to try it, you simply run
your executable (e.g, montty.)
The executable, as linked with the library proj2, will try to open an xterm into your display
whenever DeviceInit is called. This xterm will emulate the terminal device with which the
hardware procedures (e.g, WriteDataRegister) interact.
Any output from your program (written to stdout and stderr via e.g. fprintf) will go to the
Solaris shell from which you invoked the executable.
If you would like all this output to go into a file, you can run your executable with the -log
command line switch. For example, if you wanted to save the output from your program (which
may be important for debugging) into the file ttylog, you might type the following at your shell
command line:
ttytest –noX -log ttylog
You can use xterm even if you are not sitting directly in front of a Sun computer running the
Solaris operating system. You do this by telneting to a Solaris machine (several are available
by telneting to far-sun4.andrew.cmu.edu) and setting the DISPLAY environment variable like
this:
setenv DISPLAY machine.andrew.cmu.edu:0.0
where machine is the name of the computer at which you are sitting.
You may run your 15-412 terminal driver even if you are at a computer that does not run X (PC,
Mac.) To do this you run your executable with the -noX command line switch. The simulated
terminals will then output everything to the single terminal, but input from that terminal will only
go to minor device 0. In this case, you would do well to redirect the output of your program via
the -log switch, as described before. So, if you are sitting at a computer that does not run the X
window system, you might type:
tty -noX -log ttylog
Remember that in all cases you must be logged onto a Sun Solaris machine before you can run
your terminal driver.
Skeleton Makefile
We have provided a skeleton makefile, Makefile.template. It lives in
/afs/andrew/scs/cs/15-412/pub/proj2/sample/Makefile.template.
modify it to suit your needs, or at least take a careful look at it.
16
We suggest that you
It contains rules to make the executables sem-test, montty, and semtty. These are to be the
executables for the three parts of the project. The rules that make these executables will
automatically link the hardware emulation library and the thread library. Your source files should
also be linked by these rules.
You may want to modify the definitions of the variables SEMTEST_OBJ, MONTTY_OBJ and
SEMTTY_OBJ within the Makefile to include the names of your source files. These are already set
to defaults so that if your source files are sem.c, montty.c and semtty.c you need not modify
the “make” variables.
If you prefer to use the GNU C Compiler (gcc) or the Solaris native C compiler cc, you may also
change the ``make'' variable CC in the Makefile.
The files sem-test.c, montty-test.c and semtty-test.c are the programs implementing the
user threads that will use your monitors and/or terminal driver.
To use one of the tests that we provide, or those of your own creation, you can copy the file into
your directory and rename it to montty-test.c or semtty-test.c depending on what part of
your project you want to compile (or all of them.)
Any include files written by you and included by your programs should be in your work
directory and should be included with double quotes, that is, you should include them using
#include "file.h" rather than #include <file.h>.
Suggested Plan of Attack
Please start early.
There is no need for you to do things in any order. However, some parts of this assignment will
take longer than you think. This is not because you will have to write a lot of code, but because
some of the bugs you will encounter will be reluctant to show themselves. Typical solutions have
been about 400-500 lines of C code.
Please start early.
One of many reasonable plans to attack this project follows.
Please start early.
1) Read this handout. Read it again. Make sure you understand the bit about the top and
bottom halves of device drivers. Think about how to join them. Realize that the top and
the bottom may have completely different and independent timing, and that the
granularity of the objects they operate on are different (lines versus characters.)
Please start early.
17
2) Decide where buffers and other shared structures will be needed and which functions will
interact with them. Draw out your design on paper. Show the buffers and other data
structures. As well as the procedures that will access them.
Please start early.
3) Identify the synchronization problems.
Please start early.
4) Identify the critical section. Add notations indicating these to your diagram – this
completes the first part of the checkpoint.
Please start early.
5) Focus on how the monitor and semaphore implementations will differ, and try to
modularize the differences so that you can re-use as much of the code as possible.
Please start early.
6) Write a monitor bottom half that succesfully echoes characters to the terminal, without
having the ReceiveInterrupt handler block on the TransmitInterrupt. This
completes the second portion of the checkpoint.
Please start early.
7) Splice WriteTerminal() into your bottom half. Make sure that the echo has priority over
WriteTerminal() and that two or more concurrent WriteTerminal()s do not interfere
with each other.
Please start early.
8) Splice ReadTerminal() into your driver. Make sure that echoing of characters to the
screen is not affected by an application's failure to call ReadTerminal().
Please start early.
9) Think about where best to introduce input and output character processing. Realize that
the processing required for echo, output, and input, are different, but that they are not
entirely dissimilar either.
Please start early.
10) Add character processing.
Please start early.
11) Thoroughly test what you have written so far.
Please start early.
.
18
12) Implement and test the semaphore primitives.
Please start early.
13) Implement the semaphore version of what you have done so far.
Please start early.
14) Test everything some more.
15) Congratulations – you’re done!
19
15-412: Project # 2
The Terminal Driver: Concurrency & Synchronization
1 Overview
1 Times of Interest
1 Educational Objectives
1 Project Overview
2 Miscellany
2 Miscellany
2 Groups
2 Environment
2 Form
2 Grading
3 Checkpoint
3 What is a Terminal Driver?
4 The Terminal Hardware
5 The Hardware Emulation API
6 Required Terminal Driver Procedures
6 Required Terminal Driver Procedures
6 Interrupt Handlers
6 Terminal Driver API
7 Terminal Behavior
7 Character Processing
8 Line-oriented Terminal
9 Sharing Discipline
10 Driver Implementation Constraints
11 User Programs
12 The Thread Package
13 Implementation Using the Mesa Monitor Paradigm
12 Implementing Semaphores
15 A Few Important Notes
16 Running Your Terminal Driver
16 Skeleton Makefile
17 Suggested Plan of Attack
20
Download