This lab will familiarize you with the implementation of system
calls and upcalls. In particular,
you will implement new system calls (sigalarm and sigreturn).
To start the lab, update your repository and create a new branch for your solution:
$ git fetch origin
$ git checkout -b alarm origin/xv6-19auIn 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. Your solution is correct
if it passes alarmtest and usertests.
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, 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.
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
$ usertests
...
ALL TESTS PASSED
$The first 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 how system calls work (i.e., the code in
kernel/trampoline.S and kernel/trap.c). Which register contains the
address to which system calls return?
Your solution will be only a few lines 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; if you modify alarmtest.c, make sure
your kernel changes cause the original alarmtest to pass the tests.
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. if(which_dev == 2) ...alarmtest.asm, periodic is at address 0).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:
Your solution will 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).
Have usertrap save enough state in struct proc when the timer
goes off that sigreturn can correctly return to the interrupted
user code.
Prevent re-entrant calls to the handler—if a handler hasn’t returned yet, the kernel shouldn’t call it again.
Once you pass test0 and test1, run usertests to make sure you
didn’t break any other parts of the kernel.
Your solution requires multiple user-kernel transitions and state save/restore in the kernel. This is not necessary. Implement a more efficient mechanism, such as user-level exception handling described in Hardware and Software Support for Efficient Exception Handling.
Additionally, the “N” extension of RISC-V (see the RISC-V instruction set manual) provides hardware support for user-level trap handling. Implement the “N” extension in M- or S- mode, or in QEMU, and modify your xv6 to use it.
Another approach to reduce the number of user-kernel transitions is downloading code into the kernel. For example, on Linux one can use extended BPF for tracing and profiling. Add BPF support to xv6 and use it to implement alarm.
This completes the lab. In the lab directory, commit your changes, type make tarball, and submit the tarball through Canvas.