Chapter 8. Project 3: Scheduling

Table of Contents

1. Introduction
2. Multilevel Feedback Scheduling
3. Semaphores
4. Timing
5. Background Processes
6. Evaluating the Scheduling Policies

1. Introduction

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

2. 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 Kernel_Thread structures) 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 (in src/geekos/kthread.c) from being a struct to being an array of structs; one for each priority level.

A newly created process's Kernel_Thread structure 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 Kernel_Thread to run, look at the head of the highest priority queue. If there is a Kernel_Thread there, place it on the run queue. If not, go to the next lowest priority queue, and keep repeating until you find a Kernel_Thread. 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 Kernel_Thread 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 To make the scheduling policy configurable, you should implement the Sys_SetSchedulingPolicy() system call in src/geekos/syscall.c. This system call will take two parameters, policy (passed in state->ebx) and quantum (passed in state->ecx). 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.

3. Semaphores

You will add the following system calls to your kernel (all defined in src/geekos/syscall.c):

  • Sys_CreateSemaphore()
  • Sys_P()
  • Sys_V()
  • Sys_DestroySemaphore()

Sys_CreateSemaphore() creates a semaphore. The user address and length of the string containing the semaphore name are stored in state->ebx and state->ecx, respectively. 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. If the name corresponds to a semaphore that already exists, your function should return its ID. Otherwise, it should create a new semaphore, using the value in state->edx as the initial count value for the semaphore. In either case, you should add semaphore id (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.

The P() and V() operations acquire and release the semaphore, respectively. P() waits until the semaphore's count is greater than 0, decrements the count by 1, and then proceeds. V() increments the semaphore count by 1, and should wake up a thread that is waiting to acquire the semaphore, if any. When Sys_P() and Sys_V() are called, the kernel will check if the process 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.

Sys_DestroySemaphore() 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(). Note that you should ensure that when processes exit, they release their references to any semaphores they have access to, even if they don't explicitly call Destroy_Semaphore().

4. 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, Sys_GetTimeOfDay().

Sys_GetTimeOfDay() will return the value of the kernel global variable g_numTicks. The variable is already implemented in the kernel, so 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.

5. Background Processes

The testing of semaphores and the testing of scheduling algorithms both require concurrent processes. These concurrent processes may be cooperating through the semephores and they may be competing for scheduling on the CPU. So the addition of semaphores and the MLF scheduling necessitates a mechanism for easily creating concurrent processes. We choose to implement such a mechanism from the shell program, but other programs could be constructed to also generate concurrent processes.

The shell mechanism to allow concurrency is known as creating a "background" process. In the shell provided, whenever a new process is successfully created through the Spawn_Program() system call, the shell waits for the newly launched process to terminate by calling the Wait() system call. You must add functionality to the shell to allow it to continue without the Wait(). In particular, in a manner similar to UNIX, we want to allow the user to include a '&' character at the end of the command line and, if the shell detects this character at the end of the line, the subsequent Wait() is omitted. Otherwise the Wait() is executed as usual.

Note that this '&' indicating a request for a background process is information between the user and the shell. It should not be part of the command that is passed to Spawn_Program().

6. Evaluating the Scheduling Policies

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

You should investigate and be able to explain 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.