CSE 121 Spring 2003 Project 3
Due 7 June 2003
Overview
Problem Summary
For Project 3, you will extend the non-preemptive threads package of Project
2 to include priorities, preemptive scheduling, condition time-outs, and
thread-safe user input from stdin. Generation of timer interrupts and exception
handling will be discussed in section or in class. It is suggested that
you experiment with each of these concepts prior to beginning the project.
You should write this project in C, and it should run as specified on
ieng9. The latter is more of an issue than Project 2 because you will be
using system calls of Unix that are not as well standardized as those that
you used for Project 2.
Project 3 is due at the midnight of June 7, 2003.
Thread Priorities
Users can assign a priority to a thread from the range MinPriority (0)
to MaxPriority (128). Your threads package may find it useful to use priorities
outside of this range. The currently active thread should be the highest
priority thread that is runnable (i.e. not blocked on a condition). Lower
priority threads execute only when there are no higher priority threads
ready to run. As soon as a higher priority thread is ready to execute, it
preempts any lower priority thread that is currently running.
Time-outs
Your threads package will support timed conditions. For the most part,
signals and conditions function the same as defined in Project 2. In Project
3, though, one will be able to set a timeout with a condition. A thread that
waits on a condition for timeout milliseconds will become ready, just as
if it had received a signal with a value of NULL. If the condition is signalled
before that timeout, however, then the blocked thread will behave as specified
in Project 2.
Pre-emptive Scheduling
In Project 2, a thread runs until it explicitly yields or waits on a condition.
Thus, a thread that goes into an infinite loop will never yield the process
to another thread.
In Project 3 you will preempt threads that run for too long without blocking
or yielding. The interval between preemptions (called the quantum) set
by a new function defined in thread.h.
The quantum will be changeable, but in your design document, you should
recommend an appropriate value for the quantum. Be sure to defend why you
recommended the value that you do.
Timers and Signals
Your threads package will need to set up a signal handler for timer signals
sent to the process. Note, this signal handler can also help you with scheduling.
We will use virtual time for the project instead of wall-clock time. Virtual
time does not count CPU cycles spent on other processes, and so, is more
suitable for this project. The Unix call setitimer() can be used to generate
SIGVTALRM signals (i.e. virtual time signals). As covered in class, handlers
using the Unix signal() system call. You will need to establish a signal
handler for the SIGVTALRM signal. The handler you specify will be called
when a (Unix timer) signal is received by the process. Your handler should
save its context, decide which context to run next, and switch (maybe longjmp)
to it. The use of all relevant Unix calls, as well as the saving and restoring
of signal masks, will be covered in section or in class, but you can read
about it yourself by looking at the man pages on setitimer and signal.
Do not confuse Unix signals with thread signals. Unix signals are sent
via kill() or setitimer() and are handled by the handler you specify in
signal(). Thread signals are implemented by your package and are sent using
t_sig().
To drive the system, you will use setitimer to generate interrupts at
a certain rate. The period between these interrupts is called the granularity
of your thread package's clock. You will need to determine a reasonable
granularity for the clock. Test your package at different granularities
to determine this value. Explain in your documentation why you chose a particular
granularity and be able to defend it.
Standard Headers
You will be provided with the same two standard header files, called
/home/solaris/ieng9/cs121s/public/p2/{standard.h,thread.h}, which
you should #include unchanged in your program. (Don't copy them into
your directory, and don't hand them in.)
The following definitions have been added to the standard.h header
file:
- Priorities : Two priority values have been defined.
MinPriority defines the lowest priority that a client can assign
to a thread, and MaxPriority defines the highest priority.
The second header file thread.h has been modified to update the
interface to your threads package to reflect the new capabilities defined
for project three, and to include further functionality.
Data Abstractions
The data abstractions defined for project two still pertain for this project.
Modifications may be required to satisfy the specification for project
two. Below are some questions worth thinking about:
- How will priorities affect these data structures?
- What attributes are now associated with a thread?
- How will conditions support time-outs?
System Commands
For project three you will need to become familiar with the system timer,
signal (interrupt), and file control interfaces. You may need to use some
of these system capabilities to implement time-slicing, condition time-outs,
concurrency control (needed to satisfy safety properties), and non-blocking
user input. It is suggested that you look at the man pages for the following
systems calls: signal, setitimer (itimer), sigset_t, sigemptyset, sigaddset,
sigprocmask, siglongjmp, sigsetjmp, and fcntl.
Some questions you may ask yourself include:
- What timing information do I need to keep?
- What is the maximum quantum of time that I want to allow a thread
to run?
- At what granularity do I want to receive clock interrupts?
- What will my timer interrupt handler do?
- How do asynchronous interrupts (like timer interrupts) affect my
process?
- When will I perform (or at least check for) a context switch between
threads? (Note: A context switch may be performed in many instances.
Think carefully about this and include these observations in your document.)
- What about critical regions of code?
- Do I ever need to mask out interrupts? If so, how? Why?
- How do I read in user input asynchronously?
Needed Routines
At the minimum you must write the routines described below. Their type
signatures are defined in thread.h. For those routines that were
specified in Project 2, only the modifications to their implementation will
be listed. These routines must still provide the basic functionality outlined
in Project 2. Note, this project can be implemented elegantly if the threads
functionality is reused for implementing itself. This was done in Project
2, for example, where we suggested that new threads can be created by a spawner
thread created by t_start and activated by waiting on a condition
signaled in t_fork. Describe in your documentation the degree to
which you reuse thread code.
Condition cond_create(void)
Boolean cond_is_empty(Condition cond)
void cond_destroy(Condition cond)
int cond_set_timeout(Condition cond, int msec)
Each condition can have a time-out quantum associated with it. Initially,
when the condition is created it will not have a time-out value. cond_set_timeout
can be used to set a time-out quantum for a condition, or to unset the
time-out quantum (by specifying msec equal to zero). It will return the
old time-out quantum that was previously set for that condition. If the
time-out quantum is set, then when a thread blocks on that condition it
will only block for the given quantum of time without being signaled. When
the ticker of the blocked thread counts down to zero, the thread should
be unblocked and moved to the ready queue with a signal value of NULL.
If cond_set_timeout is called when there are blocked threads
on the condition, then these threads will time out based on the new quantum.
For example, suppose a thread t waits on a condition with a time-out of
100 msec. After 40 msec, the time-out associated with the condition is set
to 200 msec. Then, t will time out after 200 more msec have elapsed - that
is, 240 msec after it initially executed t_wait.
void t_start(ThreadFunc f, any_ptr v, Priority p)
void t_fork(ThreadFunc f, any_ptr v, Priority p)
As in Project 2, you must bootstrap the system. This requires initializing
all internal threads, conditions, attributes, etc. that your package requires.
In addition, t_start is responsible for creating the first thread.
Al thread starts executing the user's routine f with parameter
v at priority level p.
Each thread, including the initial thread, is assigned a priority. You
will need to keep track of this priority for the life of the thread. This
priority will be used for scheduling. The currently-running thread must
be the ready thread with the highest priority. If a signal is sent to a
condition upon which more than one thread is blocked, then the thread with
the highest priority is the one that is unblocked. And, if Thread A signals
an empty condition with a value x, after which Thread B with a higher priority
signals the condition with a value y, then a thread that subsequently executes
t_wait will obtain the value y. Like in Project 2, the priority
with respect to blocking and signalling on conditions should be FIFO for
threads with the same priority.
The stack space of a thread that has terminated (i.e., returned from the
specified function) is expected to be recycled in this project. This can
be accomplished in many ways. For example, the finished thread can block
on a "dead-thread" condition. Then, t_fork can signal this condition
instead of the spawner condition. Doing so will require some modification
to spawner.
t_start does not return until all the user's threads have exited
or blocked (if all are blocked, the system is deadlocked). It should set
up a jmp_buf which will be longjmp'd to when deadlock
is detected, or when the last thread exits. In addition, it should be possible
for the main program to call t_start repeatedly.
long t_set_quantum(long msec)
This routine is used to set the quantum of time that threads are allowed
to execute. Once a thread has continuously run for this quantum of time
it should check the ready queue to determine if another thread is available
to run. If there is another thread with the same priority, then the current
thread should pre-empt and allow this other thread to run. (Why do you
only need to check for threads with the same priority?)
A quantum of zero indicates no preemption. The initial value of the quantum
should be zero.
int getline (char *buffer, int n)
This routine reads up to n characters from standard in (file descriptor
1), placing them into array buffer. This function returns when n characters
have been entererd, a \n is entered, or a ^D is entered. It returns 0 if ^D
is entered, 1 otherwise.
Priority t_priority()
This routine returns the current priority of the thread.
Priority t_set_priority(Priority p)
This routine is used to set, or reset, the priority of a thread. It
returns the old priority value. Note, when a thread priority is reset, it
may be immediately preempted.
void t_yield()
This routine places the current thread on the ready queue, possibly
allowing another thread to run. It has no effect if there are no other threads
on the ready queue. Note, your scheduler must take into account thread
priorities when selecting the next thread to execute.
any_ptr t_wait(Condition cond)
The calling thread blocks, waiting for some other thread to signal the
condition (using t_sig below). If several threads wait on the
same condition, they should be woken up, one per signal, in priority order.
If there are pending unreceived signals on the condition, the thread will
not block, but will take the highest priority signal (where the priority
of a signal is equal to the priority of the thread that placed the signal
on the queue via t_sig).
void t_sig(Condition cond, any_ptr val, Boolean queue_signal)
A condition cond has been signalled. val is an optional
parameter that will be passed to the thread awoken by the condition. If
there are threads currently blocked on the condition, the highest priority
thread should be woken up and made ready to run. This process will be inside
t_wait, and so that function will return val.
As in Project 2, if no thread is blocked on the condition, what action
is taken will depend on the queue_signal parameter. If queue_signal
is TRUE, the signal info (including at least the parameter val)
gets added to a ``pending signals'' queue of the condition, with the assumption
that some future thread will block on the condition. Signals should be
queued in priority order. If queue_signal is FALSE, the
signal and parameter val are simply discarded.
Testing
We will link a couple of test programs to your module(s) to test the required
operations defined for project three. That is why we have provided you
with the definitions in thread.h. By including this header file,
we will be able to test your package.
The test modules will include main(). We will make them available
for public use within two weeks. When you hand in your program, your makefile
should compile all of the test cases. (See ``Handing In'', below.)
Documentation
You should hand in a Design Document with your project. This document
should provide an overview of your program's organization and design. It
should serve as a rough ``road-map'' of your program that an outsider might
read before tackling the code. You should list and briefly describe the
major functions, sketch out a typical execution path, and explain any major
design decisions or unusual algorithms. A rehash of this handout is not
acceptable documentation. Look through the project description though, in
it you may find many questions and other suggestions which would fit nicely
into your documentation.
You may also turn in other documents with your project if you feel they
will help to explain your project better (e.g. a "known bug" list if there
are several known bugs, or a "README" file
Handing In
Your project must be compiled into a library by your makefile. A threads
library can be created by adding these lines to your makefile (be sure
to include any needed object files, including the queue implementation,
in the library). For example:
libthreads.a: file1.o file2.o
ar crv libthreads.a file1.o file2.o
ranlib libthreads.a
This library can then be compiled with other .o files. For example:
file: file_x.o file_y.o libthreads.a
gcc -o file file_x.o file_y.o libthreads.a
The completed project must be submitted via turnin. It should include
all required files needed to grade your project. For example, you will
need to include all of your .h and .c files, your makefile,
your document, your threads library, plus others necessary files. Do not
include the standard.h, thread.h, and test files which we have provided
you. You must make the appropriate links to the /home/solaris/ieng9/cs121s/public/p2
directory in your make file.
The tar file should include a make file (titled makefile) set
up such that the command `` make'' will create all three test
executables {mintest; testthread; rtest} that
we have specified for you to run. Your executables will be the combination
of your .c files and our test modules (we provide the main()).
Last revised on 15 May 2003 by ryhuang.