Programming Project 2:
Due Date: ______________________________
Duration: One
Week
In this project you will
learn about threads and gain familiarity writing programs involving concurrency
control. You will begin by
studying the thread package, which implements multi-threading. You will make modifications and
additions to the existing code.
Then you will use the threads package to solve some traditional
concurrency problems.
In addition, you will gain
familiarity programming in the KPL language.
If you have difficulties
with this project or want to go into more depth, take a look at the document
called ÒThe Thread Scheduler and Concurrency Control PrimitivesÓ:
http://web.cecs.pdx.edu/~harry/Blitz/BlitzDoc/ThreadScheduler.htm
http://web.cecs.pdx.edu/~harry/Blitz/BlitzDoc/ThreadScheduler.pdf
Start by creating a
directory for the files youÕll need for this project. You might call it:
~YourUserName/cs333/p2
Then download all the files
from:
http://www.cs.pdx.edu/~harry/Blitz/OSProject/p2/
into your directory. You should get the following files:
makefile
DISK
System.h
System.c
Runtime.s
Switch.s
List.h
List.c
Thread.h
Thread.c
Main.h
Main.c
Synch.h
Synch.c
In this project, you will
modify and hand in the following files:
Main.c
Synch.h
Synch.c
After getting the files, you
should be able to compile all the code (as is) with the UNIX make command. The program executable we are building will be called
ÒosÓ. You can execute the program
by typing:
% make
% blitz –g os
In the course of your
experimentation, you may modify other files besides Synch.h, Synch.c and Main.c, but the code you are required
to write and turn in doesnÕt require any changes to the other files. For example, you may wish to uncomment
some of the print statements, to see what happens. However, your final
versions of Synch.h, Synch.c and Main.c must work with the other files, exactly as they are
distributed.
Be sure you copy all
files. Even though there are
similarities with some of the files used for the previous project, there may be
some subtle but important differences.
The code provided in this
project provides the ability to create and run multiple threads, and to control
concurrency through several synchronization methods.
Start by looking over the System package. Focus on the material toward the
beginning of file System.c, namely
the functions:
printInt
printHex
printChar
printBool
nl
StrEqual
StrCopy
StrCmp
Min
Max
printIntVar
printHexVar
printBoolVar
printCharVar
printPtr
Get familiar with the
printing functions; youÕll be calling them a lot. Some are implemented in assembly code and some are
implemented in KPL in the System
package.
The following functions are
used to implement the heap in KPL:
KPLSystemInitialize
KPLMemoryAlloc
KPLMemoryFree
Objects can be allocated on
the heap and freed with the alloc
and free statements. The HEAP implementation is very
rudimentary in this implementation.
In your kernel, you may allocate objects during start-up but after that,
YOU MUST NOT ALLOCATE OBJECTS ON THE HEAP! Why? Because the heap might fill up and then what is a kernel
supposed to do? Crash.
In this project, you should
not allocate anything on the heap.
The following functions can
be ignored since they concern aspects of the KPL language that we will not be
using:
KPLUncaughtThrow
UncaughtThrowError
KPLIsKindOf
KPLSystemError
The Runtime.s file contains a number of routines coded in assembly
language. It contains the program
entry point and the interrupt vector in low memory. Take a look at it.
Follow what happens when program execution begins at location 0x00000000
(the label Ò_entryÓ). The code
labeled Ò_mainEntryÓ is included in code the compiler produces. The Ò_mainEntryÓ code will call the main function, which appears in the
file Main.c.
In Runtime.s, follow what happens when a timer interrupt occurs. It makes an Òup-callÓ to a routine
called _P_Thread_TimerInterruptHandler. This name refers to Òa function called TimerInterruptHandler in a package
called Thread.Ó (_P_Thread_TimerInterruptHandler
is the name the compiler will give this function.)
All the code in this project
assumes that no other interrupt types (such as a DiskInterrupt) occur.
In Runtime.s, follow what
would happen if another sort of interrupt should ever occur.
The KPL language will check
for lots of error conditions, such as the use of a null pointer. Try changing the program to make this
error. Follow in Runtime.s what happens when this
occurs.
Next take a look at the List package. Read the header file carefully. This package provides code that implements a linked
list. WeÕll use linked lists in
this project. For example, the
threads that are ready to run (and waiting for time on the CPU) will be kept in
a linked list called the Òready listÓ.
Threads that become BLOCKED will sit on other linked lists. Also look over the List.c code file.
The key class in this
project is named Thread, and it is
located in the Thread package along
with other stuff (see Thread.h, Thread.c). For each thread, there will be a single Thread object. Thread is a subclass of Listable, which means that each Thread object contains a next pointer and can be added to a
linked list.
The Thread package is central and you should study this code
thoroughly. This package contains
one class (called Thread) and
several functions related to thread scheduling and time-slicing:
InitializeScheduler ()
IdleFunction (arg: int)
Run (nextThread: ptr to Thread)
PrintReadyList ()
ThreadStartMain ()
ThreadFinish ()
FatalError (errorMessage: ptr to array of char)
SetInterruptsTo (newStatus: int) returns int
TimerInterruptHandler ()
FatalError
is the simplest function. We will
call FatalError whenever we wish to
print an error message and abort the program. Typically, weÕll call FatalError
after making some check and finding that things are not as expected. FatalError
will print the name of the thread invoking it, print the message, and then shut
down. It will throw us into the
BLITZ emulator command line mode.
Normally, the next thing to do might be to type the ÒstÓ command (short
for ÒstackÓ), to see which functions and methods were active.
(Of course the information
printed out by the emulator will pertain to only the thread that invoked FatalError. The emulator does not know about threads, and it is pretty
much impossible to extract information about other threads by examining bytes
in memory.)
The next function to look at
is SetInterruptsTo, which is used to
change the ÒIÓ interrupt bit in the CPU.
We can use it to disable interrupts with code like this:
... = SetInterruptsTo
(DISABLED)
and we can use it to enable
interrupts:
... = SetInterruptsTo
(ENABLED)
This function returns the
previous status. This is very
useful because we often want to DISABLE interrupts (regardless of what they
were before) and then later we want to return the interrupt status to whatever
it was before. In our kernel,
weÕll often see code like:
var oldIntStat:
int
...
oldIntStat =
SetInterruptsTo (DISABLED)
...
oldIntStat =
SetInterruptsTo (oldIntStat)
Next take a look at the Thread class. Here are the fields of Thread:
name: ptr to array of char
status: int
systemStack: array [SYSTEM_STACK_SIZE] of int
regs: array [13] of int
stackTop: ptr to void
initialFunction: ptr to function (int)
initialArgument: int
Here are the operations
(i.e., methods) you can do on a Thread:
Init (n: ptr to array of char)
Fork (fun: ptr to function (int), arg: int)
Yield ()
Sleep ()
CheckOverflow ()
Print ()
Each thread is in one of the
following states: JUST_CREATED,
READY, RUNNING, BLOCKED, and UNUSED, and this is given in the status field. (The UNUSED status is given to a Thread after it has terminated. WeÕll need this in later projects.)
Each thread has a name. To create a thread, youÕll need a Thread variable.
First, use Init to initialize
it, providing a name.
Each thread needs its own
stack and space for this stack is placed directly in the Thread object in the field called systemStack.
Currently, this is an array of 1000 words, which should be enough. (It is conceivable our code could
overflow this limit; there is a check to make sure we donÕt overflow this
limited area.)
All threads in this project
will run in System mode. Therefore
the stack is called the Òsystem stackÓ.
In later projects, weÕll see that this stack is used only for kernel
routines. User programs will have
their own stacks in their virtual address spaces, but we are getting ahead of
ourselves.
The Thread object also has space to store the state of the CPU, namely
the registers. Whenever a thread
switch occurs, the registers will be saved in the Thread object. These
fields (regs and stackTop) are used by the assembly code
routine named Switch.
Execute and trace through
the output of SimpleThreadExample in
file Main.c.
In TimerInterruptHandler there is a statement
printChar ('_')
which is commented out. Try uncommenting it. Make sure you understand the output.
In TimerInterruptHandler, there is a call to Yield. Why is this
there? Try commenting this
statement? What happens. Make sure you understand how Yield works here.
Trace through the
output. Try changing this code to
see what happens.
In this part, you must
implement the class Mutex. The class specification for Mutex is given to you in Synch.h:
class Mutex
superclass Object
methods
Init ()
Lock ()
Unlock ()
IsHeldByCurrentThread () returns bool
endClass
You will need to provide
code for each of these methods. In
Synch.c youÕll see a behavior construct for Mutex. There are methods for Init,
Lock, Unlock, and IsHeldByCurrentThread,
but these have dummy bodies.
YouÕll need to write the code for these four methods.
You will also need to add a
couple of fields to the class
specification of Mutex to implement the
desired functionality.
How can you implement the Mutex class? Take a close look at the Semaphore class; your implementation of Mutex will be quite similar.
First consider the IsHeldByCurrentThread method, which may
be invoked by any thread. The code
of this method will need to know which thread is holding a lock on the mutex;
then it can compare that to the currentThread
to see if they are the same. So,
you might consider adding a field (perhaps called heldBy) to the Mutex
class, which will be a pointer to the thread holding the mutex. Of course, youÕll need to set it to the
current thread whenever the mutex is locked. You might use a null value in this field to indicate that no
thread is holding a lock on the mutex.
When a lock is requested on
the mutex, youÕll need to see if any thread already has a lock on this
mutex. If so, youÕll need to put
the current process to sleep. For
putting a thread to sleep, take a look at the method Semaphore.Down. At any
one time, there may be zero, one, or many threads waiting to acquire a lock on
the mutex; youÕll need to keep a list of these threads so that when an Unlock is executed, you can wake up one
of them. As in the case of Semaphores, you should use a FIFO
queue, waking up the thread that has been waiting longest.
When a mutex lock is
released (in the Unlock method),
youÕll need to see if there are any threads waiting to acquire a lock on the
mutex. You can choose one and move
it back onto the readyList. Now the waiting thread will begin
running when it gets a turn. The
code in Semaphore.Up does something
similar.
It is also a good idea to
add an error check in the Lock
method to make sure that the current thread asking to lock the mutex doesnÕt
already hold a lock on the mutex.
If it does, you can simply invoke FatalError. (This would probably indicate a logic
error in the code using the mutex.
It would lead to a deadlock, with a thread frozen forever, waiting for
itself to release the lock.)
Likewise, you should also add a check in Unlock to make sure the current thread really does hold the lock
and call FatalError if not. YouÕll be using your Mutex class later, so these checks will
help your debugging in later projects.
The function TestMutex in Main.c is provided to exercise your implementation of Mutex. It creates 7 threads that compete vigorously for a single
mutex lock.
Both the Tanenbaum and
Silberschatz, et al. textbooks contain a discussion of the Producer-Consumer
problem, including a solution.
Implement this in KPL using the classes Mutex and Semaphore. Deal with multiple producers and
multiple consumers, all sharing a single bounded buffer.
The Main package contains some code that will serve as a
framework. The buffer is called buffer and contains up to BUFFER_SIZE (e.g., 5) characters. There are 5 producer processes, each
modeled by a thread, and 3 consumer processes, each modeled by a thread. Thus, there are 8 threads in addition
to the main thread that creates the others.
Each producer will loop,
adding 5 characters to the buffer.
The first producer will add five ÔAÕ characters, the second producer
will add five ÔBÕs, etc. However,
since the execution of these threads will be interleaved, the characters will
be added in a somewhat random order.
A starting framework for
your solution is provided in Main.c. Each philosopher is modeled with a
thread and the code weÕve provided sets up these threads. The synchronization will be controlled
by a ÒmonitorÓ called ForkMonitor.
The code for each
thread/philosopher is provided for you.
Look over the PhilosophizeAndEat
method; you should not need to change this code.
The monitor to control
synchronization between the threads is implemented with a class called ForkMonitor. The following class specification of ForkMonitor is provided:
class ForkMonitor
superclass Object
fields
status: array
[5] of int
-- For each philosopher: HUNGRY,
-- EATING, or THINKING
methods
Init ()
PickupForks (p:
int)
PutDownForks
(p: int)
PrintAllStatus
()
endClass
YouÕll need to provide the
code for the Init, PickupForks and PutDownForks methods.
YouÕll also need to add additional fields and perhaps even add another
method.
The code for PrintAllStatus is provided. You should call this method whenever
you change the status of any philosopher.
This method will print a line of output, so you can see what is
happening.
How can you proceed? YouÕll need a mutex to protect the
monitor itself. There are two main
methods (PickupForks and PutDownForks) which are called by the
philosopher threads. Upon
beginning each of these methods, the first thing is to lock the monitor
mutex. This will ensure that only
one thread at a time is executing within the monitor. Just before each of these methods returns, it must unlock
the monitor (by unlocking the monitorÕs mutex) so that other threads can enter
the monitor code.
YouÕll also need to use the Condition class, which is provided in
the Synch package. (The Condition class uses the class Mutex,
so it is assumed that youÕve finished and tested the Mutex class.)
The BLITZ emulator has a
number of parameters and one of these is how often a timer interrupt
occurs. The default value is every
5000 instructions. You might try
changing this parameter to see how it affects your programs behavior. To change the simulation parameters,
type the sim command into the
emulator. This command will give
you the option to create a file called
.blitzrc
After creating this file,
you can edit it by hand. The next
time you run the emulator, it will use this new value. Also note that too small a
value—like 1000—will cause the program to hang. What do you suppose causes this effect?
QUESTION: Both
textbooks discuss the semantics of signaling a condition variable. They mention ÒHoare semantics.Ó The comments in the code in Synch.c say that this version
implements ÒMesa-styleÓ semantics.
Is this the same or different from Hoare semantics?
The following files contain
an example of what correct output should look like:
DesiredOutput1.pdf
DesiredOutput2.pdf
DesiredOutput3.pdf
Complete all the above
steps.
Please submit hardcopy of
the following files:
Synch.h
Synch.c
Main.c
Also include hardcopy
showing the output for steps 5, 6, and 7.
Please print both your code
and the output using a fixed-width font like this, in this and all future assignments. Much of the output is designed to look
nice when printed with a fixed-width font, but is more difficult to read in a
standard variable-width font.
In LARGE BLOCK LETTERS,
write your full name on the first page.
PLEASE STAPLE ALL PAGES!
In this course, the code you
submit will be graded on both correctness
and style. Correctness means that the code must work right and not
contain bugs. Style means that the
code must be neat, well commented, well organized, and easy to read.
In style, your code should
match the code we are distributing to you. The indentation should be the same and the degree of
commenting should be the same.