This second of the Weensy OS problem sets introduces you to a slightly more advanced, yet still weensy, operating system. WeensyOS 1.0 introduced you to boot loading, different methods of hardware interaction, and the need for protection. WeensyOS 2.0 shows off some new stuff: process control blocks, scheduling, and synchronization. It is also our first operating system with a true kernel!
weensyos2.tar.gz |
Source code for WeensyOS 2.0, the Scheduler OS |
Please check back over the next couple days as we improve this problem set description.
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
.
Email this file as an attachment to kohler@cs.ucla.edu
.
I won't be able to confirm that I got your problem set, especially if you
hand in at the last minute; so you may want to save a copy in your SEASnet
home directory.
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:
weensyos2.tar.gz
.weensyos2
directory.answers.txt
file (or answers.html
or answers.pdf
) in that weensyos2
directory.gmake tarball
from the weensyos2
directory. This will create a file named weensyos2-yourusername.tar.gz
.weensyos2-yourusername.tar.gz
file as an
attachment to kohler@cs.ucla.edu
.Use whatever setup worked for you for WeensyOS 1, whether that was the
Linux lab, the regular SEASnet machines (with the cross-compiler and Bochs
emulator installed in /u/cs/class/cs111/cbin/bin
), or your own
machine (instructions).
If you are using the regular SEASnet machines, make sure the
/u/cs/class/cs111/cbin/bin
directory is in your
PATH
environment variable:
In bash/sh: $ export PATH=${PATH}:/u/cs/class/cs111/cbin/bin In tcsh/csh: % setenv PATH ${PATH}:/u/cs/class/cs111/cbin/bin
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 %
Keep the weensyos2.tar.gz
file around -- it will be
useful if we release any updates to the code.
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:
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 (our first one!): a privileged piece of code that arbitrates between the machine's user processes. (The SchedOS kernel does not implement protection, 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 kernel:
To sum up, when the user executes a trap instruction, the kernel executes a context switch or kernel crossing and starts running kernel code.
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: it initializes the hardware,
initializes a process control block 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.
pcb_t
defined in
schedos-kern.h
. This refers to the registers_t
structure defined in x86struct.h
.schedos-kern.c
.start
function from schedos-kern.c
, which
initializes the kernel.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
pcb_priority
field of the process control block, 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.)
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:
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:
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.
x86sync.h
directly.x86sync.h
to build a lock
data type, then use lock_acquire
and lock_release
operations.lock_acquire
and
lock_release
operations.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.