In the operating system, there are three situations that will cause the CONTROL flow of the CPU to shift: in the user mode, the CPU enters the kernel mode through the ECall instruction; Exceptions occur, such as dividing by zero and accessing invalid addresses; The device is interrupted, for example, the hard disk completes the read/write request. These situations can be collectively referred to as traps.

Traps should be transparent in the normal case, that is, when the handler is executed, the state of the previous program can be restored. This requires the kernel to save previous state information, such as registers, when it falls into kernel state and then restore it after executing the handler.

There are four steps to handling a trap in XV6: the CPU does the hardware operation, the assembly vector is set, the C trap handler decides how to handle it, and the system call or device driver handles the trap. The kernel typically handles these traps in three different ways: user-mode traps, kernel-mode traps, and clock interrupts.

The RISC-V CPU has a series of control registers to determine how to handle traps, which are set by the kernel.

  • stvec: Trap handler entry, where the CPU jumps to handle traps
  • sepc: Save the trap when it happenedpc, the use ofsretInstructions will bepcrestore
  • scause: Trap cause
  • sscratchThe kernel stores specific values, as shown below
  • sstatus:sstatusIn theSIEWhether the bit control interrupt is allowed;SPPBits indicate whether the trap comes from user mode or regulatory mode.

When a trap occurs, the hardware does the following:

  1. If the device is disconnected andSIEIf it is empty, it does not respond
  2. emptySIETo close the interrupt
  3. savepctosepc
  4. Save the current mode toSPP
  5. Set up thescause
  6. Switch to supervisory mode
  7. copystvectopc
  8. Start executing the handler

The hardware does not automatically switch between the kernel page table and the kernel stack, nor does it save registers other than the PC, and the handler must do this. This design gives the software greater flexibility. Setting up the PC must be done by the hardware, because user instructions can break isolation when switching to kernel mode.

User-mode trap

The process for handling user mode traps in XV6 is as follows: uservec > usertrap > userTrapret > userret.

Since the CPU does not perform page table switching, the user page table must contain a mapping of the uservec function (the function to which STvec points). This function switches satP to the kernel page table, and must have the same address in the user and kernel page tables in order for the switched instruction to continue. To meet the above requirements, XV6 maps a page named Trampoline to the same virtual address trampoline, which contains the instruction trampoline.s, and sets stvec to uservec.

uservec

On entering the uservec function, all 32 registers are owned by the interrupt code, and uservec needs registers to execute instructions. Therefore, RISC-V provides the Sscratch register, which saves A0 by CSRRW A0, sscratch, A0 instruction, Then you can use the A0 register.

After that, the function needs to save all the user registers into the TrapFrame structure. The address of this structure is stored in the SScratch register before entering user mode, so after the previous CSRRW operation, it is stored in A0. When creating a process, the kernel applies for a page to hold the trapFrame, which is located just below the TRAMPOLINE, and the process’s P -> TrapFrame also points to that page.

Finally, the function retrieves the kernel stack address, hartid, usertrap address, and kernel page table address from trapFrame, switches the page table, and jumps to usertrap function.

usertrap

The job of userTrap is to determine the trap type, process it, and return it. The kernelvec function first sets stvec to kernelvec’s address, leaving kernelvec functions to handle kernelvec interrupts. Then save the SEPC register to prevent it from being overwritten. Then determine the trap type, if it is a system call, the PC points to the next ecall instruction, and then to the Syscall function processing; If it’s a device interrupt, hand it over to DevIntr; Otherwise it is an exception and the process is terminated. At the end it determines whether the process has been killed or relinquish the processor when a clock interrupt occurs.

usertrapret

This function first sets stvec to the address of uservec, then sets trapFrame (which is used in Uservec), and then restores the SEPC registers. Finally, the userret function is called.

Finally, the page table and registers are restored in the userret function in the reverse step of uservec.

The system calls

Take the system call in initcode.s as an example, place the two parameters in register A0 A1 and the system call number in register A7, and then execute the ecall instruction.

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall
Copy the code

The syscall function retrieves the value of a7, searches the syscalls array, finds the corresponding handler (sys_exec), and puts the return value in trapFrame -> A0.

Kernel state trap

The kernel-trap processing path is: Kernelvec -> Kerneltrap -> Kernelvec

kernelvec

Since the trap occurs in kernel state, there is no need to deal with SATP and stack pointer, just save all general purpose registers. Then jump to KernelTrap for processing, when the function returns, and then restore the saved register.

kerneltrap

Kerneltrap needs to handle only two kinds of traps: device interrupts and exceptions. Call devintr to determine whether it is a device interrupt. If it is not a device interrupt, then it is an exception, and the exception occurs in kernel state. The kernel calls panic to terminate execution. If it’s a clock interrupt, let the processor go. Since the yield function causes the SEPC sSTATUS register to be modified, it is saved and restored in kernelTrap.

Missing page exception

In XV6, exceptions are not handled; they are simply killed or panic. In real operating systems, exceptions are handled in detail. For example, copy on Write (COW) fork is implemented using page missing exceptions.

In RISC-V, there are three different types of page missing exceptions: Load page faults (which occur when load instructions convert virtual addresses), Store Page faults (which occur when store instructions convert virtual addresses), and Instruction Page faults (which occur when instruction addresses convert virtual addresses). The cause of the exception is stored in the Scause register, and the address where the conversion failed is stored in stval.

COW fork gives the child the same physical page as the parent, but makes it read-only. When the child or parent executes the store instruction, an exception is raised, the page is copied, and mapped in read-write mode to the parent’s address space.

Another technique is lazy allocation. When an application calls SBRK, it increases the address space but marks the new address in the page table as invalid. The physical page is actually assigned to the process when a page miss exception occurs at the new address.

In paging from Disk, the operating system saves a portion of the page to the disk and marks the page entry as invalid. In paging from disk, the operating system will retrieve the memory from the disk when reading or writing the page. In addition, other techniques such as Automatically extending stacks and memory-mapped files also use missing page exceptions.