CMSC 412 Programming Assignment #2

Due March 18, 2002 (10:00 AM)

Introduction

The purpose of this project is to explore scheduling algorithms and learn about inter-process synchronization via semaphores.

Multilevel Feedback Scheduling

There are many scheduling algorithms. In this project, you will augment the existing GeekOS  Round-Robin scheduling algorithm with a multilevel feedback scheduler. In  Round-Robin, all threads (really their process control blocks) sit in a FIFO queue. In a multi-level feedback scheduler, you will use 4 queues instead of 1. Each queue is assigned a priority level. The queues will be numbered 0 through 3, with 0 being the highest priority, and 3 being the lowest. This will require changing s_runQueue from being a struct to being an array of structs one for each priotiy level.

 

A newly created process's PCB will be placed on the ready queue of highest priority (i.e., 0). If the process runs for the full quantum, then it will be placed on the next lowest priority (1, if the process was new). Each time a process completes a full quantum, it will be placed on the ready queue with 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, the priority level will increase by one level until after blocking three quanta in a row it will be back to priority 0. 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. Scheduling always attempts to look at the highest priority queue and work down. This may mean low priority processes are starved.  

 

The choice of which scheduler to use should be made within the function Get_Next_Runnable(). Any function that calls the Get_Next_Runnable()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 has been selected.

 

You will need to handle the case of the Idle thread specially.  It should be placed in the lowest level of scheduling priority and should never be permitted to move out of that level.

 

Your operating system should be able to switch which scheduling algorithm is being used via a system call.  The system call int Set_Scheduling_Policy(int policy, int quantum) should be implemented. If the value of policy is 0, the system should switch to round robin scheduling, if the policy is 1, the system should switch to multi-level feedback. Other values of this parameter should result in an error code being returned (i.e. a non-zero return value). The value of the quantum parameter should be the number of ticks that a user process may run before getting removed from the processor. To implement the tunable quantum, you should change the constant MAX_TICKS in timer.c to be a global variable (whose default value is MAX_TICKS) that is set by this system call. The system call should be call #7.

Semaphores

You will add the following system calls to your kernel:

 

       int Create_Semaphore(char *name, int ival)

   int P(int s)

   int V(int s)

   int Destroy_Semaphore(int s)

  

 

The system call numbers for these system calls should be 9-12 respectively. 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 whose names may be up to 25 characters long. If there are no semaphores left (i.e., there were 20 semaphores with unique names already given), a negative number can be returned indicating an error.

 

In your kernel function Sys_Create_Semaphore, 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. The parameter 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. 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.  When waiting on a semaphore operation, the process may not use a busy wait. To block a process, you can use the Wait call in the kernel.  You should create a new thread queue for processes blocked on semaphore operations.   To wakeup up processes waiting on a given queue, you can use the Wakeup function.

 

Destroy_Semaphore(int s)will delete the passed semaphore.  It will keep track of how many processes have references to this semaphore, and delete the semaphore from the table when the last process that can access this semaphore calls Destroy_Semaphore().

Paramters to new processes

You will also need to extend the implementation of spawn to allow passing parameters to new processes. The strings you pass to the spawn system call will contain not only the name of the program, but also the parameters (like a UNIX command line).  The implementation of spawn in the kernel will need to convert this string into an array of each individual parameter. Parameters are separated by one or more spaces. The results should be two items argc and argv. Argc will contain the number of elements in the argv array.  The argv array will contain an array of character pointers.  The spawn system call should cause these parameters to be passed to the Main function of the spawned process.  This will require using copying these values into the memory of the newly created thread. The easiest place to put these values is at the end of the memory of the user process.  You can then decrease the stack pointer by one page so that it doesn’t overwrite the arguments.  Finally, your _Entry routine will need to cause argc and argv to be passed the Main of your user process.

Timing

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() (system call #8).

 

Get_Time_Of_Day() will return the value of the kernel global variable g_numTicks. The variable is already implemented in the kernel, you only need to implement the system call to read it. You can use this system call to determine how much time has elapsed between two events. You can do this 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. For example, if your process context switches out, then runs a second process, the second process’s time during the context switch will be included in the first process’s total time. This is known as "wall clock" time. One can also just calculate the time used by the process itself.  This time is called process time (or sometimes virtual time). GeekOS currently calculates this time, but you do not need to use this information in this project.

What to turn in

You should submit a compressed tar file for the project2 directory.  Also, include the file RESULTS described below. Don’t forget to run gmake clean before submitting your directory.

 

In addition to the code, you should run several tests on the supplied application workload.exe, varying the quantum length as well as the two scheduling algorithms. At minimum try running the system with the inputs of:

/c/workload.exe rr 1

/c/workload.exe rr 100

/c/workload.exe mlf 1

/c/workload.exe mlf 100

 

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.

 

Files

            Print.c            - improved print routines for to be compiled into libuser.a

            Workload.c, long.c, ping.c, pong.c – test programs