In this assignment you will enhance your current operating system by adding three new features: (1) quantum-based preemption (making your scheduler implement a Round Robin strategy and a multi-level feedback strategy), (2) semaphore operations, and (3) blocking I/O with your keyboard handler.
We have provided files for your use. These files include:
You will use proj2.c to run tests on the two scheduling algorithms. The other files have been updated and should be warning-free under Borland C++ 4.5.
Most of the work in this project will go into implementing ``sleep'' and ``wakeup'' mechanisms. Your kernel will support the following two functions:
Ksleep( int semaphoreID ) Kwakeup( int semaphoreID )
These functions are not top-level system calls,
such as Proc_start.
These are function calls
that the kernel
will use to help implement system calls such as
In project 2, a process was in one of two states. Either it was running or ready. There were queues associated with each of these states. In this project, a process can be in a third state. It can be blocked. A blocked process is not ready to run, and is usually waiting for some event (such as an interrupt) to wake the process up. If a process is blocked, it is placed on a blocked queue.
A process is blocked if it would have busy waited if it ran. This occurs in two cases. A process is either waiting for some I/O operation to complete (the interrupt handler will then wake a process when the operation completes), or it is waiting for some other process to execute a V() operation if it is blocked because of a call to P(). A blocked process will have its PCB placed on a blocked queue (also called a sleep queue), and hence will not be considered by the scheduler when it comes time to pick a new process's PCB to place on the run queue. The scheduler will only choose PCBs from the ready queue. By blocking a process, we make more efficient use of the CPU.
You will implement two functions that are only visible to the kernel (they are not system calls) which will place PCBs into a semaphore (blocked) queue, and remove them from the semaphore queue. These functions are Ksleep() and Kwakeup().
Ksleep() will put the running process to sleep by moving the PCB from the run queue to one of several semaphore queues. The parameter, semaphoreID , will determine which queue to place the PCB on. A suggestion for implementing the semaphore queues is to use an array. The array will be indexed by semaphoreID . Since Ksleep() puts the running process to sleep, it should schedule a new process to run by calling Scheduler().
Kwakeup()will be used wake a process up. You will use semaphoreID to index into the semaphore array, as mentioned previously, find the appropriate semaphore queue, and dequeue the first PCB (if there is one) from this queue, and place it on the ready queue. Kwakeup() will normally be called during an interrupt handler (and System_service()). The process that called Kwakeup() will eventually resume (assuming it isn't context switched). Contrast this with Ksleep() which puts the running process to sleep, and schedules a new process.
Ksleep() should only be called while executing a system call (i.e., in System_service). Do not use Ksleep() while executing a general ISR such as Key_handler().
Why? Recall that an interrupt handler can go off at any time. Ksleep() puts the current running process to sleep (i.e., blocks the current process). If you use Ksleep() in an interrupt handler like Key_handler(), you will be randomly putting processes to sleep. This is not a good idea. The user process should decide when it goes to sleep. Either it goes to sleep from a P() call or a system call (typically, dealing with I/O).
On the other hand, you can call Kwakeup() from either a system call or an interrupt service routine -- but not directly from a user program. The reason you are allowed to call Kwakeup() from an interrupt handler is because it merely moves a PCB from a semaphore queue to the ready queue. The process being interrupted will continue to execute after the interrupt completes. Kwakeup()will be used in the implementation of V() which will be explained later.
There are three modes of execution: interrupt, kernel and user. However, you will only keep track of two modes: kernel mode and non-kernel mode (which will be called user mode). In kernel mode (i.e., while servicing a system call), the running process may not be context switched. If a timer interrupt goes off while executing in kernel mode, and if the process is slated to be context-switched, this action will be postponed until the end of the kernel code, and the system call will be allowed to run until completion. We will describe when and how to switch processes if it occurs in kernel mode.
DOS provides a timer interrupt service routine (ISR) for the timer chip in the PC that runs continuously with a frequency of 18.2 Hz (the period is about 55 milliseconds). You will replace the DOS timer ISR with your own. You will do this by resetting the interrupt vector 8 to your ISR. Like all other interrupt handlers, make sure that you save the address of the old timer handler -- e.g., in a variable declared as void interrupt (*OldtimerISR)(void). A new process will be initialized to have a full quantum, which is some integer multiple of the number of times the timer interrupt goes off before the process is context-switched. For example, this value might be 4. When a timer interrupt occurs, three possibilities exist:
You must implement a command line switch which allows you to set the quantum. If your executable were called kernel, then, you should be able to do the following:
% kernel -q 4
The -q option takes a single integer which represents the number of times the timer interrupt goes off before the user process is context switched. For example, if this value was set to 4, then a process that has just placed on the run queue will have its quantum set to 4. After the fourth occurrence of the timer interrupt, the process will be preempted, and a new process scheduled to run. This new process will also be given a full quantum of 4 units. The only time a process does not complete its full quantum is if it is blocked or terminates. If the -q option is not there, use a default value. Make sure to check if the quantum is positive. If not, use the default value.
There is one more function that must be performed in all three circumstances. Note that the old timer ISR is in charge of maintaining the TOD clock, the floppy disk drive, as well as other duties. To keep this functionality, you can make a direct "call" to the old ISR by simply executing OldtimerISR(). The Borland compiler is smart enough to generate code to simulate an INT instruction, while bypassing the CPU's interrupt architecture, i.e., it translates a call to OldtimerISR() to:
OldtimerISR(); ---> PUSHF ; push flags CALLF oldtimerISR ; make call
Since the old timer ISR sends the external interrupt controller the EOF signal ("outp(0x20, 0x20)"), you should not do this in your timer ISR. You can make a call to OldtimerISR() as the first statement in your timer interrupt handler. If so desired, the same direct calling technique can be used to interface with Yield_process() from the timer ISR.
Each PCB should now have a new field that describes the current execution mode (either USER mode or KERNEL mode). When System_service is entered, save the mode, then set it to KERNEL. Restore the saved mode on exiting. System_service now looks like this:
save_mode = run->mode;
run->mode = KERNEL;
switch(type) {
...
}
run->mode = save_mode;
if (preempt && run->mode == USER){
preempt = FALSE;
geninterrupt(0x63); /* To Yield_process */
}
return;
One way to compare scheduling algorithms is to see how long it takes a process to complete from the time of creation to the termination of the process. You will investigate these differences by implementing a system call, Get_time_of_day() which is modelled after the UNIX system call, gettimeofday().
Get_time_of_day()
will return the value of a global variable called Ticks.
Ticks
should be initialized to 0 in main().
Each time the timer interrupt goes off, increment Ticks.
You can use this system call to determine how long a process has
run in terms of ticks. This can be accomplished by calling
Get_time_of_day()once
at the beginning of the process (in the user code) and once at
the end. You can calculate how long the process took to run, as
well as when the process first got scheduled (based on ticks).
Notice that there is no attempt to remove time spent by other
processes when calculating how much time is used by a process.
For example, if process A context switches out,
then process B runs, process B's quantum will be included in the
amount of time used by process A. This is known as "wall clock"
time. It is possible to only count the amount of time the process
spends in the run queue and ignore the time used by other processes.
However, you will not do this.
There are many scheduling algorithms, each exhibiting its own
behavior. You will implement a FIFO scheduler (the same
as in project 2, but includes preemption)
as well as a multilevel feedback scheduler. In the implementation
of a FIFO scheduler, you will implement the ready queue as a
single FIFO queue (as in project 2). For the multi-level feedback
scheduler, you will use four FIFO queues instead of one to implement
a ready queue. Each queue in a multilevel feedback scheduler is
assigned a priority level. The queues will be numbered 0 through 3,
with 0 being the highest priority, and 3 being the lowest.
A newly created process's PCB will be placed on the ready queue
of highest priority (i.e., 0). If a process remains on the run
queue for the full quantum, then when it is slated to be placed
back on the ready queue, it will be placed on the next lowest
priority queue (1, if the process was new).
Each time a process completes a full quantum, it will be placed
on the ready queue at the next lowest priority until it is at
priority 3, at which point it can not go any lower. Hence, CPU
intensive processes will be eventually placed on the lowest priority
queue. If the process is blocked, record the priority level prior
to blocking. When this process becomes unblocked, you will
place it on the ready queue with this priority number.
To schedule a new PCB to run, look at the head of the highest
priority queue. If there is a PCB there, place it on the run queue.
If not, go to the next lowest priority queue, and keep repeating
until you find a PCB. The scheduler always picks the PCB with the
highest priority to run next. This may mean low priority
processes are starved.
You must implement a command line argument for choosing between
multilevel feedback versus FIFO. Use a -f
command line option to indicate FIFO, and a -m
command line option to implement the multilevel feedback. FIFO
should be used if neither of these switches are specified.
The choice between which scheduler to use should be made within
the function Scheduler()
using some sort of if
statement. Any function that calls the Scheduler()
should be unaware which scheduling algorithm is being used (i.e.,
do not pass the scheduling type as an argument). It should only
be aware that some PCB from is being placed from the ready queue
to the run queue, and started up. The scheduler, however, will
obviously need to know which scheduling algorithm is being used
(as well as other functions moving the PCB into and out of
the ready queue). It will determine the scheduling algorithm
by referring to a global variable. This variable, which you
define, should be set once in main() based on
command line arguments (or a default value if no scheduling
algorithm is specified).
Your Get_char()
routine should now be implemented as a system call. (This should
be done as before, where Get_char()
activates System_service,
etc.)
When a process attempts to read a character from the character
queue, and the queue is empty, you should block the process via
Ksleep().
Likewise, when the keyboard ISR sees that the queue is empty,
and then puts a character in, it does a corresponding Kwakeup().
These calls are made by the kernel and keyboard handler. Note
P()
and V()
are not called.
You will add the following system calls to your kernel:
Blocking and unblocking by semaphore operations will be implmented
in System-service via Ksleep()
and Kwakeup().
Create_semaphore(name,ival)
is a request by the current process to use a semaphore. A
process can not call P() or V() unless it calls
Create_semaphore().
Think of it as a constructor. The user gives a name for the
semaphore, as well as the semaphore's initial value. It will
get back a semaphore ID, an integer between 0 and N-1. You should
be able to handle at least 20 semaphores. If there are no semaphores
left (i.e., there were N semaphores with unique names already
given), a negative number can be returned indicating an error.
In System_service(),
you will check if another process has made this system call with
the same name. If so, you must return back the semaphore ID
(SID) associated with this name. ival
is ignored in this case. The SID value returned will allow the
user process to tell the kernel which semaphore it wants to use.
You will also add this SID to the list of semaphores the current
process can use, as well increment the count of registered users
which are permitted to use the semaphore.
If this is the first time Create_semaphore()
has been called by the name passed in, then find an unused SID,
and initialize the value of the semaphore variable to ival.
Again, add the SID to the list of semaphores that the current
process can use, as well as incrementing the semaphore's count
of registered users.
Whenever a user process calls P()
or V(),
the kernel will check if the user has permission to make this
call. It will do so by checking if the process has the SID in
its list of SIDs that it can access (which is why you needed to
create such a list). If it is there, it will be allowed to execute
P()
or V().
If not, the kernel should return back a negative value.
You will want to implement the semaphores as described in the
text (Silberschatz and Galvin) with two exceptions. First,
unlike the text, the integer passed to P() or V() is NOT
a semaphore variable. It is a semaphore ID. The kernel will
be able to associate the semaphore ID with the semaphore's value.
That way, you aren't permitted to look at the value of the variable.
Second, you should decrement the value after you make the test,
not before. This way, the semaphore value never goes negative.
The V()system call should use Kwakeup()
as part of its implementation. The semantics of V, as defined
in your textbook, does not say anything about which process is
woken up. All it says is that the value of the semaphore variable
is incremented by 1.
The implementation of V()
can be done in one of several ways. In all variations, the value
of the semaphore variable is incremented by 1. You will implement
a version of V() which only wakes up the PCB at the head of the
semaphore queue. (To wake up a process means to place its PCB
in the ready queue). Alternatively, one could wake up an arbitrary
PCB in the queue, or even place all the PCBs from the semaphore
queue to the ready queue. You should find out why no problems
occur when all PCBs are placed on the ready queue (all but one
should go to sleep again). This version is somewhat inefficient
but allows the scheduler to decide which process to wake up.
You will not implement this version.
A new field will be added to your PCB; that of a semaphore list.
This list will contain all SIDs returned from a Create_semaphore()
called by that process. When a process terminates, you will need
to free up the semaphores it created. You can write a function
called Free_semaphores() to
handle this. You will need to update the number of registered
users for the semaphores freed. If this number drops to 0, then
you can add the SID to the list of free SIDs that can be used.
When the scheduler picks a new process to from the ready queue
to place on the run queue, it will initialize the quantum, and
then call Dispatch(). The scheduler will base the choice of
scheduling algorithm on a global variable indicating whether it
should use a FIFO scheme or multi-level feedback scheme.
If there is no process to execute and processes are still blocked,
then the kernel will switch to a special stack and halt (normally,
interrupts are handled on the stack of the currently running process,
but if there isn't one, there's no stack either). When an interrupt
breaks the CPU out of the halt, the scheduler checks the ready
queue and schedules a process if there is one. This is how processes
that are awakened in an interrupt service routine get to run.
Normally, the kernel operates with interrupts enabled. In previous
projects, you have left the interrupts disabled, but this means
decreased opportunities for concurrency in the kernel. There is
no need to disable interrupts for mutual exclusion with other
processes executing in kernel mode, since kernel mode cannot be
preempted.
However, interrupts need to be turned off when a system call (such
as Get_char())
manipulates the same data structures as its corresponding ISR
(e.g., Key_handler()).
Interrupts should be turned off when global kernel data structures
are manipulated (in Ksleep()
and Kwakeup(),
for example).
Interrupts also need to be turned off when context switching and
when switching stacks (such as in the scheduler). Interrupts need
to be on when the scheduler halts, since that's the only way to
get out of the halted condition (e.g., the timer interrupt can
start up the CPU again).
The end-of-interrupt signal should be sent to the PIC (port 0x20)
when the current interrupt condition is cleared. (Again, the old
interrupt handler, which yours should call, does this.)
As with project 2, all files necessary to run your project must
be located a directory labeled by the project number (say, P3)
of your 3.5" diskette. The only required filenames are proc.c,
proc2.c, and makefile. Note: your new processes need to use P()
and V() to insure that printed output is legible. With timer-driven
scheduling, you never know when a process will be switched. If
this occurs in the middle of a "Cprintf" invocation,
your output may look like garbage.
Also, run several tests on proc2.c, varying the quantum length,
as well as the two scheduling algorithms. Provide a hardcopy
write-up listing the results, as well as explaining why the results
occurred. The exercise is meant to let you consider the effects
of quantum length and scheduling algorithms on the run of several
processes.
You should run the tests with quantum values of 1, 5, 10, and 100, at
the very least, and if you have time, run it with times of 25,
50, and 75, as well. Use both algorithms. This should give
you 14 tests to run.
Implementing
Multilevel Feedback
Keyboard
Handler
Semaphores
int Create_semaphore(char *name, int ival)
int P (int s)
int V (int s)
PCB and Proc_term Modification
Scheduler
while (no process on ready queue) asm HLT;
schedule a process
Information You Should
Have Learned By Now -- Interrupts
What to turn in