Background
Understanding Threads
The first step is to read and understand the code for the initial thread system.
Pintos already implements thread creation and thread completion, a simple scheduler to switch between threads, and synchronization primitives (semaphores, locks, condition variables, and optimization barriers).
Some of this code might seem slightly mysterious. You can read through parts of the source code to see what's going on.
If you like, you can add calls to
printf()
almost anywhere, then recompile and run to see what happens and in what order.You can also run the kernel in a debugger and set breakpoints at interesting spots, single-step through code and examine data, and so on.
Create a New Thread
When a thread is created, you are creating a new context to be scheduled. You provide a function to be run in this context as an argument to thread_create()
.
The first time the thread is scheduled and runs, it starts from the beginning of that function and executes in that context.
When the function returns, the thread terminates. Each thread, therefore, acts like a mini-program running inside Pintos, with the function passed to
thread_create()
acting likemain()
.
At any given time, exactly one thread runs and the rest, if any, become inactive. The scheduler decides which thread to run next.
If no thread is ready to run at any given time, then the special "idle" thread, implemented in
idle()
, runs.Synchronization primitives can force context switches when one thread needs to wait for another thread to do something.
Context Switch
The mechanics of a context switch are in threads/switch.S
, which is 80x86 assembly code. (You don't have to understand it.)
It saves the state of the currently running thread and restores the state of the thread we're switching to.
Using the GDB debugger, slowly trace through a context switch to see what happens (see section GDB).
You can set a breakpoint on
schedule()
to start out, and then single-step from there.Be sure to keep track of each thread's address and state, and what procedures are on the call stack for each thread.
You will notice that when one thread calls
switch_threads()
, another thread starts running, and the first thing the new thread does is to return fromswitch_threads()
.You will understand the thread system once you understand why and how the
switch_threads()
that gets called is different from theswitch_threads()
that returns. See section Thread Switching, for more information.
Warning
In Pintos, each thread is assigned a small, fixed-size execution stack just under 4 kB in size.
The kernel tries to detect stack overflow, but it cannot do so perfectly. You may cause bizarre problems, such as mysterious kernel panics, if you declare large data structures as non-static local variables, e.g. int buf[1000];.
Alternatives to stack allocation include the page allocator and the block allocator (see section Memory Allocation).
Source Files
This part provides a brief overview of the source files related to lab1. You will not need to modify most of this code, but the hope is that presenting this overview will give you a start on what code to look at.
Synchronization
Proper synchronization is an important part of the solutions to these problems.
Any synchronization problem can be easily solved by turning interrupts off: while interrupts are off, there is no concurrency, so there's no possibility for race conditions. Therefore, it's tempting to solve all synchronization problems this way, but don't.
Instead, use semaphores, locks, and condition variables to solve the bulk of your synchronization problems.
Read the tour section on synchronization (see section Synchronization) or the comments in
threads/synch.c
if you're unsure what synchronization primitives may be used in what situations.
In the Pintos projects, the only class of problem best solved by disabling interrupts is coordinating data _shared between a kernel thread and an interrupt handler_.
Because interrupt handlers can't sleep, they can't acquire locks. This means that data shared between kernel threads and an interrupt handler must be protected within a kernel thread by turning off interrupts.
This project only requires accessing a little bit of thread state from interrupt handlers.
For the alarm clock, the timer interrupt needs to wake up sleeping threads.
In the advanced scheduler, the timer interrupt needs to access a few global and per-thread variables. When you access these variables from kernel threads, you will need to disable interrupts to prevent the timer interrupt from interfering.
When you do turn off interrupts, take care to do so for the least amount of code possible.
Otherwise you can end up losing important things such as timer ticks or input events. ****
Turning off interrupts also increases the interrupt handling latency, which can make a machine feel sluggish if taken too far.
The synchronization primitives themselves in synch.c
are implemented by disabling interrupts.
You may need to increase the amount of code that runs with interrupts disabled here, but you should still try to keep it to a minimum.
Disabling interrupts can be useful for debugging, if you want to make sure that a section of code is not interrupted.
You should remove debugging code before turning in your project. (Don't just comment it out, because that can make the code difficult to read.)
There should be no busy waiting in your submission.
A tight loop that calls
thread_yield()
is one form of busy waiting.
Last updated