Lab alarm: System calls & upcalls

This lab will familiarize you with the implementation of system calls and upcalls (or signals). In particular, you will implement new system calls (sigalarm and sigreturn).

Trap handling in the xv6 book

Before you start coding, read §4, Traps and system calls of the xv6 book, and related source files:

  • kernel/trampoline.S: the assembly involved in changing from user space to kernel space and back
  • kernel/trap.c: code handling all interrupts

To start the lab, fetch the latest versions and create a new branch for your solution:

$ git fetch origin
$ git checkout -b alarm origin/main
$ make clean

RISC-V assembly

It will be important to understand a bit of RISC-V assembly. There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly 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!

Question

  • Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
  • Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
  • At what address is the function printf located?
  • What value is in the register ra just after the jalr to printf in main?

Question

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);

Question

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:

  • The GCC compiler stores the frame pointer of the currently executing function in the register s0/fp.
  • The return address lives at a fixed offset (-8) from the frame pointer of a stack frame, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
  • Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. One can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h). These number are helpful for backtrace() to terminate its loop.

Alarm

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. Your solution is correct if it passes alarmtest and usertests. For usertests, some tests results in 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, 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. 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.

Invoking the Alarm 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) ...

Resuming Interrupted Code

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, test1, and test2, run usertests to make sure you didn’t break any other parts of the kernel.