Lab alarm: System calls & upcalls

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-19au

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. 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.

test0: invoke handler

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:

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    if(which_dev == 2) ...

test1: resume execution

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:

Once you pass test0 and test1, run usertests to make sure you didn’t break any other parts of the kernel.

Optional challenges

Challenge: user-level trap handling

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.

Challenge: BPF

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.