Assume you have already cloned xv6:
git pull; git checkout uthread
Run xv6, and run uthread
from the xv6 shell, and you will observe a panic.
Your job is to complete uthread_switch.S
, so that you see output
similar to this (make sure to run with CPUS=1
):
uthread
creates two threads and switches back and forth between
them. Each thread prints “my thread …” and then yields to give
the other thread a chance to run.
To observe the above output, you need to complete uthread_switch.S
,
but before jumping into uthread_switch.S
, first understand how
uthread.c
uses thread_switch
. uthread.c
has two global variables
current_thread
and next_thread
. Each is a pointer to a thread
structure. The thread structure has a stack for a thread and a saved
stack pointer (sp, which points into the thread’s stack). The job
of uthread_switch
is to save the current thread state into the
structure pointed to by current_thread
, restore next_thread
’s state,
and make current_thread
point to where next_thread
was pointing to,
so that when uthread_switch
returns next_thread
is running and is
the current_thread
.
You should study thread_create
, which sets up the initial stack for
a new thread. It provides you good hint what thread_switch
should
do. In particular, note that thread_create
allocates 32 bytes of
space for saving registers. That is all x86 registers, and the
intent is that thread_switch
use the assembly instructions popal
and pushal
to restore and save registers.
To write the assembly in thread_switch
, you need to know how the C
compiler lays out struct thread
in memory, which is as follows:
--------------------
| 4 bytes for state|
--------------------
| stack size bytes |
| for stack |
--------------------
| 4 bytes for sp |
-------------------- <--- current_thread
......
......
--------------------
| 4 bytes for state|
--------------------
| stack size bytes |
| for stack |
--------------------
| 4 bytes for sp |
-------------------- <--- next_thread
The variables next_thread
and current_thread
each contain the address
of a struct thread
.
To write the sp
field of the struct that current_thread
points to,
you should write assembly like this:
movl current_thread, %eax
movl %esp, (%eax)
This saves %esp
in current_thread->sp
. This works because sp
is at
offset 0 in the struct. You can study the assembly the compiler
generates for uthread.c
by looking at uthread.asm
.
To test your code it might be helpful to single step through your
thread_switch
using gdb. You can do this as follows:
Note the continue to skip the first time that gdb breaks. This first
break is not in uthread
. uthread
is not even running yet. Why is
gdb breaking then?
Once your xv6 shell runs, type uthread, and gdb will break at
thread_switch
. Now you can type commands like the following to
inspect the state of uthread
:
What address is 0x161
, which sits on the top of the stack of next_thread
?
The user-level thread package interacts badly with the operating
system in several ways. For example, if one user-level thread blocks
in a system call, another user-level thread won’t run, because the
user-level threads scheduler doesn’t know that one of its threads
has been descheduled by the xv6 scheduler. As another example, two
user-level threads will not run concurrently on different cores,
because the xv6 scheduler isn’t aware that there are multiple threads
that could run in parallel. Note that if two user-level threads
were to run truly in parallel, this implementation won’t work because
of several races (e.g., two threads on different processors could
call thread_schedule
concurrently, select the same runnable thread,
and both run it on different processors.)
There are several ways of addressing these problems. One is using scheduler activations and another is to use one kernel thread per user-level thread (as Linux kernels do). Implement one of these ways in xv6.
Add locks, condition variables, barriers, etc. to your thread package.