System calls are one of the most fundamental concepts in operating systems, particularly in Linux. They form the interface between user space (where your applications run) and kernel space (where the operating system core manages resources such as memory, CPU, devices, and filesystems).
This tutorial provides a structured overview of what system calls are, why they exist, how they work and how you can interact with them directly.
1. What is a System Call?
A system call (syscall) is a controlled entry point through which user programs request services from the Linux kernel.
Because user applications are restricted from directly accessing hardware or kernel memory (to ensure stability and security), they must rely on system calls to perform privileged operations such as:
- Reading and writing files
- Creating and managing processes
- Allocating memory
- Communicating over the network
- Accessing devices
In short, system calls act as the API of the kernel.
2. User Space vs. Kernel Space
Modern operating systems enforce a separation between:
- User space: Where regular applications execute. Programs here cannot directly touch hardware or kernel data structures.
- Kernel space: Where the operating system core runs. It has full control over hardware and memory.
This separation protects the system: if a user application crashes, it does not take down the entire operating system.
When a system call is made, the CPU changes its mode from user mode to kernel mode, executes the requested operation, and then returns to user mode.
3. How System Calls Work
At a high level, the process looks like this:
- Application calls a wrapper function
Most programming languages don’t interact with system calls directly. Instead, the C standard library (glibc) provides wrapper functions. For example, when you call printf(), it eventually calls the write() system call. - System call instruction
The wrapper prepares the arguments, sets the system call number in a specific CPU register, and executes a CPU instruction such as syscall (x86_64) or svc (ARM). This instruction triggers a context switch to kernel mode. - Kernel execution
The kernel looks up the system call handler based on the syscall number, validates the arguments and performs the requested operation. - Return to user space
The kernel places the result in a register and switches back to user mode. The wrapper then returns control to the application.
4. Common Linux System Calls
Some of the most frequently used system calls include:
- File I/O:
- open() – open a file or device
- read() – read from a file descriptor
- write() – write to a file descriptor
- close() – close a file descriptor
- Process management:
- fork() – create a new process
- execve() – replace the current process with a new one
- wait() – wait for a child process to finish
- exit() – terminate a process
- Memory management:
- mmap() – map files or devices into memory
- brk() – adjust the end of the data segment
- Networking:
- socket() – create a network socket
- connect() – connect to a remote socket
- send() / recv() – send and receive data
5. System Call Numbers
Each system call is identified internally by a unique number. For example, on x86_64 Linux:
- read() has syscall number 0
- write() has syscall number 1
- open() has syscall number 2
These numbers can be found in the kernel source or in headers such as: /usr/include/asm/unistd_64.h
Since system call numbers differ between architectures (x86_64, ARM, RISC-V, etc.), it is strongly recommended to use symbolic constants instead of hardcoding numbers.
6. Using System Calls in C
Most of the time, you use system calls through their libc wrappers. For example:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0)
{
perror("open");
return 1;
}
const char *msg = "Hello, Linux system calls!\n";
if (write(fd, msg, 27) < 0)
{
perror("write");
}
close(fd);
return 0;
}
Here:
- open() calls the kernel to create or open a file.
- write() requests the kernel to write data to the file.
- close() tells the kernel to release the file descriptor.
7. Making Raw System Calls
You can also bypass libc and invoke system calls directly:
#include <unistd.h>
#include <sys/syscall.h>
int main()
{
const char msg[] = "Hello, raw syscall!\n";
syscall(SYS_write, 1, msg, sizeof(msg) - 1);
return 0;
}
Here:
- SYS_write is the system call number for write()
- 1 is the file descriptor for standard output
This method is rarely used in production software but is useful for low-level programming and debugging.
8. Error Handling
If a system call fails, it typically returns -1 and sets the global variable errno to indicate the error. Common errors include:
- EACCES – Permission denied
- ENOENT – File not found
- EBADF – Invalid file descriptor
- ENOMEM – Not enough memory
Example:
int fd = open("nonexistent.txt", O_RDONLY);
if (fd < 0)
{
perror("open failed");
}
9. Why System Calls Matter
System calls are critical because they:
- Provide isolation between applications and the kernel.
- Enforce security by controlling access to resources.
- Enable portability by abstracting hardware operations behind a consistent interface.
- Form the foundation of higher-level libraries and APIs.
In fact, nearly all programming activities on Linux, from opening a terminal window to sending data over the internet, rely on system calls.
10. TL/DR
- A system call is the mechanism for user-space programs to request services from the kernel.
- They enable safe interaction with hardware and protected resources.
- System calls are identified by numbers but typically accessed through libc wrappers.
- Direct invocation is possible using the syscall() function.
- Proper error handling is essential when working with system calls.
Understanding system calls is fundamental for systems programming, operating system design and debugging low-level software behavior in Linux.