OS161 Assignment 3
Assignment3
In this assignment you will solve a synchronization problem using the synchronization primitives in OS161, two of which you have implemented in the previous assignment. You will also learn about having multiple remotes configured for your git clone, and about branching and merging with git.
Step 1. Prepare
Pull some new code required for this assignment:
1 |
|
At this point, git will probably complain about merge conflicts. Follow this tutorial to resolve them before proceeding.
Now, push the new code into your master repository and tag your tree to indicate that you are beginning to work on Assignment 3:
1 |
|
Step 2. Understand the problem
Why we need to solve this synchronization problem?
As you will realize in the future programming assignments, which all directly involve programming kernel services, proper synchronization among threads is essential. You will not get far if you are unable to properly synchronize the code in the scheduler, virtual memory system and the file system. This exercise lets you practice concurrent programming and the use of synchronization primitives in a less challenging environment.
Here is the synchronization problem you will have to solve. This is a well-known and a very serious known as
AirBalloon
.After a war erupts in their kingdom, Princess Marigold must help Prince Dandelion (her younger brother) escape from danger. Marigold places Dandelion in a hot air balloon, which is connected to the ground by NROPES ropes -- each rope is connected to a hook on the balloon as well as a stake in the ground. Marigold and Dandelion must work together to sever all of these ropes so that Dandelion can escape. Marigold unties the ropes from the ground stakes while Dandelion unhooks the ropes from the balloon. To free a rope it must be either severed from the ground or unhooked from the balloon: not both.
Unfortunately, one of Princess Marigold and Prince Dandelion's enemy, Lord
FlowerKiller
, is also at work.FlowerKiller
is rearranging the ropes to thwart Princess Marigold and Prince Dandelion. He will randomly switch ropes attached to two different stakes. This leads to chaos!Without Lord
FlowerKiller's
dastardly behavior, there would be a simple 1:1 correspondence between balloon hooks and ground stakes (each hook corresponds to exactly one stake, and each stake corresponds to exactly one hoop). However, while LordFlowerKiller
may break this symmetry. Say, LordFlowerKiller
decides to switch ropes connected to Stake 1 and Stake 5. Before the switch, the rope connected to Stake 1 on the ground is also connected to Hook 1 on the balloon, and the rope connected to Stake 5 on the ground is also connected to Hook 5 on the balloon. After the switch, however, one rope is connected to Stake 1 on the ground and to Hook 5 on the balloon, and vice versa.As Marigold and Dandelion cut ropes, they must delete mappings, so that they remove all the ropes as efficiently as possible. Each character is represented by a thread. Dandelion selects ropes to sever by generating a random balloon hook index, and Marigold selects ropes by generating a random ground stake index.
Marigold and Dandelion must work as efficiently as possible, so one should not try to disconnect a rope that has already been disconnected. For example, if Dandelion already unhooked a rope, Marigold should see that the rope is hanging loose and not try to unook it from the stake. Similarly, Dandelion should not try to unook a rope that Marigold disconnected on the ground.
Lord
FlowerKiller
is on the ground, so like Marigold, he selects ropes by their ground stake index.Due to recent advances in genetic technology, there is another unfortunate circumstance: Lord
FlowerKiller
figured out how to clone himself, so now there areN_LORD_FLOWERKILLER
copies of of him at work (each is a separate thread)!
problems to avoid in your implementation:
Marigold randomly selects Stake 7, sees that the rope is still attached. She marks the rope as severed. Suppose that the hook corresponding to the severed rope is Hook 11. If Dandelion now randomly selects Hook 11, he must see that the rope is severed and not try to sever it again. What data structures might you use to avoid a race condition in this situation? What would you need to lock to make sure that Marigold and Dandelion don't sever the same rope twice?
Worse yet, Lord FlowerKiller
might be wreaking havoc with the same ropes. For example, imagine that he decides to swap the rope attached to Stake 7 with the rope attached to Stake 4, and vice versa. How do you make sure to avoid race conditions that this situation creates?
Now consider how Lord FlowerKillers
might step on each other toes. Suppose Lord FlowerKiller1 grabs Stake1 and Stake4 to swap the corresponding ropes, and Lord FlowerKiller2 grabs Stake4 and Stake1. How might this cause deadlock? What would you do to prevent it?
In addition to our character threads, there is also a Balloon thread that does not do much. It gets started in the beginning of the program, at the same time as the other threads, and just sits there and waits until all the ropes have been severed. Then it announces that Prince Dandelion escaped and exits.
Step 3. Understand the requirements
Your solution must satisfy these conditions:
- Avoid deadlocks and race conditions
- You may use semaphores, locks and condition variables available in OS161, but do not use spinlocks,
wchans
, or atomic primitives directly, and don't turn on/off the interrupts. - Permit Marigold, Dandelion and Lord
FlowerKiller
threads to operate concurrently (no "big lock" solutions) - The Balloon thread exits after all ropes have been severed
- The Main thread (the one that starts all other threads) exits after all threads are done.
- You must deallocate all the memory allocated by your code before all threads exit. If you don't, there will be a memory leak! We will run your program multiple times to make sure your kernel does not leak.
- Marigold must access the ropes via stakes. She does not have access to hooks. Programmatically, this means that you need to have some kind of data structure representing stakes, which Marigold indexes. Marigold can update stakes and ropes (that are connected to stakes), but she may not update hooks. Similarly,
LordFlowerkillers
may access ropes via stakes only. Dandelion, on the other hand, may access ropes only via hooks. He cannot see or access stakes.
What your code should print?
-
Every time Marigold severs the rope, the Marigold thread prints: "Marigold severed rope N from stake K", where N is the index of the severed rope, which can range from 0 to NROPES - 1, and K is the index of the stake to which the rope was attached. K also ranges from 0 to NROPES-1. Be sure to print the statement exactly as shown, with no extra characters. The print statement must appear on its own line.
-
Every time Dandelion severs the rope, the Dandelion thread prints: "Dandelion severed rope N", where N is the index of the severed rope, which can range from 0 to NROPES - 1. Be sure to print the statement exactly as shown, with no extra characters. The print statement must appear on its own line.
-
Lord
FlowerKiller
prints the following statement every time he switches a rope: "LordFlowerKiller
switched rope N from stake K to stake P". N, K and P are corresponding rope and stake indices. If he switches two ropes, which is exactly what he will do every time, he must print two statements: one for each rope. -
There must be a print statement for every rope indicating that it was severed by either Dandelion or Marigold.
-
Each rope must be severed exactly one: only one "severed" print statement for each rope.
-
To randomly select a stake to unhook (or two stakes to swap) or a hook, use
random()
function in OS161. -
Every time a character succeeded unhooking one rope or switching one pair of ropes, the corresponding thread must yield (call thread_yield()).
-
Marigold's print statements must indicate that she is severing the ropes from correct stakes. For example, if there is a print statement from Lord
FlowerKiller
saying "LordFlowerKiller
switched rope 4 from stake 4 to stake 2", then Marigold's statement about severing rope 2 must say: "Marigold severed rope 4 from stake 2". (Dandelion's print statements do print the stake index corresponding to the severed rope, because Dandelion cannot access stakes.) -
When all ropes have been severed (after the "severed" print statements for all NROPES ropes were displayed), the Balloon thread must print "Balloon freed and Prince Dandelion escapes!" and exit.
-
Each thread, except the main one, must print the "thread starting" message just as it begins running. I.e.:
"Dandelion thread starting"
"Marigold thread starting"
"Lord
FlowerKiller
thread starting""Balloon thread starting"
-
Each thread, including the main one, must print the "thread done" message just before it exits. I.e.:
"Dandelion thread done"
"Marigold thread done"
"Lord
FlowerKiller
thread done""Balloon thread done"
"Main thread done"
-
The main thread must print only after all other threads have printed their departing statements. That is, the "Main thread done" statement must be the last one to appear before your kernel returns to the main menu.
-
The print statements must be atomic: characters from one print statement must not interleave with another.
-
The print statements from different actors must interleave: for instance you can't have all Dandelion print statements appear together first, and then all Marigold statements appear together. You will accomplish it by using fine-grained locks (no one big lock) and by calling
thread_yield()
every time after Dandelion or Marigold sever the rope or after LordFlowerKiller
switches the ropes.
Step 4. Write, test and debug the code
configure the kernel:
1 |
|
Test your code by invoking sp1
from the kernel menu.
Debugging tips:
- Get one type of thread working first, e.g., Marigold or Dandelion. Make sure it does not deadlock with itself and otherwise works as you expect. Then add other thread types.
- Deadlocks are easier to debug than race conditions. If you are struggling, put one big lock around the critical sections that are causing you trouble. Make sure your code is working. Then gradually reduce the size of the critical section, making your big lock cover a smaller and smaller area of the code, until your code breaks. This way you will narrow down the few lines of code where the error is occurring.
- If you encounter assertions in the VM system (e.g., in the kmalloc.c file), chances are you are freeing a pointer that you did not allocate or are doing something else funky with the memory. If your kernel panics on a TLB fault, you are probably accessing a pointer that you did not allocate or a null pointer.
- If you need to figure out where a thread is stuck, print the address of the thread struct in your code:
kprintf("%p\n", curthread)
;. We will call the pointer that is printedthreadaddr
. Then you can print the contents of the thread struct in GDB:print *(struct thread) <threadaddr>
. You can also figure out which thread holds the lock by printing the lock struct in gdb, for example:print stake->lk_stake
;. If you are keeping track of the lock holder inside the lock structure, that field will show thethreadaddr
or the holder thread. - If your code is stuck in a deadlock, you should be able to attach to it with the debugger using the
os161-gdb
command and following the instructions linked in A1. This should work even if you did not launch your sys161 with -w flag.
My Solution
Use the synchronization primitives(lock, CV, semaphore) implemented in A2 to solve this synchronization problem. Specifically, I choose to use a lock for each object in the problem(stake, rope, hook) and use condition variables for some threads to wait for other threads to exit.
Why and How I represent each object(stake, rope, hook) in the problem?
Object-oriented programming makes this problem easier. Plus, there needs to be some kind of separation between stakes and hooks as they can only operate on their own side of the rope. Use a lock for each object to avoid big lock.
Also, we need to protect shared global variables if they exist.
1 |
|
How to have some threads to wait for other threads to exit?
Specifically, I use a condition variable for balloon thread waiting for all dandelion, marigold and flowerkiller threads to exit. I use another cv for main thread waiting for balloon thread to exit.
1 |
|
Let's go into the core of the problem. What is my design and locking protocols that must be maintained? How do all threads know when they are done?
Use three structures: rope, stake and hook.
Each rope and stake has its own lock. Actually we don't need a lock for hooks as they always maintain 1:1 correspondence to the ropes. Dandelion accesses each rope only from its hook, required to lock the rope before severing it. Marigold should lock the stake first before accessing the rope from its stake, and lock the rope before severing it. Flowerkiller should lock both stakes first before accessing the ropes from them, and lock the corresponding ropes(in particular order) before swaping them.
Dandelion and marigold threads should exit when global variable ropes_left = 0
. Flowerkiller threads should exit when global variable ropes_left < 2
. Balloon thread should wait for all dandelion, marigold and flowerkillers threads to exit before exiting. Airballoon(main) thread should wait for balloon thread to exit before exiting.
1 |
|
As shown in the code, I encapsulate some operations into helper functions in order to clarify the main logic.
1 |
|
Test sp1 passes!
We have finished Assignment3!😊