492 lines
16 KiB
HTML
492 lines
16 KiB
HTML
<html>
|
|
<head>
|
|
<title>Lab: Alarm and uthread</title>
|
|
<link rel="stylesheet" href="homework.css" type="text/css" />
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Lab: Alarm and uthread</h1>
|
|
|
|
This lab makes you familiar with the implementation of system calls
|
|
and switching between threads of execution. In particular, you will
|
|
implement new system calls (<tt>sigalarm</tt> and <tt>sigreturn</tt>)
|
|
and switching between threads of a user-level thread package.
|
|
|
|
<h2>Warmup: RISC-V assembly</h2>
|
|
|
|
<p>For this lab it will be important to understand a bit of RISC-V assembly.
|
|
|
|
<p>Add a file user/call.c with the following content, modify the
|
|
Makefile to add the program to the user programs, and compile (make
|
|
fs.img). The Makefile also produces a binary and a readable
|
|
assembly a version of the program in the file user/call.asm.
|
|
<pre>
|
|
#include "kernel/param.h"
|
|
#include "kernel/types.h"
|
|
#include "kernel/stat.h"
|
|
#include "user/user.h"
|
|
|
|
int g(int x) {
|
|
return x+3;
|
|
}
|
|
|
|
int f(int x) {
|
|
return g(x);
|
|
}
|
|
|
|
void main(void) {
|
|
printf(1, "%d %d\n", f(8)+1, 13);
|
|
exit();
|
|
}
|
|
</pre>
|
|
|
|
<p>Read through user/call.asm and understand it. The instruction manual
|
|
for RISC-V is in the doc directory (doc/riscv-spec-v2.2.pdf). Here
|
|
are some questions that you should answer for yourself:
|
|
|
|
<ul>
|
|
<li>Which registers contain arguments to functions? Which
|
|
register holds 13 in the call to <tt>printf</tt>? Which register
|
|
holds the second argument? Which register holds the third one? Etc.
|
|
|
|
<li>Where is the function call to <tt>f</tt> from main? Where
|
|
is the call to <tt>g</tt>?
|
|
(Hint: the compiler may inline functions.)
|
|
|
|
<li>At what address is the function <tt>printf</tt> located?
|
|
|
|
<li>What value is in the register <tt>ra</tt> just after the <tt>jalr</tt>
|
|
to <tt>printf</tt> in <tt>main</tt>?
|
|
</ul>
|
|
|
|
<h2>Warmup: system call tracing</h2>
|
|
|
|
<p>In this exercise you will modify the xv6 kernel to print out a line
|
|
for each system call invocation. It is enough to print the name of the
|
|
system call and the return value; you don't need to print the system
|
|
call arguments.
|
|
|
|
<p>
|
|
When you're done, you should see output like this when booting
|
|
xv6:
|
|
|
|
<pre>
|
|
...
|
|
fork -> 2
|
|
exec -> 0
|
|
open -> 3
|
|
close -> 0
|
|
$write -> 1
|
|
write -> 1
|
|
</pre>
|
|
|
|
<p>
|
|
That's init forking and execing sh, sh making sure only two file descriptors are
|
|
open, and sh writing the $ prompt. (Note: the output of the shell and the
|
|
system call trace are intermixed, because the shell uses the write syscall to
|
|
print its output.)
|
|
|
|
<p> Hint: modify the syscall() function in kernel/syscall.c.
|
|
|
|
<p>Run the xv6 programs you wrote in earlier labs and inspect the system call
|
|
trace. Are there many system calls? Which system calls correspond
|
|
to code in the applications you wrote?
|
|
|
|
<p>Optional: print the system call arguments.
|
|
|
|
|
|
<h2>Alarm</h2>
|
|
|
|
<p>
|
|
In this exercise you'll add a feature to xv6 that periodically alerts
|
|
a process as it uses CPU time. This might be useful for compute-bound
|
|
processes that want to limit how much CPU time they chew up, or for
|
|
processes that want to compute but also want to take some periodic
|
|
action. More generally, you'll be implementing a primitive form of
|
|
user-level interrupt/fault handlers; you could use something similar
|
|
to handle page faults in the application, for example.
|
|
|
|
<p>
|
|
You should add a new <tt>sigalarm(interval, handler)</tt> system call.
|
|
If an application calls <tt>sigalarm(n, fn)</tt>, then after every
|
|
<tt>n</tt> "ticks" of CPU time that the program consumes, the kernel
|
|
should cause application function
|
|
<tt>fn</tt> to be called. When <tt>fn</tt> returns, the application
|
|
should resume where it left off. A tick is a fairly arbitrary unit of
|
|
time in xv6, determined by how often a hardware timer generates
|
|
interrupts.
|
|
|
|
<p>
|
|
You should put the following test program in <tt>user/alarmtest.c</tt>:
|
|
|
|
<b>XXX Insert the final program here; maybe just give the code in the repo</b>
|
|
<pre>
|
|
#include "kernel/param.h"
|
|
#include "kernel/types.h"
|
|
#include "kernel/stat.h"
|
|
#include "kernel/riscv.h"
|
|
#include "user/user.h"
|
|
|
|
void test0();
|
|
void test1();
|
|
void periodic();
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
test0();
|
|
test1();
|
|
exit();
|
|
}
|
|
|
|
void test0()
|
|
{
|
|
int i;
|
|
printf(1, "test0 start\n");
|
|
alarm(2, periodic);
|
|
for(i = 0; i < 1000*500000; i++){
|
|
if((i % 250000) == 0)
|
|
write(2, ".", 1);
|
|
}
|
|
alarm(0, 0);
|
|
printf(1, "test0 done\n");
|
|
}
|
|
|
|
void
|
|
periodic()
|
|
{
|
|
printf(1, "alarm!\n");
|
|
}
|
|
|
|
void __attribute__ ((noinline)) foo(int i, int *j) {
|
|
if((i % 2500000) == 0) {
|
|
write(2, ".", 1);
|
|
}
|
|
*j += 1;
|
|
}
|
|
|
|
void test1() {
|
|
int i;
|
|
int j;
|
|
|
|
printf(1, "test1 start\n");
|
|
j = 0;
|
|
alarm(2, periodic);
|
|
for(i = 0; i < 1000*500000; i++){
|
|
foo(i, &j);
|
|
}
|
|
if(i != j) {
|
|
printf(2, "i %d should = j %d\n", i, j);
|
|
exit();
|
|
}
|
|
printf(1, "test1 done\n");
|
|
}
|
|
</pre>
|
|
|
|
The program calls <tt>sigalarm(2, periodic1)</tt> in <tt>test0</tt> to
|
|
ask the kernel to force a call to <tt>periodic()</tt> every 2 ticks,
|
|
and then spins for a while. After you have implemented
|
|
the <tt>sigalarm()</tt> system call in the kernel,
|
|
<tt>alarmtest</tt> should produce output like this for <tt>test0</tt>:
|
|
|
|
<b>Update output for final usertests.c</b>
|
|
<pre>
|
|
$ alarmtest
|
|
alarmtest starting
|
|
.....alarm!
|
|
....alarm!
|
|
.....alarm!
|
|
......alarm!
|
|
.....alarm!
|
|
....alarm!
|
|
....alarm!
|
|
......alarm!
|
|
.....alarm!
|
|
...alarm!
|
|
...$
|
|
</pre>
|
|
<p>
|
|
|
|
<p>
|
|
(If you only see one "alarm!", try increasing the number of iterations in
|
|
<tt>alarmtest.c</tt> by 10x.)
|
|
|
|
<p>The main challenge will be to arrange that the handler is invoked
|
|
when the process's alarm interval expires. You'll need to modify
|
|
usertrap() in kernel/trap.c so that when a
|
|
process's alarm interval expires, the process executes
|
|
the handler. How can you do that? You will need to understand in
|
|
detail how system calls work (i.e., the code in kernel/trampoline.S
|
|
and kernel/trap.c). Which register contains the address where
|
|
system calls return to?
|
|
|
|
<p>Your solution will be few lines of code, but it will be tricky to
|
|
write the right lines of code. The most common failure scenario is that the
|
|
user program crashes or doesn't terminate. You can see the assembly
|
|
code for the alarmtest program in alarmtest.asm, which will be handy
|
|
for debugging.
|
|
|
|
<h3>Test0: invoke handler</h3>
|
|
|
|
<p>To get started, the best strategy is to first pass test0, which
|
|
will force you to handle the main challenge above. Here are some
|
|
hints how to pass test0:
|
|
|
|
<ul>
|
|
|
|
<li>You'll need to modify the Makefile to cause <tt>alarmtest.c</tt>
|
|
to be compiled as an xv6 user program.
|
|
|
|
<li>The right declaration to put in <tt>user/user.h</tt> is:
|
|
<pre>
|
|
int sigalarm(int ticks, void (*handler)());
|
|
</pre>
|
|
|
|
<li>Update kernel/syscall.h and user/usys.S (update usys.pl to update
|
|
usys.S) to allow <tt>alarmtest</tt> to invoke the sigalarm system
|
|
call.
|
|
|
|
<li>Your <tt>sys_sigalarm()</tt> should store the alarm interval and
|
|
the pointer to the handler function in new fields in the <tt>proc</tt>
|
|
structure; see <tt>kernel/proc.h</tt>.
|
|
|
|
<li>You'll need to keep track of how many ticks have passed since the
|
|
last call (or are left until the next call) to a process's alarm
|
|
handler; you'll need a new field in <tt>struct proc</tt> for this
|
|
too. You can initialize <tt>proc</tt> fields in <tt>allocproc()</tt>
|
|
in <tt>proc.c</tt>.
|
|
|
|
<li>Every tick, the hardware clock forces an interrupt, which is handled
|
|
in <tt>usertrap()</tt>; you should add some code here.
|
|
|
|
<li>You only want to manipulate a process's alarm ticks if there's a a
|
|
timer interrupt; you want something like
|
|
<pre>
|
|
if(which_dev == 2) ...
|
|
</pre>
|
|
|
|
<li>Only invoke the process's alarm function, if the process has a
|
|
timer outstanding. Note that the address of the user's alarm
|
|
function might be 0 (e.g., in alarmtest.asm, <tt>periodic</tt> is at
|
|
address 0).
|
|
|
|
<li>It will be easier to look at traps with gdb if you tell qemu to
|
|
use only one CPU, which you can do by running
|
|
<pre>
|
|
make CPUS=1 qemu
|
|
</pre>
|
|
|
|
</ul>
|
|
|
|
<h3>test1(): resume interrupted code</h3>
|
|
|
|
<p>Test0 doesn't tests whether the handler returns correctly to
|
|
interrupted instruction in test0. If you didn't get this right, it
|
|
is likely that test1 will fail (the program crashes or the program
|
|
goes into an infinite loop).
|
|
|
|
<p>A main challenge is to arrange that when the handler returns, it
|
|
returns to the instruction where the program was interrupted. Which
|
|
register contains the return address of a function? When the kernel
|
|
receives an interrupt, which register contains the address of the
|
|
interrupted instruction?
|
|
|
|
<p>Your solution is likely to require you to save and restore
|
|
registers---what registers do you need to save and restore to resume
|
|
the interrupted code correctly? (Hint: it will be many). There are
|
|
several ways to do this, but one convenient way is to add another
|
|
system call <tt>sigreturn</tt> that the handler calls when it is
|
|
done. Your job is to arrange that <tt>sigreturn</tt> returns to the
|
|
interrupted code.
|
|
|
|
Some hints:
|
|
<ul>
|
|
<li>Add the <tt>sigreturn</tt> system call, following the changes
|
|
you made to support <tt>sigalarm</tt>.
|
|
|
|
<li>Save enough state when the timer goes in the <tt>struct
|
|
proc</tt> so that <tt>sigreturn</tt> can return to the
|
|
interrupted code.
|
|
|
|
<li>Prevent re-entrant calls to the handler----if a handler hasn't
|
|
returned yet, don't call it again.
|
|
</ul>
|
|
|
|
<p>Once you pass <tt>test0</tt> and <tt>test1</tt>, run usertests to
|
|
make sure you didn't break any other parts of the kernel.
|
|
|
|
<h2>Uthread: switching between threads</h2>
|
|
|
|
<p>Download <a href="uthread.c">uthread.c</a> and <a
|
|
href="uthread_switch.S">uthread_switch.S</a> into your xv6 directory.
|
|
Make sure <tt>uthread_switch.S</tt> ends with <tt>.S</tt>, not
|
|
<tt>.s</tt>. Add the
|
|
following rule to the xv6 Makefile after the _forktest rule:
|
|
|
|
<pre>
|
|
$U/_uthread: $U/uthread.o $U/uthread_switch.o
|
|
$(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $U/_uthread $U/uthread.o $U/uthread_switch.o $(ULIB)
|
|
$(OBJDUMP) -S $U/_uthread > $U/uthread.asm
|
|
</pre>
|
|
Make sure that the blank space at the start of each line is a tab,
|
|
not spaces.
|
|
|
|
<p>
|
|
Add <tt>_uthread</tt> in the Makefile to the list of user programs defined by UPROGS.
|
|
|
|
<p>Run xv6, then run <tt>uthread</tt> from the xv6 shell. The xv6 kernel will print an error message about <tt>uthread</tt> encountering a page fault.
|
|
|
|
<p>Your job is to complete <tt>uthread_switch.S</tt>, so that you see output similar to
|
|
this (make sure to run with CPUS=1):
|
|
<pre>
|
|
~/classes/6828/xv6$ make CPUS=1 qemu
|
|
...
|
|
$ uthread
|
|
my thread running
|
|
my thread 0x0000000000002A30
|
|
my thread running
|
|
my thread 0x0000000000004A40
|
|
my thread 0x0000000000002A30
|
|
my thread 0x0000000000004A40
|
|
my thread 0x0000000000002A30
|
|
my thread 0x0000000000004A40
|
|
my thread 0x0000000000002A30
|
|
my thread 0x0000000000004A40
|
|
my thread 0x0000000000002A30
|
|
...
|
|
my thread 0x0000000000002A88
|
|
my thread 0x0000000000004A98
|
|
my thread: exit
|
|
my thread: exit
|
|
thread_schedule: no runnable threads
|
|
$
|
|
</pre>
|
|
|
|
<p><tt>uthread</tt> 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.
|
|
|
|
<p>To observe the above output, you need to complete <tt>uthread_switch.S</tt>, but before
|
|
jumping into <tt>uthread_switch.S</tt>, first understand how <tt>uthread.c</tt>
|
|
uses <tt>uthread_switch</tt>. <tt>uthread.c</tt> has two global variables
|
|
<tt>current_thread</tt> and <tt>next_thread</tt>. Each is a pointer to a
|
|
<tt>thread</tt> structure. The thread structure has a stack for a thread and a
|
|
saved stack pointer (<tt>sp</tt>, which points into the thread's stack). The
|
|
job of <tt>uthread_switch</tt> is to save the current thread state into the
|
|
structure pointed to by <tt>current_thread</tt>, restore <tt>next_thread</tt>'s
|
|
state, and make <tt>current_thread</tt> point to where <tt>next_thread</tt> was
|
|
pointing to, so that when <tt>uthread_switch</tt> returns <tt>next_thread</tt>
|
|
is running and is the <tt>current_thread</tt>.
|
|
|
|
<p>You should study <tt>thread_create</tt>, which sets up the initial stack for
|
|
a new thread. It provides hints about what <tt>uthread_switch</tt> should do.
|
|
Note that <tt>thread_create</tt> simulates saving all callee-save registers
|
|
on a new thread's stack.
|
|
|
|
<p>To write the assembly in <tt>thread_switch</tt>, you need to know how the C
|
|
compiler lays out <tt>struct thread</tt> in memory, which is as
|
|
follows:
|
|
|
|
<pre>
|
|
--------------------
|
|
| 4 bytes for state|
|
|
--------------------
|
|
| stack size bytes |
|
|
| for stack |
|
|
--------------------
|
|
| 8 bytes for sp |
|
|
-------------------- <--- current_thread
|
|
......
|
|
|
|
......
|
|
--------------------
|
|
| 4 bytes for state|
|
|
--------------------
|
|
| stack size bytes |
|
|
| for stack |
|
|
--------------------
|
|
| 8 bytes for sp |
|
|
-------------------- <--- next_thread
|
|
</pre>
|
|
|
|
The variables <tt>&next_thread</tt> and <tt>¤t_thread</tt> each
|
|
contain the address of a pointer to <tt>struct thread</tt>, and are
|
|
passed to <tt>thread_switch</tt>. The following fragment of assembly
|
|
will be useful:
|
|
|
|
<pre>
|
|
ld t0, 0(a0)
|
|
sd sp, 0(t0)
|
|
</pre>
|
|
|
|
This saves <tt>sp</tt> in <tt>current_thread->sp</tt>. This works because
|
|
<tt>sp</tt> is at
|
|
offset 0 in the struct.
|
|
You can study the assembly the compiler generates for
|
|
<tt>uthread.c</tt> by looking at <tt>uthread.asm</tt>.
|
|
|
|
<p>To test your code it might be helpful to single step through your
|
|
<tt>uthread_switch</tt> using <tt>riscv64-linux-gnu-gdb</tt>. You can get started in this way:
|
|
|
|
<pre>
|
|
(gdb) file user/_uthread
|
|
Reading symbols from user/_uthread...
|
|
(gdb) b *0x230
|
|
|
|
</pre>
|
|
0x230 is the address of uthread_switch (see uthread.asm). When you
|
|
compile it may be at a different address, so check uthread_asm.
|
|
You may also be able to type "b uthread_switch". <b>XXX This doesn't work
|
|
for me; why?</b>
|
|
|
|
<p>The breakpoint may (or may not) be triggered before you even run
|
|
<tt>uthread</tt>. How could that happen?
|
|
|
|
<p>Once your xv6 shell runs, type "uthread", and gdb will break at
|
|
<tt>thread_switch</tt>. Now you can type commands like the following to inspect
|
|
the state of <tt>uthread</tt>:
|
|
|
|
<pre>
|
|
(gdb) p/x *next_thread
|
|
$1 = {sp = 0x4a28, stack = {0x0 (repeats 8088 times),
|
|
0x68, 0x1, 0x0 <repeats 102 times>}, state = 0x1}
|
|
</pre>
|
|
What address is <tt>0x168</tt>, which sits on the bottom of the stack
|
|
of <tt>next_thread</tt>?
|
|
|
|
With "x", you can examine the content of a memory location
|
|
<pre>
|
|
(gdb) x/x next_thread->sp
|
|
0x4a28 <all_thread+16304>: 0x00000168
|
|
</pre>
|
|
Why does that print <tt>0x168</tt>?
|
|
|
|
<h3>Optional challenges</h3>
|
|
|
|
<p>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 <tt>thread_schedule</tt>
|
|
concurrently, select the same runnable thread, and both run it on different
|
|
processors.)
|
|
|
|
<p>There are several ways of addressing these problems. One is
|
|
using <a href="http://en.wikipedia.org/wiki/Scheduler_activations">scheduler
|
|
activations</a> and another is to use one kernel thread per
|
|
user-level thread (as Linux kernels do). Implement one of these ways
|
|
in xv6. This is not easy to get right; for example, you will need to
|
|
implement TLB shootdown when updating a page table for a
|
|
multithreaded user process.
|
|
|
|
<p>Add locks, condition variables, barriers,
|
|
etc. to your thread package.
|
|
|
|
</body>
|
|
</html>
|
|
|