System call: How it works, why expensive, how to reduce them

We will be focusing on the x86 instruction set. A running program on CPU can be stopped under three conditions:

  1. It has finished execution

  2. There is a timer interrupt

  3. It wants to yield control to the OS because of a system call

When the program wants to make a system call, it issues a trap instruction, where the program moves from user mode to kernel mode (i.e., ring 3 to ring 0).

GS is a variable which in user mode, that points to thread-local storage, but in kernel mode it points to the per-CPU data, which is a special portion available per CPU to provide faster access to certain data. When a trap instruction is issued, swapgc happens. This per cpu block, also known as the pcpu_hot, has few variables such as user stack pointer and top of the stack of the kernel.

Data that is saved on the kernel stack after a trap instruction

Trap frame: So firstly, the program saves a trap frame, which provides information about which current instruction the program is running in user mode. Other system call variables, such as rax, which stores the system call int, then rsi, rdi, rdx, r10, r9, r8, these variables are saved on the kernel stack.

RIP: Then the current instruction pointer is saved, rip.

Then the callee information is saved, and we can begin switching to the kernel stack. Currently, user code is executing; now we will execute on the kernel stack. For this, the current value of the user rsp is stored in the gs user stack pointer. Then the rsp is switched to the top of the kernel stack, and then at that end the value of the GS stack user stack pointer is pushed on the top of the kernel stack.

Things that are expensive in context of system call.

Reducing no of system calls

System call batching, multiple reads and writes into a single system call, so that we can reduce the number of context switches. Another is using IO_URING.

Another thing is that we can totally skip hardware and just use zero-cost copying using mmap, which maps the file into the address space of the process.

Caching system call result for system call where the result doesn’t change much.

Moving executable code under the kernel mode so that less context switching is needed. We can just get the result directly from the kernel code execution with epbf.

Use vDSO: For calls likeĀ clock_gettime, prefer the vDSO path to avoid kernel entry.