From 0c3125b9ebf5fde1396620da3f5839b88a3ae50b Mon Sep 17 00:00:00 2001 From: Frans Kaashoek Date: Fri, 2 Aug 2019 08:52:36 -0400 Subject: [PATCH] Add uthread --- labs/syscall.html | 269 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 216 insertions(+), 53 deletions(-) diff --git a/labs/syscall.html b/labs/syscall.html index 68abad2..d465f35 100644 --- a/labs/syscall.html +++ b/labs/syscall.html @@ -1,58 +1,20 @@ -Lab: system calls +Lab: Alarm and uthread -

Lab: system calls

+

Lab: Alarm and uthread

-This lab makes you familiar with the implementation of system calls. -In particular, you will implement a new system -calls: sigalarm and sigreturn. - -Note: before this lab, it would be good to have recitation section on gdb and understanding assembly - -

Warmup: system call tracing

- -

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

-When you're done, you should see output like this when booting -xv6: - -

-...
-fork -> 2
-exec -> 0
-open -> 3
-close -> 0
-$write -> 1
- write -> 1
-
- -

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

Hint: modify the syscall() function in kernel/syscall.c. - -

Run the programs you wrote in the lab and inspect the system call - trace. Are there many system calls? Which systems calls correspond - to code in the applications you wrote above? - -

Optional: print the system call arguments. +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 (sigalarm and sigreturn) +and switching between threads of a user-level thread package.

RISC-V assembly

-

For the alarm system call it will be important to understand RISC-V -assembly. Since in later labs you will also read and write assembly, -it is important that you familiarize yourself with RISC_V assembly. +

For this lab it will be important to understand RISC-V assembly.

Add a file user/call.c with the following content, modify the Makefile to add the program to the user programs, and compile (make @@ -96,8 +58,43 @@ void main(void) { to printf in main? +

Warmup: system call tracing

+ +

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

+When you're done, you should see output like this when booting +xv6: + +

+...
+fork -> 2
+exec -> 0
+open -> 3
+close -> 0
+$write -> 1
+ write -> 1
+
+ +

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

Hint: modify the syscall() function in kernel/syscall.c. + +

Run the programs you wrote in the lab and inspect the system call + trace. Are there many system calls? Which systems calls correspond + to code in the applications you wrote above? + +

Optional: print the system call arguments. + -

alarm

+

Alarm

In this exercise you'll add a feature to xv6 that periodically alerts @@ -227,7 +224,7 @@ alarmtest starting code for the alarmtest program in alarmtest.asm, which will be handy for debugging. -

Test0: invoke handler

+

Test0: invoke handler

To get started, the best strategy is to first pass test0, which will force you to handle the main challenge above. Here are some @@ -279,7 +276,7 @@ use only one CPU, which you can do by running -

test1(): resume interrupted code

+

test1(): resume interrupted code

Test0 doesn't tests whether the handler returns correctly to interrupted instruction in test0. If you didn't get this right, it @@ -311,16 +308,182 @@ use only one CPU, which you can do by running

  • Prevent re-entrant calls to the handler----if a handler hasn't returned yet, don'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. +

    Uthread: switching between threads

    +

    Download uthread.c and uthread_switch.S into your xv6 directory. +Make sure uthread_switch.S ends with .S, not +.s. Add the +following rule to the xv6 Makefile after the _forktest rule: + +

    +$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
    +
    +Make sure that the blank space at the start of each line is a tab, +not spaces. + +

    +Add _uthread in the Makefile to the list of user programs defined by UPROGS. + +

    Run xv6, then run uthread from the xv6 shell. The xv6 kernel will print an error message about uthread encountering a page fault. + +

    Your job is to complete uthread_switch.S, so that you see output similar to +this (make sure to run with CPUS=1): +

    +~/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
    +$
    +
    + +

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

    To observe the above output, you need to complete uthread_switch.S, but before +jumping into uthread_switch.S, first understand how uthread.c +uses uthread_switch. uthread.c has two global variables +current_thread and next_thread. Each is a pointer to a +thread structure. The thread structure has a stack for a thread and a +saved stack pointer (sp, which points into the thread's stack). The +job of uthread_switch is to save the current thread state into the +structure pointed to by current_thread, restore next_thread's +state, and make current_thread point to where next_thread was +pointing to, so that when uthread_switch returns next_thread +is running and is the current_thread. + +

    You should study thread_create, which sets up the initial stack for +a new thread. It provides hints about what uthread_switch should do. +Note that thread_create simulates saving all callee-save registers +on a new thread's stack. + +

    To write the assembly in thread_switch, you need to know how the C +compiler lays out struct thread in memory, which is as +follows: + +

    +    --------------------
    +    | 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
    +
    + +The variables &next_thread and ¤t_thread each +contain the address of a pointer to struct thread, and are +passed to thread_switch. The following fragment of assembly +will be useful: + +
    +   ld t0, 0(a0)
    +   sd sp, 0(t0)
    +
    + +This saves sp in current_thread->sp. This works because +sp is at +offset 0 in the struct. +You can study the assembly the compiler generates for +uthread.c by looking at uthread.asm. + +

    To test your code it might be helpful to single step through your +uthread_switch using riscv64-linux-gnu-gdb. You can get started in this way: + +

    +(gdb) file user/_uthread
    +Reading symbols from user/_uthread...
    +(gdb) b *0x230
    +
    +
    +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". XXX This doesn't work + for me; why? + +

    The breakpoint may (or may not) be triggered before you even run +uthread. How could that happen? + +

    Once your xv6 shell runs, type "uthread", and gdb will break at +thread_switch. Now you can type commands like the following to inspect +the state of uthread: + +

    +  (gdb) p/x *next_thread
    +  $1 = {sp = 0x4a28, stack = {0x0 (repeats 8088 times),
    +      0x68, 0x1, 0x0 }, state = 0x1}
    +
    +What address is 0x168, which sits on the bottom of the stack +of next_thread? + +With "x", you can examine the content of a memory location +
    +  (gdb) x/x next_thread->sp
    +  0x4a28 :      0x00000168
    +
    +Why does that print 0x168? + +

    Optional challenges

    + +

    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 thread_schedule +concurrently, select the same runnable thread, and both run it on different +processors.) + +

    There are several ways of addressing these problems. One is + using scheduler + activations 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. + +

    Add locks, condition variables, barriers, +etc. to your thread package. + - - - -