You are expected to understand this. CS 111 Operating Systems Principles, Fall 2005

WeensyOS Minilab 2

Extra Credit Only

This second of the Weensy OS problem sets introduces you to a slightly more advanced, yet still weensy, operating system. WeensyOS 1 introduced you to boot loading, different methods of hardware interaction, and the need for protection. WeensyOS 2 shows off process descriptors, scheduling, and synchronization, and has a true kernel!

weensyos2.tar.gz     Source code for WeensyOS 2.0, the Scheduler OS

Handing in

You will electronically hand in code and a small writeup containing answers to the numbered exercises. The problem set code, weensyos2.tar.gz, unpacks into a directory called weensyos2. (We explain how to unpack it below.) You'll modify the code in this directory, and add a text file with your answers to the numbered exercises. When you're done, run the command gmake tarball. This should create a file named weensyos2-yourusername.tar.gz, which you will submit to CourseWeb.

Answers to the numbered exercises should be in a file named answers.txt, answers.html, or answers.pdf. Text files are strongly preferred. No Microsoft Word documents (or other binary format, except for PDF) will be accepted! For coding exercises, it's OK for answers.txt to just refer to your code (as long as you comment your code).

To review:

  1. Download and unpack weensyos2.tar.gz.
  2. Do your work in the weensyos2 directory.
  3. Put your answers to the numbered exercises in a answers.txt file (or answers.html or answers.pdf) in that weensyos2 directory.
  4. When you're done, run gmake tarball from the weensyos2 directory. This will create a file named weensyos2-yourusername.tar.gz.
  5. Submit that weensyos2-yourusername.tar.gz file to CourseWeb.

Part 1: Scheduling

First, you must set up your machine to compile and run WeensyOSes. We have set up the Linux Lab and SEASnet Solaris machines already, but you can also set up a home Linux machine. See the minilab tools page for instructions. You can use the setup that worked for you for WeensyOS 1, if you did WeensyOS 1.

Download and unpack the source for weensyos2.

% gtar xzf weensyos2.tar.gz
% ls weensyos2
COPYRIGHT               schedos-2.c             schedos-trap.S
GNUmakefile             schedos-3.c             schedos-x86.c
bootstart.S             schedos-4.c             schedos.h
conf                    schedos-app.h           types.h
elf.h                   schedos-boot.c          x86.h
mergedep.pl             schedos-kern.c          x86struct.h
mkbootdisk.c            schedos-kern.h          x86sync.h
schedos-1.c             schedos-symbols.ld
%

Change into the weensyos2 directory and run gmake run-schedos.

This will build and run the single operating system you'll use in WeensyOS 2, the "scheduler OS" or SchedOS. As before, this will start up Bochs, but not the emulated computer. To start the emulated computer, type "c" at the <bochs:1> prompt. After a moment you should see a window like this:

[SchedOS 1]

The SchedOS consists of a kernel and four user processes, or "applications". The processes are extremely simple: the schedos-1 process prints 320 red "1"s, the schedos-2 process prints 320 green "2"s, and so forth. Each process yields control to the kernel after each character, so that the kernel can choose another process to run. After printing all 320 characters, each process exits. The four processes coordinate their printing with a shared variable, cursorpos, located at memory address 0x190000. The kernel initializes cursorpos to point at address 0xB8000, the start of CGA console memory. Processes write their characters into *cursorpos, and then increment cursorpos to the next position.

Read and understand the SchedOS process code. Specifically, read and understand schedos-1.c.

The SchedOS has a real kernel: a privileged piece of code that arbitrates between the machine's user processes. (The SchedOS kernel does not isolate processes, but that's OK for now.) Once we have a kernel, we need a way for applications to transfer control into kernel space. The way we do it on x86, and most modern architectures, is with a trap: a special instruction that makes the processor save its state and switch into kernel mode. When the user executes a trap instruction, the processor:

  1. Looks up the trap in an interrupt descriptor table. This tells the processor where to jump (the new instruction pointer), what mode to run (here, kernel mode), and where to save the old processor state (that is, the new stack).
  2. Saves the old processor state onto the new stack.
  3. Switches to the correct mode.
  4. Sets the stack pointer and instruction pointer based on the interrupt descriptor table.
  5. Continues execution, this time in kernel mode.

To sum up, when the user executes a trap instruction, the processor switches into kernel mode and starts running kernel code. This is called a kernel crossing.

Read and understand the comments in schedos-app.h. Don't worry too much about the asm statements; just understand them at a high level.

The kernel's job is very simple. At boot time, it initializes the hardware, initializes a process descriptor for each process, and runs the first process. At that point it loses control of the machine until a system call or interrupt occurs. System calls and interrupts restart the kernel by effectively calling the trap function. Note that this simple kernel has no persistent stack: every time a system call occurs, the kernel stack starts over again from the very top, and any previous stack information is thrown away. Thus, all persistent kernel data must be stored in global variables.

Read and understand the following pieces of kernel code. Again, don't worry about every last detail; just get a feeling for the high-level structure and purpose of each function.
  1. The process descriptor structure process_t defined in schedos-kern.h. This refers to the registers_t structure defined in x86struct.h.
  2. The comments at the top of schedos-kern.c.
  3. The start function from schedos-kern.c, which initializes the kernel.
  4. The trap function from schedos-kern.c, which handles all interrupts and system call traps.

SchedOS supports two system calls, sys_yield and sys_exit. The sys_yield call yields control to another process, and sys_exit exits the current process, marking it as nonrunnable. The kernel implementations of these system calls (in trap()) both call the schedule function. This function is SchedOS's scheduler: it chooses a process from the current set of runnable processes, then runs it. In the first part of this problem set, you'll focus on this function, and SchedOS's scheduling algorithms.

Read and understand the schedule function from schedos-kern.c.

Exercise 1. What scheduling algorithm does schedule() currently implement? (What is scheduling_algorithm 0?)

Exercise 2. Add code to schedule() so that scheduling_algorithm 1 implements strict priority scheduling, with the priority order schedos-1 > schedos-2 > schedos-3 > schedos-4. Thus, the first process, schedos-1, will run until it exits; then schedos-2 will run until it exits; and so forth. You will also need to change schedos-1.c so that the schedos processes actually exit via sys_exit(), instead of just yielding forever. Test your code.

Exercise 3. Compare scheduling_algorithms 0 and 1 in terms of the average turnaround time and average response time across all four jobs. Assume that printing 1 character takes 1 millisecond.

Now complete at least one of Exercises 4A and 4B.

Exercise 4A. Change scheduling_algorithm 1 so that the priority order is defined by a separate p_priority field of the process descriptor, rather than simply process ID. To be really cool, implement a system call that lets processes set their own priority. Make sure you correctly handle the case when more than one process has the same priority.

Exercise 4B. Add another scheduling algorithm, scheduling_algorithm 2, that implements proportional-share scheduling. In proportional-share scheduling, each process gets a share of the CPU proportional to its priority. For example, say schedos-1 has priority 1 and schedos-4 has priority 4. Under proportional-share scheduling, schedos-4 will run 4 times as often as schedos-1 (at least until it exits); so we might expect to see output like "441444414444144...". (Note that this is different from the priority order in Exercise 2.)

Part 2: Synchronization

In this section of the problem set, you'll investigate synchronization issues. But synchronization isn't interesting without concurrency, and right now, our operating system is cooperatively multithreaded: each application decides when to give up control. We introduce concurrency by turning on clock interrupts and introducing preemptive multithreading. When a clock interrupt happens, the CPU will stop the currently-running process -- no matter where it is -- and transfer control to the kernel. This indicates that the current process's time quantum has expired, so the kernel will switch to another process. However, note that clock interrupts will never affect the kernel: this simple kernel runs with interrupts entirely disabled. Interrupts can only happen in user level processes.

Change scheduling_algorithm back to 0. Then change the interrupt_controller_init(0) call in schedos-kern.c to interrupt_controller_init(1). This turns on clock interrupts.

After running gmake run-schedos, you should see a window like this:

[SchedOS 2]

Exercise 5. Explain what has happened. Why does this output look different from the output without clock interrupts? Be specific (talk about particular lines of process code).

But we're not done! Let's cause clock interrupts to happen a little bit more frequently.

The HZ constant in schedos-kern.h equals the number of times per second that the clock interrupts the processor. It is set to 100 by default, meaning the clock interrupts the processor once every 10 milliseconds. Set this constant to 1000, so that the clock interrupts the processor every milliscond.

After running gmake run-schedos, you should see a window like this:

[SchedOS 3]

Note that the output has less than 320 * 4 characters! Clearly there is a race condition somewhere!

Exercise 6. Explain what has happened. Why does this output look different from the earlier two? Be specific (talk about particular lines of process code, and why the higher clock rate made a difference). Where is the race condition?

Exercise 7. Implement a synchronization mechanism that fixes this race condition.

There are lots of ways to implement the synchronization mechanism; here are a couple.

However, you must not turn off clock interrupts. That would be cheating.

(Hint: You may need to use typecasts to get the x86sync.h atomic operations to work. And note that cursorpos points to a 16-bit integer, so the C statement cursorpos++; actually increments the address stored in cursorpos by 2 bytes, not one.)

This completes the problem set.