Xv6-Lab-2
Operating System C LinuxIntroduction
Lab2 of MIT 6.S081 is about tracing system calls and resources monitoring in the xv6 kernel. The difficulty of this lab is moderate, but it reveals the boundaries between user space and kernel space and the process structure where process information is stored.
Trace System Calls
This lab requires student to print out the system call number, process id, system call name, and return value of the system call traced through the utility strace
. The child process should inherit the parent process’s tracing.
The solution is to add an extra line in the void syscall(void)
function when the system call returns, where you can get the return value and the system call number.
For the child process, you need to add a line in the fork()
function to copy the parent process’s tracing information to the child process.
I stored the mask in the proc
structure, which is a structure that stores process information.
System Observability
This task requires students to implement a system call to get current active process number and available memory size.
The idea is to traverse the process list and count the number of active processes. The available memory size can be calculated by traversing the free page table and counting the number of free pages.
This is the first time I’ve implemented a system call passing an address of the structure to the kernel to fetch the data. Previously I thought the kernel could directly access the user space, but it turns out that the kernel can only access the user space through the copyout
function. It will cost more time than directly accessing the user space, but it is more secure. The data is copied between the kernel stack and the user stack.
The following part of code is running on the kernel stack in kernel mode.
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
This is the copyout
function, which copies data from the kernel to the user space.
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
if(va0 >= MAXVA)
return -1;
Questions
- Looking at the backtrace, what is the name of the function that called
syscall
?
usertrap()
called syscall()
.
- What is the value of
p->trapframe->a7
and what does that value represent? (Hint: lookuser/initcode.S
, the first user program xv6 starts.)
SYS_exec
is the value of p->trapframe->a7
.
In the first call of syscall()
, the value of a7
is 7.
It is equals to SYS_exec
which is defined in syscall.h
as 7
.
Normally a7
this register is used to store the system call number.
- What was the previous mode that the CPU was in?
Lookup the sstatus
register in the RISC-V privileged instruction set manual.
4. Write down the assembly instruction the kernel is panicked at. Which register corresponds to the variable num?
- Why does the kernel crash? (Hint: look at figure 3-3 in the textbook; Is address 0 mapped in the kernel address space? Is that confirmed by the value in scause about? See description of scause in the RISC-V privileged instruction set manual.)
Because 0x0000000000000000
is not mapped in the kernel address space, the kernel crashes.
- What is the name of the binary that was running when the kernel panicked? What is its process id(pid)?
initcode
is the binary’s name, and its pid is 1.
Lecture Review: Organization and System Calls
Differences Between Monolithic Kernel (OS-intensive application) and Micro Kernel (Minix, L4, QNX)
These are two main kernel architecture models. The former emphasizes efficiency and performance, while the latter focuses on security and isolation.
In a monolithic architecture, all OS code runs in kernel mode, greatly reducing communication overhead for OS-intensive applications since system calls are not needed for communication. However, this leads to a bloated kernel codebase. The more code there is, the more bugs there can be, making this architecture more prone to bugs.
The micro kernel architecture, on the other hand, tries to keep kernel code to a minimum, implementing other OS services in user mode. While this simplifies the kernel’s complexity, it increases the frequency of system calls. When two OS components running in user mode need to communicate, they must use system calls like IPC, which is a significant overhead for performance-oriented and OS-intensive applications.
Major Differences Between OS and Embedded (or Real-Time) Systems
Embedded or real-time systems (e.g., using FPGA for high-speed market data services) allow programmers direct access to hardware, which is not permitted in an OS.
An OS abstracts hardware by providing system calls or virtual memory, ensuring system security (virtual memory is an abstraction of memory, and time-shared CPU is an abstraction of the CPU).
Typically, the mode of operation is controlled by a hardware register that cannot be modified by the user in the OS, only by the kernel. When crossing the OS boundary, the kernel checks the user’s permissions and the legality of system call parameters. If everything is in order, the system call is executed.
Three Main Responsibilities of an OS
- Multiplexing hardware
- Isolating processes from each other
- Facilitating interaction between user programs and the kernel, and between user programs themselves
RISC-V Architecture
RISC-V is an instruction set architecture(ISA) and a 64-bit processor.
It operates in three modes: machine mode, kernel mode, and user mode. Xv6 is an LP64C system, meaning long integers and pointers are 64 bits. RISC-V uses ecall
to switch to kernel mode.
Qemu is a virtualization platform used to emulate computer motherboards. Qemu, written in C, essentially runs a loop: reading, decoding, and executing RISC-V instructions.
It supports GDB debugger via a GDB stub, which users can connect to through a local window using the TCP protocol. This is a basic method for debugging kernels.
Compilation and Assembly Process
pro.c
–gcc–> pro.S
(RISC-V assembly) –as–> pro.o
(binary version of assembly) –ld–> pro
.asm
is the disassembly file of the kernel.
Process as the Basic Unit of Isolation in OS
Virtual memory plays a significant role here. The size of virtual memory is much smaller than the entire 64-bit address space; the actual addressable space in Xv6 is about 40 bits.
Typically, OS threads have two stacks: user stack and kernel stack, to prevent users from attacking the kernel via the stack. Apart from authentication and parameter validation, even the data is completely isolated. The OS makes considerable efforts to ensure isolation.