Lab 8: Priority Scheduling
The goal of this lab will be to introduce priority scheduling to our OS. This will allow processes to run at different priority levels, giving the user more control over how processing resources are allocated.
The Default Scheduler
Take a look at the scheduler() function in kernel/proc.c to familiarize yourself with how scheduling currently works. The basic algorithm we’ll implement will look like this:
- Instead of round robin scheduling, we’ll start by running all the highest priority tasks first
- After the highest priority tasks have run, we’ll move on to the next priority level, which will include the previous level’s tasks as well.
- This approach takes inspiration from multi-level feedback queues, where processes get “demoted” to the next level down after they run. However, we will reset processes to their original priority level after finishing a complete scheduling pass instead of permanently moving them down.
- The process will repeat until we reach the lowest priority
- Finally, the algorithm will start back at the beginning with the highest priority tasks.
It’s important to run your OS with only one CPU core during this exercise. If you run with multiple cores, then it will be a lot harder to reason about whether the scheduling decisions were made correctly or not. To do this, run:
CPUS=1 make qemu
Note the CPUS=1 part – don’t forget it! You can try doing this now, but you probably won’t be able to see much of a difference since you haven’t made any scheduling changes yet.
Tracking Process Priorities and Niceness
You will need to do some prep work before modifying scheduler(). This includes:
- Updating the process control block with a new variable to hold its scheduling priority.
p->lockshould be held while accessing / changing this value. - Adding a system call to set process nice level, which is inversely proportional to its priority. (The nicer a process is, the less CPU it uses).
- Limit the nice level from 0 (highest priority) to 3 (lowest priority). In other words, nice 0 = priority 3, and nice 3 = priority 0. Clamp out-of-bounds values to these limits.
- Creating a
niceutility that runs the specified command with a given nice level. For example, its usage should look like:nice 3 ls /to runlswith the lowest priority. - Creating a test script that will run a variety of processes at different priority levels
- Designing a consistent workload to be used as a test program
Since you have done all of the above before in your previous labs, I will spare you the long, drawn-out details. Can you add a system call purely from memory now? (I mean your own memory, not your VMs!)
Here’s a small utility that does nothing useful, but DOES do something as far as the CPU is concerned, spinner.c:
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if (argc <= 2) {
fprintf(2, "Usage: spinner name amount\n");
exit(1);
}
char *name = argv[1];
int amount = atoi(argv[2]);
int start = uptime();
for (int i = 0; i < amount; ++i) {
printf("%s", name);
for (int j = 0; j < 10000000; ++j) {
// wheee :-)
}
}
int end = uptime();
printf("\n[%s] has finished, %d work in %d ticks.\n",
name, amount, end - start);
exit(0);
}
And a script to run it:
#!/sh
nice 0 spinner A 50 &
nice 1 spinner B 25 &
nice 3 spinner C 50 &
(Glad we added script support to the OS…)
Looking at the priorities and processes listed above, what would you predict will be the outcome? (i.e., what finishes first, second, etc.?) How about with the following?
#!/sh
nice 0 spinner A 100 &
nice 0 spinner B 25 &
nice 1 spinner C 50 &
nice 2 spinner D 50 &
nice 2 spinner E 10 &
nice 3 spinner F 100 &
(We’ll refer to these as spin1.sh and spin2.sh, respectively).
Updating procdump
Once you’ve made the required changes, update procdump() in kernel/proc.c so that it will print the numeric process priorities in addition to the existing information already printed out.
Next, run nice 0 ls / and immediately press CTRL+P to see the process list. Verify that ls is correctly set to 3 for its priority (due to having a niceness of 0).
Updating the Scheduler
Finally, we need to make the scheduler aware of priorities. To do this:
- For each pass of the scheduler, you’ll iterate through the process list four times. Once for priority 3, then 2, 1, and 0.
- At each priority level, run any
RUNNABLEprocesses that have a priority greater or equal to the current priority level. - This approach could result in excessive scans through the process list, so to optimize it, track the maximum observed priority level. If it is less than the next priority step, jump to that priority level immediately.
- E.g., if we are scanning priority level 4, but the largest priority we observe during our first iteration through the process table is priority 0, jump there immediately instead of checking all the processes again at level 3.
You may wonder if this could benefit from a better data structure, maybe a heap. That would indeed be better, if we were dealing with larger process tables. Since we usually have no more than 5 or so processes running at a time, this approach will suffice for now.
Testing
For the most basic test, run two processes with opposite priority levels and observe how long it takes them to complete. Then move on to the spin1.sh and spin2.sh scripts.
And don’t forget:
CPUS=1 make qemu
Grading and Submission
Once you are finished, check your changes into your OS repo. Then have a member of the course staff take a look at your lab to check it.
To receive 75% credit:
- Make the process changes, add the
niceutility, modifyprocdump(), and confirm proper priority setting with CTRL+P
To receive full credit for this lab:
- Complete all previous requirements
- Implement the scheduling algorithm and verify the results are correct with the provided test scripts.