We first must postulate a thread creation and manipulation
interface. Will use the one in Nachos:
class Thread {
public:
Thread(char* debugName);
~Thread();
void Fork(void (*func)(int), int arg);
void Yield();
void Finish();
}
The Thread constructor creates a new thread.
It allocates a data structure with space for the TCB.
To actually
start the thread running, must tell it what function to start
running when it runs. The Fork method gives it the function
and a parameter to the function.
What does Fork do? It first allocates a stack for the
thread. It then sets up the TCB so that when the thread starts
running, it will invoke the function and pass it the correct
parameter. It then puts the thread on a run queue someplace.
Fork then returns, and the thread that called Fork
continues.
How does OS set up TCB so that the thread starts running
at the function? First, it sets the stack pointer in the
TCB to the stack. Then, it sets the PC in the TCB to be the first
instruction in the function. Then, it sets the register in
the TCB holding the first parameter to the parameter. When the
thread system restores the state from the TCB, the function will magically
start to run.
The system maintains a queue of runnable threads. Whenever
a processor becomes idle, the thread scheduler grabs a thread
off of the run queue and runs the thread.
Conceptually, threads execute concurrently. This is the best
way to reason about the behavior of threads. But in practice, the
OS only has a finite number of processors, and it can't run
all of the runnable threads at once. So, must multiplex the
runnable threads on the finite number of processors.
Let's do a few thread examples. First example: two threads
that increment a variable.
int a = 0;
void sum(int p) {
a++;
printf("%d : a = %d\n", p, a);
}
void main() {
Thread *t = new Thread("child");
t->Fork(sum, 1);
sum(0);
}
The two calls to
sum run concurrently. What are the possible results
of the program? To understand this fully, we must break the
sum subroutine up into its primitive components.
sum first reads the value of a into a register.
It then increments the register, then stores the contents
of the register back into a . It then reads the values of
of the control string,
p and a into the registers that it uses to
pass arguments to the printf routine. It then calls
printf , which prints out the data.
The best way to understand the instruction sequence
is to look at the generated assembly language (cleaned up just a bit).
You can have the compiler generate assembly code instead of
object code by giving it the -S flag. It will put the generated
assembly in the same file name as the .c or .cc file, but with
a .s suffix.
la a, %r0
ld [%r0],%r1
add %r1,1,%r1
st %r1,[%r0]
ld [%r0], %o3
!parameters are passed starting with %o0
mov %o0, %o1
la .L17, %o0
call printf
|