This lab will familiarize you with the implementation of system calls and
upcalls (or “signals”). In particular, you will implement two new system calls:
sigalarm
and sigreturn
.
Before you start coding, read §4, Traps and System Calls of the xv6 book and related source files (which we already looked through during class):
kernel/trampoline.S
kernel/trap.c
To start the lab, fetch the latest version and create a new branch for your solution:
$ git fetch origin
$ git checkout -b alarm origin/main
$ make clean
It will be important to understand a bit of RISC-V assembly for this lab assignment.
There is a file user/call.c
in your xv6 repo.
make fs.img compiles it and
also produces a readable (disassembled) version of the program in user/call.asm
.
Read the code in user/call.asm
for the functions g
, f
, and main
. Please see the assignments page for a link to the RISC-V ISA manual; however, there are many other resources online that can also provide useful information to understand RISC-V assembly.
We have included some questions below for you to think about. Note that you don’t need to submit answers to the questions in this lab. Do answer them for yourself though!
main
’s call to printf
?f
in the assembly code for main
?
Where is the call to g
? (Hint: the compiler may inline functions.)printf
located?ra
just after the jalr
to printf
in main
?In the following code,
what is going to be printed after y=
?
(note: the answer is not a specific value.)
Why does this happen?
printf("x=%d y=%d", 3);
For debugging it is often useful to have a backtrace:
a list of the function calls on the stack above the point at which the error occurred.
This is implemented by the backtrace()
function in kernel/printf.c
.
Suppose that we insert a call to backtrace()
in sys_sleep
(kernel/sysproc.c
)
and run sleep
from your lab util solution, which calls sys_sleep
.
Your output should be as follows:
[<0x0000000080002df0>]
[<0x0000000080002cca>]
[<0x0000000080002996>]
If you run make qemu-trace instead of make qemu, you should see something that looks like a Linux kernel panic:
[<0x0000000080002df0>] sys_sleep+0x16/0xb4
[<0x0000000080002cca>] syscall+0x3e/0x6c
[<0x0000000080002996>] usertrap+0x60/0x108
Read the source code of backtrace()
.
How does this function walk up the stack and print the saved return address in each stack frame?
Hints:
s0
/fp
.PGROUNDDOWN(fp)
and PGROUNDUP(fp)
(see kernel/riscv.h
). These number are helpful for backtrace()
to terminate its loop.In this lab, you will add a feature to xv6 that periodically alerts a process
as it executes on the CPU over 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. Your solution is correct if it passes alarmtest
and
usertests
. For usertests
, some tests result in the kernel printing usertrap
messages (e.g., usertrap(): unexpected scause...
), which can be ignored if
test prints “OK”. usertests
is essentially a way to make sure that your
implementation didn’t break any other existing behavior of the kernel.
You should add a new sigalarm(interval, handler)
system call. If an
application calls sigalarm(n, fn)
, then after every n
“ticks” of CPU time
that the program consumes, the kernel should cause application function fn
to
be called. When fn
returns via the sigreturn
system call, the application should resume where it previously left off.
A tick is a fairly arbitrary unit of time in xv6, determined by how often a
hardware timer generates interrupts. If an application calls sigalarm(0, 0)
,
the kernel should stop generating periodic alarm calls.
You’ll find a file user/alarmtest.c
in your xv6 repository. Add it to the
Makefile
. It won’t compile correctly until you’ve added sigalarm
and
sigreturn
system calls (see below).
alarmtest
calls sigalarm(2, periodic)
in test0
to ask the kernel to force
a call to periodic()
every 2 ticks, and then spins for a while. You can see
the assembly code for alarmtest
in user/alarmtest.asm
, which may be handy
for debugging. Your solution is correct, when alarmtest
produces output like
this and usertests
also runs correctly:
$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
usertests starting
test truncate1: OK
...
usertrap(): unexpected scause ... pid=...
sepc=... stval=...
...
test bigdir: OK
ALL TESTS PASSED
$
When you’re done, your solution will not involve a lot of code, but it may be
tricky to get it right. We’ll test your code with the version of alarmtest.c
in the original repository. You can modify alarmtest.c
to help you debug, but
make sure to revert to the original alarmtest.c
before submitting your
solution or running the grading scripts on your end.
Get started by modifying the kernel to jump to the alarm handler
in user space, which will cause test0
to print “alarm!”. Don’t worry
yet what happens after the “alarm!” output; it’s OK for now if your
program crashes after printing “alarm!”. Here are some hints:
Makefile
to cause alarmtest.c
to
be compiled as an xv6 user program.user/user.h
are: int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
user/usys.pl
(which generates user/usys.S
), kernel/syscall.h
,
and kernel/syscall.c
to allow alarmtest
to invoke the sigalarm
and sigreturn
system calls.sys_sigreturn
should just return zero.sys_sigalarm()
should store the alarm interval and the
pointer to the handler function in new fields in the proc
structure,
defined in kernel/proc.h
.struct proc
for this, too. You
can initialize proc fields in allocproc()
in proc.c
.usertrap()
; you should add some code here to keep track of the ticks that passed while a process was active on the CPU. if(which_dev == 2) ...
usertrap()
so that when a process’s alarm interval is met then the signal handler of the process is executed. An important question to think about is: When a trap on RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?alarmtest
prints “alarm!”.Chances are that alarmtest
crashes in test0
or test1
after it prints
“alarm!”, or that alarmtest
(eventually) prints “test1 failed”, or
that alarmtest
exits without printing “test1 passed”. To fix this,
you must ensure that, when the alarm handler is done, control returns
to the instruction at which the user program was originally interrupted
by the timer interrupt. You must ensure that the register contents
are restored to the values they held at the time of the interrupt,
so that the user program can continue undisturbed after the alarm.
Finally, you should “re-arm” the alarm counter after each time it
goes off, so that the handler is called periodically.
As a starting point, we’ve made a design decision for you: user
alarm handlers are required to call the sigreturn
system call when
they have finished. Have a look at periodic
in alarmtest.c
for an
example. This means that you can add code to usertrap
and sys_sigreturn
that cooperate to cause the user process to resume properly after
it has handled the alarm.
Some hints:
usertrap
save enough state in struct proc
when the timer
goes off that sigreturn
can correctly return to the interrupted
user code.test2
checks for this behavior.Once you pass test0
, test1
, and test2
, run usertests
to make sure you
didn’t break any other parts of the kernel.