OS161 Assignment 2
Assignment2
Use this document as a guideline for writing high-quality code.
In this assignment you finally get to write your own code in OS161! By the end of this assignment you will:
- Have a good understanding of the implementation of spinlocks and semaphores in OS161
- Implement locks
- Implement condition variables.
Step 1. Prepare
Make sure you don't have any uncommitted updates in your repo. Now, tag your repository as shown here:
1 |
|
Now push that new tag:
1 |
|
Make a directory submit/asst2 in your os161 tree. You will put your file with the answers to code reading questions in that directory.
- Physical memory and CPUs
Change sys161.conf
file in ~/os161/root
:
1 |
|
- Built-in thread tests
The thread test code uses the semaphore synchronization primitive
You should trace the execution of one of these thread tests in GDB to see how the scheduler acts, how threads are created, and what exactly happens in a context switch. You should be able to step through a call to thread_switch()
and see exactly where the current thread changes.
- Debugging concurrent programs
thread_yield()
is automatically called for you at intervals that vary randomly.
Change sys161.conf
file in ~/os161/root
:
1 |
|
Once you are done with initial debugging/testing, remember to set the random device back to autoseed
.
This should allow you to test your solutions under varying conditions and may expose scenarios that you had not anticipated, which is central to effective testing.
Step 2. Code reading exercises
- What happens to a thread when it exits (i.e., calls thread_exit())? What about when it sleeps?
thread_exit()
detaches the thread from the process, checks the stack guard band, turns interrupts off on this processor and performs a thread switch(find another thread to run).
thread.c
777:
1 |
|
First, wchan_sleep
makes sure that we are not in an interrupt handler, hold the specific spinlock associated with the wait channel WC and no others. Then, it performs a thread switch, and puts the thread go to sleep on the specified wait channel WC before finding another thread to run. Lastly, when being woken up, it locks the spinlock again before returning.
thread.c
1026:
1 |
|
- What function(s) handle(s) a context switch?
thread.c
560:
thread_switch()
Machine-dependent functions for working on switchframes:
During context switch, switch what is saved on the kernel stack.
switchframe_switch()
- What does it mean for a thread to be in each of the possible thread states?
During context switching, we need to put the thread in the right place according to its newstate
.
-
S_RUN:
For
S_RUN
, it means the thread is currently running. A thread should never be put into theS_RUN
state withinthread_switch
. But if it does, call panic. -
S_READY: A thread in the
S_READY
state is ready to run but is not currently running.thread_make_runnable
is called to add it to the run queue so that the scheduler can pick it up for execution later. -
S_SLEEP: A thread in the
S_SLEEP
state is blocked, waiting to be woken up. The thread is added to the wait channelwc
, and it will remain in this state until a wakeup call (likewchan_wake
) signals it. After putting the thread on the list, we release the spinlock associated with the wait channelwc
. -
S_ZOMBIE: A thread in the
S_ZOMBIE
state has finished its execution but has not yet been fully cleaned up. The thread is added to the CPU's zombie listc_zombies
, where it waits to be freed.
thread.c
: thread_switch()
: 595:
1 |
|
- What does it mean to turn interrupts off? How is this accomplished? Why is it important to turn off interrupts in the thread subsystem code?
Turning interrupts off means that the current processor is temporarily prevented from responding to hardware interrupts, meaning that the current CPU cannot perform a context switch.
In os161, turning interrupts off is accomplished by changing the processor's interrupt priority level(IPL). The function splhigh()
is used to set the IPL to its highest level to disable all interrupts.
1 |
|
It is important to turn off interrupts in the thread subsystem code. When we perform operations on threads such as context switching, blocking threads, exit threads, we don't want to be interrupted. If we are interrupted during those operations, we may lose important information of the thread. For example, if we are interrupted during context switching when we are saving the thread's state, we may lose the state during interrupts. Turning off interrupts also helps avoid deadlock and improve performance.
- What happens when a thread wakes up another thread? How does a sleeping thread get to run again?
When a thread wants to wake up another thread, it calls the function wchan_wakeone
, the function grabs a thread from the wait channel and then calls thread_make_runnable
to make the target thread runnable.
In thread_make_runnable
, the target thread should lock the run queue of the CPU, ensuring only one thread in the run queue can modify the CPU. Then it puts the target thread on the run queue of the CPU.
- What function(s) choose(s) the next thread to run?
thread_switch
- How does it (do they) pick the next thread?
schedule
and thread_switch
both use threadlist_remhead
function, which removes and returns the first thread from the run queue of the current CPU, which is the next thread to run.
- What role does the hardware timer play in scheduling? What hardware independent function is called on a timer interrupt?
The hardware timer generates periodic interrupts to allow the operating system to regain control of the CPU at regular intervals, ensuring that it can check the status of threads and make scheduling decisions.
hardclock
is the hardware independent function called on a timer interrupt.
- Describe how
wchan_sleep()
andwchan_wakeone()
are used to implement semaphores.
For function V
, wchan_wakeone()
is called after it increments sem_count
and wants to wake up another thread.
For function P
, wchan_sleep
is called when sem_count = 0
and wants to put the thread to sleep waiting to be awakened. It should be called after we lock the wait channel(using semaphore spinlock) so that we can put the thread to sleep while also release the wait channel lock afterwards. After being woken up, it locks the wait channel again before returning.
1 |
|
- How does the implementation of
wchan
ensure that a thread never misses a wakeup signal: that another thread cannot attempt to awaken the first thread just as it is preparing to sleep, but before it is actually placed into the sleep queue?
In P
, wchan_sleep
is called after we acquire the semaphore spinlock(spinlock_acquire(&sem->sem_lock)
), which also serves as locking the wait channel. In this way, when we call wchan_sleep
to put the current thread to sleep, no other threads can manipulate the wait channel(i.e. by calling wchan_wakeone
). That is to say, no threads can sneak in and try to awaken the thread during its "sleep" process.
1 |
|
Step 3. Implement locks for OS161
The interface for the lock struct is defined in kern/include/synch.h
. Stub code is provided in kern/thread/synch.c
.
Requirements:
- Comply to the interface. Take a look at the stub code provided in
kern/thread/synch.c
. Your implementation must conform to this API. Please don't change it, as this will break the tests that we will use to test your assignment. - Mutual exclusion. It's probably a no-brainer that your locks must provide mutual exclusion. You are going to implement the simplest semantics of locks: only one thread can hold a lock at any given time.
- No busy-waiting. If you carefully read the OS161 code, you noticed that there is an implementation of spinlocks. Spinlocks derive their name from the method that they use to wait until the lock becomes available: they spin, or busy-wait, hogging the CPU and checking if the lock became available. While this is perfectly fine for short critical section and only on a multiprocessor system (why?), a general implementation of locks must be more universal. We must be able to use them on a system with a single CPU and for long critical sections, e.g., those involving I/O operations, where it makes no sense to busy-wait on the CPU. Therefore, your locks implementation must not busy-wait on a taken lock. It must give up the CPU until the lock becomes available.
Implementation
FAQs
How to avoid busy-waiting?
When a thread tries to acquire the lock, if the lock is not avaiable, we puts the thread to sleep. After the thread holding the lock releases the lock, it wakes up another thread waiting on the wait channel.
How to protect the lock and the wait channel?
We use the spinlock provided by the os161 stub code to protect both the lock and the wait channel.
Code
lock structure:
1 |
|
lock methods:
1 |
|
Methods Implementation:
1 |
|
test sy2
passes!

Step 4. Implement condition variables for OS161
Requirements:
Just like with locks, your solution must:
- Comply to the existing interface.
- Implement proper cv semantics as explained here.
- Not use busy-waiting to wait on cv.
Implementation
FAQs
How to protect the wait channel?
Again, we use the spinlock provided by the os161 stub code to protect the wait channel.
What do we do in
wait()
?
First, we check that we hold the lock.
Then, release the lock and go to sleep, protected by the spinlock.
The sleep()
function releases the spinlock and atomatically re-acquires the spinlock before returning.
Therefore, after sleep
returns, we need to release the spinlock explicitly and re-acquire the lock before returning.
Do we need to release the lock in
signal()
after we wake up another thread.
No. Users who use the cv should explicitly releases the lock after they call signal
. It is not signal()
's responsibility to do so. So is the broadcast()
function.
Code
CV structure:
1 |
|
CV methods:
1 |
|
Methods Implementation:
1 |
|
Test sy3
and sy4
pass!


We have finished Assignment2!😊