Installation (∼ 20mn)
This command starts xv6 in the qemu emulator. When xv6 starts, it launches a simple shell allowing to execute some basic commands. The code of shell is located in the sh.c file.
If you look at the source code, you can find the classic architecture of a system: the kernel library is called xv6 and consists of the files indicated in the variable OBJS that you will find in the Makefile. Each of the basic commands corresponds to a source file (cat.c, sh.c etc.). You can find the list of these commands, each prefixed with a _ in the variable UPROGS (user commands) of the Makefile.
In another terminal, navigate to the sources of xv6, then run gdb. If you see an error message similar to this one :
So, copy and paste the line displayed just after the line "To enable execution of this file add" in your gdb configuration file (~/.gdbinit) in order to allow the use of the .gdbinit script by gdb.
Once gdb starts correctly, type b main (break main) to interrupt the execution of xv6 when it reaches the main function then resume the execution of xv6 with the command c (continue). If all goes well, xv6 stops at the start of the main function.
Display the code of the main function with list. You can examine the code that initializes the system (the source code of this function can be found in main.c. Technically, this code is called from a trampoline in assembler which is located in entry.S and which takes care of installing the minimum vital allowing to switch to C. Technically, when grub loads the kernel, it jumps directly to the entry label, the assembler part correctly initializes the processor (we will explain how later) then jump on in the main function.
First system call (∼ 1h10)
- uses printf to display "starting". Attention, the printf function of xv6 takes a first parameter corresponding to the stream (stdin, stderr, stdout) you want to write on. As we want to write to the standard output, this parameter is 1 (remember of your CSC3102 courses).
- calls exit() to exit the command. Without exit(), the program does not stop naturally like in Linux at the end of main and therefore runs random code that usually leads to an error.
- int fwake(int slot, int val) : this function considers that the kernel has internal variables, called slots, that are numbered from 0 to 5. The fwake function assigns the value val to the slot th slot, then wakes up the processes that are waiting for the variable slot to change value (see next function);
- int fwait(int slot, int val) : this function pause the process as long as the slot number slot is not val.
A typical usage of fwake/fwait is given here:
This program starts by creating a child process that waits until slot 1 is set to 42 (initially, a slot is set to 0). The father process sleeps for 500 ticks (around 5s, time is managed in a very approximate in xv6) then sets slot 1 to 42, which wakes up the child process.
Start by copying and pasting this code into your ftest.c file.
- give a syscall number to our system calls by adding SYS_fwait (number 22) and SYS_fwake (number 23) in syscall.h;
- then, rather than writing the code that calls kernel functions 22 and 23 using assembly code, you are asked to add SYSCALL(fwait) and SYSCALL(fwake) at the end of usys.S. This macro then creates the fwait and fwake functions which call the system functions 22 and 23 for us. Technically, for fwait, this macro creates a fwait function which places the syscall number (22) in the registry %eax (remember that registers are internal variables of a processor) then executes the instruction int 0x40 which corresponds to the trap instruction in xv6;
- then add the definition of these functions to the user.h file. This file contains the C signatures of assembler functions found in usys.S . The signatures of these functions are given to the previous question: int fwait (int slot, int val) and int fwake (int slot, int val) .
The test part in userland allowing to test the new system calls is now ready. Since you have not yet associated any code with these system calls in the kernel, calls must fail miserably with "unknown sys call 22" and "unknown sys call 23 " messages. Check that this is the case by compiling, by running xv6 (make qemu) and running your ftest command.
First, we take a look at what's going on in userland. To do this, open the file ftest.asm. This file contains the assembly code of your command ftest:
- in that file, find the code corresponding to fwait(1, 42). You should see the setting up of arguments (push $0x2a knowing that 0x2a is equal to 42, and push $1) followed by a call to fwait (call 371). 371 is the address where the fwait function was generated. (Note: it is quite possible that you have an address other than 371 in your generated code).
- we can then follow the flow of execution by looking for this 371 in
the file. You should see the assembly code for the
fwait function generated from usys.S. This assembly
code consists of two instructions:
- mov $0x16, %eax : sets %eax to 22, this is the number that corresponds to the fwait system call,
- int $0x40 : trap instruction which switches to system mode.
From this point, the processor switches to system mode and continues its execution in the vector64 function (note that $0x40 = 64) which was associated with the trap when starting xv6. We can therefore follow the flow of execution as follows:
- the vector64 function is located in the file vector.S. Technically, during an int 0x40, the processor switches to system mode and performs this function;
- the assembler function vector64 sets arguments which, in fine, will be received by the C trap function located in the trap.c file. These are the values 64 and 0;
- the code continues in alltraps located in the file trapasm.S. This function fills a struct trapframe (defined in x86.h) by saving the registers (the continuation of pushl followed by pushal), then set up a trampoline before jumping in the C trap function located in trap.c; To understand how the struct trapframe is constructed, draw the contents of the stack after all these pushes and compare with the structure.
- the trap function receives a tf argument. tf->trapno is equal to 64 because vector64 executed push $64. This value being equal to T_SYSCALL, the trap function calls syscall located in syscall.c;
- the syscall function finds argument 22 corresponding to the system call by consulting tf->eax (remember the mov $0x16, %eax in userland ). As this entry does not yet exist in the kernel, syscall displays that the system call is unknown;
- as the system call was unsuccessful, syscall logs the value -1 in tf->eax. This value is the return value of the system call. The execution continues in the opposite direction until we come back to the call site at fwait(1, 42) in the main of ftest.c.
To navigate more easily in the sources of xv6, we invite you to use the ctags tool (sudo apt-get install exuberant-ctags) which allows you to index source code and perform research.
- To generate an index, run the command make tags which invokes etags *.S *.c (you can also modify the Makefile to also index *.h files).
- With emacs:
- To find the definition of a function (or of a type), position the cursor on the type, then do M-. ("META dot", so the Esc key then the . key)
- To go back, type M-,
- With VIM:
- To find the definition of a function or a type, position the cursor on the function, then type C-] (Ctrl and ])
- To go back, type C-t
In the syscall.c file, modify the syscalls table to match respectively the system call SYS_fwait to sys_fwait and the SYS_fwake system call to sys_fwake. Add the signatures of these functions just before the definition of the syscalls structure, as it is done for sys_uptime.
Check that the messages are displayed when you run the command ftest .
Implementing system calls (∼ 1h)
- set the wait_slot field of the current process to 1,
- set the state of the current process to SLEEPING, which makes the process ineligible.
- call sched() so that xv6 runs another process (xv6 chooses a process in RUNNABLE state and therefore ignore our process),
- reset wait_slot to 0.
- acquire the lock table.lock,
- extract the slot and arg arguments,
- assign the value val to slots [slot],
- browse process table (from ptable.proc  to ptable.proc[NPROC]) and for each of them, return it in the RUNNABLE state if it is in the SLEEPING state and if its wait_slot variable istrue.
- release the lock table.lock.
very important question for the future
You should notice that these functions generalize the principle of waiting. sleep pause a process while waiting for an event on chan, and wakeup wakes up processes that are waiting for an event on chan. We could therefore have implemented our functions as follows:
In order to see the code of the solution, execute the following sequence of instructions in a terminal:
And to see only the added code: