Overview
Every time a program on Linux does something that touches the outside world, reading a file, allocating memory, sending a packet, spawning another process, it eventually has to ask the kernel to do it. User space code can’t just reach into the kernel’s data structures and start editing process tables or filesystem metadata; that would be a security and stability nightmare. Instead, it has to go through a narrow, well-defined door: the system call interface. This post is a reference table for that door on x86-64 Linux, listing every syscall number, its libc-facing name, its man page, and the kernel function that actually handles it once your program’s request lands.
The data below comes straight from arch/x86/entry/syscalls/syscall_64.tbl and the various syscalls.h headers in the Linux 6.7 kernel source. That pinning to a specific version matters more than it might seem. Syscall numbers are basically a permanent contract: once a number ships in a released kernel, it can never be reassigned to something else, because that would silently break every binary that was compiled expecting the old meaning. Numbers can be retired (their Entry Point becomes a stub that just returns -ENOSYS), but they don’t get reused. So this table is less like a snapshot of “what Linux can do today” and more like an append-only ledger of every syscall Linux has ever shipped on this architecture, plus a few epitaphs for the ones that got removed along the way.
From C function call to kernel trap
On x86-64, a syscall isn’t a normal function call. The CPU has to actually change privilege levels, from ring 3 (user space) to ring 0 (kernel space), and that transition is handled by a dedicated instruction: syscall. Before executing it, the calling convention says you load the syscall number into %rax, and up to six arguments into %rdi, %rsi, %rdx, %r10, %r8, and %r9, in that order. The return value comes back in %rax.
If you’ve written normal x86-64 C code you’ll notice that argument list looks almost, but not quite, like the standard System V calling convention, which uses %rdi, %rsi, %rdx, %rcx, %r8, %r9. The odd one out is the fourth argument: syscalls use %r10 where a regular function call would use %rcx. That’s not an arbitrary choice, it’s forced by the hardware. The syscall instruction itself clobbers %rcx (it stows the return address there) and %r11 (it stows the flags register there), so the kernel entry code can’t trust whatever was in %rcx when the trap happened. Glibc’s syscall wrappers quietly shuffle the fourth argument into %r10 for you, which is why almost nobody who isn’t writing raw assembly or using syscall(2) directly ever notices this wrinkle.
Once the trap lands, the kernel looks up %rax in exactly the table you’re looking at on this page, jumps to the matching entry point, does its work with full kernel privileges, and returns. The %rax value on your side then flips meaning: negative values in the range of errno codes mean failure (glibc translates -EACCES into errno = EACCES and a return value of -1), non-negative values mean success. This is also why raw syscalls and their libc wrappers can disagree about return conventions; the wrapper is doing translation work on your behalf.
How the table in this post gets built
Every row in syscall_64.tbl is a tab-separated line: number, ABI (common, 64, or x32), name, and the C entry point symbol. A build-time script turns that file into generated headers, and the entry points themselves are defined with the SYSCALL_DEFINEx family of macros (SYSCALL_DEFINE0 for a syscall with no arguments, up to SYSCALL_DEFINE6). These aren’t just syntactic sugar for writing a normal function. They also handle 32-bit compatibility name mangling, do compile-time type checking on the arguments, and emit metadata that feeds the kernel’s own tracing infrastructure, which is part of how tools like ftrace’s syscall tracepoints and perf trace can print syscall arguments by name instead of as raw, meaningless integers. If you want the canonical, maintainer-approved walkthrough of what it takes to add a brand new syscall today (and there’s a fair amount of ABI-compatibility ceremony involved), the kernel source ships Documentation/process/adding-syscalls.rst, and it’s worth reading even if you never plan to submit one yourself, just to understand how conservative the kernel is about this interface.
Reading the “Entry Point” column
Most rows point to a real function, usually named sys_<something>. A handful point to sys_ni_syscall or are shown here as a bare dash. That’s the kernel’s way of saying “this number exists, but nothing is behind it on this architecture.” sys_ni_syscall (“no implementation”) just returns -ENOSYS and moves on. You’ll see this for syscalls that were designed, given a number, and then abandoned before merge (create_module, get_kernel_syms, query_module, all leftovers from the old dynamically-loadable-module experiments of the mid-90s), for ones that only ever made sense on other architectures, and for genuinely dead protocols like afs_syscall, security, and tuxcall, which trace back to features (AFS filesystem hooks, a security-module framework that never shipped, the old TUX in-kernel web server) that Linux tried and dropped. The number stays reserved forever anyway, both to avoid an ABI collision and honestly as a bit of a historical fossil record.
One more thing worth flagging before diving into the tables: not every syscall you’ll see used in practice actually makes the trip into the kernel every time it’s called. A small set of extremely hot, read-only-ish calls, gettimeofday, clock_gettime, getcpu, time, are backed by the vDSO (virtual dynamic shared object), a tiny shared library the kernel maps into every process’s address space. The vDSO version reads shared kernel-maintained data directly in user space and skips the trap into ring 0 entirely on the common path, only falling back to the real syscall (the one listed in this table) if something unusual is going on. That’s why clock_gettime can be called millions of times a second in a hot loop without visibly showing up as syscall overhead in a profiler like strace -c; most of the time it never becomes a syscall at all.
The syscall tables
0-99: the fundamentals
This first block is the bread and butter of Unix programming, and if you squint, you can see the shape of a whole operating system philosophy in these hundred numbers. File I/O comes first (read, write, open, close, the stat family), because “everything is a file descriptor” is close to Unix’s founding idea. Memory management follows (mmap, mprotect, munmap, brk), because a process’s address space is itself just another resource the kernel hands out and reclaims.
A few entries deserve a closer look. mmap here is implemented by sys_ksys_mmap_pgoff, and the “pgoff” in that name is a real detail: the raw syscall takes its file offset argument in units of pages, not bytes, so glibc’s mmap() wrapper quietly right-shifts the byte offset you pass it before making the call. Get that shift wrong in hand-rolled assembly and you’ll map the wrong part of a file without any error at all.
Process creation is where things get genuinely interesting. You’ll see three separate syscalls here that all “create a new process”: fork, vfork, and clone. They’re not three unrelated features, they’re three points on the same spectrum, and understanding the spectrum explains a lot about how Linux processes and threads actually relate to each other. fork duplicates the calling process, giving the child its own copy-on-write address space, its own file descriptor table (sharing the underlying open file descriptions, but with independent descriptor numbers), and so on; nothing is actually copied at the moment of the call, pages are just marked read-only and shared until either process writes to one, at which point the kernel splits them apart. vfork is an older, narrower optimization for the extremely common “fork, then immediately exec” pattern: it hands the child the parent’s actual address space (no copying, not even copy-on-write bookkeeping) and suspends the parent until the child either execs or exits, on the assumption that the child won’t touch memory in a way that matters before it replaces itself. It’s a sharp tool with real footguns if that assumption is violated, which is exactly why posix_spawn and clone(CLONE_VM | CLONE_VFORK) exist as safer, more explicit ways to get the same performance win. clone is the general-purpose primitive underneath both of them: it takes a bitmask of flags saying exactly what the new task should share with its parent, address space, file descriptor table, signal handlers, filesystem info, and so on. Crucially, this is also how Linux implements threads. A “thread” in the POSIX sense is, at the kernel level, just a task created via clone with CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND (roughly speaking), meaning it shares almost everything with its “parent” except its own stack and register state. Linux doesn’t have a separate thread abstraction baked into the scheduler; threads and processes are the same kind of thing (a task_struct) with different sharing flags.
The signal-related entries here, rt_sigaction, rt_sigprocmask, rt_sigreturn, are worth a second look too, because rt_sigreturn is a syscall almost nobody calls on purpose. When the kernel delivers a signal to your process, it doesn’t just jump to your handler and hope for the best; it first saves your interrupted register state onto your stack and pushes a tiny trampoline whose entire job is to call rt_sigreturn once your handler returns, so the kernel can restore exactly the state you were in before the signal arrived. If that trampoline or the saved state gets corrupted (a classic stack-smashing exploitation technique in the pre-mitigation era), rt_sigreturn is the syscall that gets abused to splice in attacker-controlled register state, which is part of why modern exploit mitigations pay it special attention.
Finally, notice the network syscalls in the 41-55 range: socket, connect, accept, sendto, recvfrom, and friends each get their own dedicated number here. That’s actually an x86-64-specific design choice. On 32-bit x86, all of networking is squeezed through a single multiplexed syscall, socketcall(2), where the first argument is an opcode saying which “real” operation you meant and the rest is a pointer to a packed argument array. That design saved syscall table entries back when 32-bit Linux only had a few hundred slots to work with, but it adds a layer of indirection and makes tracing and auditing individual operations harder. When the 64-bit ABI was designed, there was no legacy to preserve on this front, so each network operation simply got its own first-class number, cleaner and slightly faster to dispatch.
| %rax | Name | Manual | Entry Point |
|---|---|---|---|
| 0 | read | read(2) | sys_read |
| 1 | write | write(2) | sys_write |
| 2 | open | open(2) | sys_open |
| 3 | close | close(2) | sys_close |
| 4 | stat | stat(2) | sys_newstat |
| 5 | fstat | fstat(2) | sys_newfstat |
| 6 | lstat | lstat(2) | sys_newlstat |
| 7 | poll | poll(2) | sys_poll |
| 8 | lseek | lseek(2) | sys_lseek |
| 9 | mmap | mmap(2) | sys_ksys_mmap_pgoff |
| 10 | mprotect | mprotect(2) | sys_mprotect |
| 11 | munmap | munmap(2) | sys_munmap |
| 12 | brk | brk(2) | sys_brk |
| 13 | rt_sigaction | rt_sigaction(2) | sys_rt_sigaction |
| 14 | rt_sigprocmask | rt_sigprocmask(2) | sys_rt_sigprocmask |
| 15 | rt_sigreturn | rt_sigreturn(2) | sys_rt_sigreturn |
| 16 | ioctl | ioctl(2) | sys_ioctl |
| 17 | pread64 | pread64(2) | sys_pread64 |
| 18 | pwrite64 | pwrite64(2) | sys_pwrite64 |
| 19 | readv | readv(2) | sys_readv |
| 20 | writev | writev(2) | sys_writev |
| 21 | access | access(2) | sys_access |
| 22 | pipe | pipe(2) | sys_pipe |
| 23 | select | select(2) | sys_select |
| 24 | sched_yield | sched_yield(2) | sys_sched_yield |
| 25 | mremap | mremap(2) | sys_mremap |
| 26 | msync | msync(2) | sys_msync |
| 27 | mincore | mincore(2) | sys_mincore |
| 28 | madvise | madvise(2) | sys_madvise |
| 29 | shmget | shmget(2) | sys_shmget |
| 30 | shmat | shmat(2) | sys_shmat |
| 31 | shmctl | shmctl(2) | sys_shmctl |
| 32 | dup | dup(2) | sys_dup |
| 33 | dup2 | dup2(2) | sys_dup2 |
| 34 | pause | pause(2) | sys_pause |
| 35 | nanosleep | nanosleep(2) | sys_nanosleep |
| 36 | getitimer | getitimer(2) | sys_getitimer |
| 37 | alarm | alarm(2) | sys_alarm |
| 38 | setitimer | setitimer(2) | sys_setitimer |
| 39 | getpid | getpid(2) | sys_getpid |
| 40 | sendfile | sendfile(2) | sys_sendfile64 |
| 41 | socket | socket(2) | sys_socket |
| 42 | connect | connect(2) | sys_connect |
| 43 | accept | accept(2) | sys_accept |
| 44 | sendto | sendto(2) | sys_sendto |
| 45 | recvfrom | recvfrom(2) | sys_recvfrom |
| 46 | sendmsg | sendmsg(2) | sys_sendmsg |
| 47 | recvmsg | recvmsg(2) | sys_recvmsg |
| 48 | shutdown | shutdown(2) | sys_shutdown |
| 49 | bind | bind(2) | sys_bind |
| 50 | listen | listen(2) | sys_listen |
| 51 | getsockname | getsockname(2) | sys_getsockname |
| 52 | getpeername | getpeername(2) | sys_getpeername |
| 53 | socketpair | socketpair(2) | sys_socketpair |
| 54 | setsockopt | setsockopt(2) | sys_setsockopt |
| 55 | getsockopt | getsockopt(2) | sys_getsockopt |
| 56 | clone | clone(2) | sys_clone |
| 57 | fork | fork(2) | sys_fork |
| 58 | vfork | vfork(2) | sys_vfork |
| 59 | execve | execve(2) | sys_execve |
| 60 | exit | exit(2) | sys_exit |
| 61 | wait4 | wait4(2) | sys_wait4 |
| 62 | kill | kill(2) | sys_kill |
| 63 | uname | uname(2) | sys_newuname |
| 64 | semget | semget(2) | sys_semget |
| 65 | semop | semop(2) | sys_semop |
| 66 | semctl | semctl(2) | sys_semctl |
| 67 | shmdt | shmdt(2) | sys_shmdt |
| 68 | msgget | msgget(2) | sys_msgget |
| 69 | msgsnd | msgsnd(2) | sys_msgsnd |
| 70 | msgrcv | msgrcv(2) | sys_msgrcv |
| 71 | msgctl | msgctl(2) | sys_msgctl |
| 72 | fcntl | fcntl(2) | sys_fcntl |
| 73 | flock | flock(2) | sys_flock |
| 74 | fsync | fsync(2) | sys_fsync |
| 75 | fdatasync | fdatasync(2) | sys_fdatasync |
| 76 | truncate | truncate(2) | sys_truncate |
| 77 | ftruncate | ftruncate(2) | sys_ftruncate |
| 78 | getdents | getdents(2) | sys_getdents |
| 79 | getcwd | getcwd(2) | sys_getcwd |
| 80 | chdir | chdir(2) | sys_chdir |
| 81 | fchdir | fchdir(2) | sys_fchdir |
| 82 | rename | rename(2) | sys_rename |
| 83 | mkdir | mkdir(2) | sys_mkdir |
| 84 | rmdir | rmdir(2) | sys_rmdir |
| 85 | creat | creat(2) | sys_creat |
| 86 | link | link(2) | sys_link |
| 87 | unlink | unlink(2) | sys_unlink |
| 88 | symlink | symlink(2) | sys_symlink |
| 89 | readlink | readlink(2) | sys_readlink |
| 90 | chmod | chmod(2) | sys_chmod |
| 91 | fchmod | fchmod(2) | sys_fchmod |
| 92 | chown | chown(2) | sys_chown |
| 93 | fchown | fchown(2) | sys_fchown |
| 94 | lchown | lchown(2) | sys_lchown |
| 95 | umask | umask(2) | sys_umask |
| 96 | gettimeofday | gettimeofday(2) | sys_gettimeofday |
| 97 | getrlimit | getrlimit(2) | sys_getrlimit |
| 98 | getrusage | getrusage(2) | sys_getrusage |
| 99 | sysinfo | sysinfo(2) | sys_sysinfo |
100-199: identity, scheduling, and system administration
The next hundred numbers are less about moving bytes and more about who you are and what you’re allowed to do. The getuid/setuid/geteuid/getegid cluster and its “resXid” cousins (setresuid, getresuid, setresgid, getresgid) implement the real, uid, and effective/saved-uid model that setuid binaries rely on: a program can temporarily drop or reclaim privilege by juggling these three separate uid slots, which is the mechanism behind everything from sudo to ping needing raw sockets without running as root the whole time. capget and capset sit right next to them and represent a newer, finer-grained answer to the same problem: instead of the binary “root or not” split, POSIX capabilities break root’s power into dozens of discrete bits (CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, and so on), so a process, or a container, can be handed exactly the sliver of privilege it needs and nothing more. This is the plumbing underneath Docker’s --cap-drop/--cap-add flags.
setpgid, getpgrp, and setsid build the process-group and session hierarchy that makes job control possible: Ctrl+C in your terminal doesn’t know or care about individual PIDs, it sends SIGINT to a whole foreground process group at once, and that grouping is exactly what these calls set up. sigaltstack is a small but clever one: it lets a process register a separate, dedicated stack for signal handlers to run on. The reason that matters is a genuinely nasty edge case: if your normal stack overflows, the kernel wants to deliver SIGSEGV to your handler, but if the handler also runs on the already-overflowed stack, it will immediately fault again trying to push its own frame. An alternate signal stack, set up ahead of time, sidesteps that entirely, and it’s how tools that need to reliably catch stack overflows (Go’s runtime, for instance) manage to do so.
The sched_* family (sched_setscheduler, sched_getparam, sched_rr_get_interval, and so on) controls which scheduling class a task lives in. Most processes run under Linux’s default fair scheduler (CFS at the time this table was generated, since replaced by EEVDF in newer kernels), but these syscalls let a privileged process opt into real-time classes like SCHED_FIFO or SCHED_RR, which get strict priority-based, preempt-almost-everything scheduling instead of fair time-sharing. That’s the kind of thing audio production software and industrial control loops need and almost nothing else should touch, because a buggy real-time process can genuinely starve the rest of the system.
A few entries here are worth reading as history lessons. _sysctl (156) is stubbed out to sys_ni_syscall in modern kernels; the syscall-based interface to kernel tunables was superseded by the /proc/sys filesystem interface decades ago, and the number was retired rather than reused. create_module, get_kernel_syms, and query_module (174, 177, 178) are dead for a similar reason: they belonged to an early, clunky kernel module-loading API from the mid-90s that was replaced by init_module and delete_module (which are very much alive at 175 and 176) well before Linux 6.7. nfsservctl, getpmsg, putpmsg, afs_syscall, tuxcall, and security (180-185) are similarly abandoned: an in-kernel NFS server control interface, STREAMS message-passing (never really at home on Linux), the Andrew File System, an in-kernel HTTP server, and a security-module hook framework that got replaced by the Linux Security Module (LSM) infrastructure you’ll find used by SELinux and AppArmor today. The numbers are permanent tombstones for all of them.
Rounding out the block, the extended attribute calls (setxattr through fremovexattr, 188-199) let arbitrary key/value metadata be attached to a file beyond the standard permission bits. This sounds like a minor bookkeeping feature until you realize it’s the storage layer for POSIX ACLs, SELinux security labels, and capabilities stored on binaries (setcap), so a filesystem that doesn’t support extended attributes well (some network filesystems historically didn’t) quietly loses access to a chunk of Linux’s security model.
| %rax | Name | Manual | Entry Point |
|---|---|---|---|
| 100 | times | times(2) | sys_times |
| 101 | ptrace | ptrace(2) | sys_ptrace |
| 102 | getuid | getuid(2) | sys_getuid |
| 103 | syslog | syslog(2) | sys_syslog |
| 104 | getgid | getgid(2) | sys_getgid |
| 105 | setuid | setuid(2) | sys_setuid |
| 106 | setgid | setgid(2) | sys_setgid |
| 107 | geteuid | geteuid(2) | sys_geteuid |
| 108 | getegid | getegid(2) | sys_getegid |
| 109 | setpgid | setpgid(2) | sys_setpgid |
| 110 | getppid | getppid(2) | sys_getppid |
| 111 | getpgrp | getpgrp(2) | sys_getpgrp |
| 112 | setsid | setsid(2) | sys_setsid |
| 113 | setreuid | setreuid(2) | sys_setreuid |
| 114 | setregid | setregid(2) | sys_setregid |
| 115 | getgroups | getgroups(2) | sys_getgroups |
| 116 | setgroups | setgroups(2) | sys_setgroups |
| 117 | setresuid | setresuid(2) | sys_setresuid |
| 118 | getresuid | getresuid(2) | sys_getresuid |
| 119 | setresgid | setresgid(2) | sys_setresgid |
| 120 | getresgid | getresgid(2) | sys_getresgid |
| 121 | getpgid | getpgid(2) | sys_getpgid |
| 122 | setfsuid | setfsuid(2) | sys_setfsuid |
| 123 | setfsgid | setfsgid(2) | sys_setfsgid |
| 124 | getsid | getsid(2) | sys_getsid |
| 125 | capget | capget(2) | sys_capget |
| 126 | capset | capset(2) | sys_capset |
| 127 | rt_sigpending | rt_sigpending(2) | sys_rt_sigpending |
| 128 | rt_sigtimedwait | rt_sigtimedwait(2) | sys_rt_sigtimedwait |
| 129 | rt_sigqueueinfo | rt_sigqueueinfo(2) | sys_rt_sigqueueinfo |
| 130 | rt_sigsuspend | rt_sigsuspend(2) | sys_rt_sigsuspend |
| 131 | sigaltstack | sigaltstack(2) | sys_sigaltstack |
| 132 | utime | utime(2) | sys_utime |
| 133 | mknod | mknod(2) | sys_mknod |
| 134 | uselib | uselib(2) | - |
| 135 | personality | personality(2) | sys_personality |
| 136 | ustat | ustat(2) | sys_ustat |
| 137 | statfs | statfs(2) | sys_statfs |
| 138 | fstatfs | fstatfs(2) | sys_fstatfs |
| 139 | sysfs | sysfs(2) | sys_sysfs |
| 140 | getpriority | getpriority(2) | sys_getpriority |
| 141 | setpriority | setpriority(2) | sys_setpriority |
| 142 | sched_setparam | sched_setparam(2) | sys_sched_setparam |
| 143 | sched_getparam | sched_getparam(2) | sys_sched_getparam |
| 144 | sched_setscheduler | sched_setscheduler(2) | sys_sched_setscheduler |
| 145 | sched_getscheduler | sched_getscheduler(2) | sys_sched_getscheduler |
| 146 | sched_get_priority_max | sched_get_priority_max(2) | sys_sched_get_priority_max |
| 147 | sched_get_priority_min | sched_get_priority_min(2) | sys_sched_get_priority_min |
| 148 | sched_rr_get_interval | sched_rr_get_interval(2) | sys_sched_rr_get_interval |
| 149 | mlock | mlock(2) | sys_mlock |
| 150 | munlock | munlock(2) | sys_munlock |
| 151 | mlockall | mlockall(2) | sys_mlockall |
| 152 | munlockall | munlockall(2) | sys_munlockall |
| 153 | vhangup | vhangup(2) | sys_vhangup |
| 154 | modify_ldt | modify_ldt(2) | sys_modify_ldt |
| 155 | pivot_root | pivot_root(2) | sys_pivot_root |
| 156 | _sysctl | _sysctl(2) | sys_ni_syscall |
| 157 | prctl | prctl(2) | sys_prctl |
| 158 | arch_prctl | arch_prctl(2) | sys_arch_prctl |
| 159 | adjtimex | adjtimex(2) | sys_adjtimex |
| 160 | setrlimit | setrlimit(2) | sys_setrlimit |
| 161 | chroot | chroot(2) | sys_chroot |
| 162 | sync | sync(2) | sys_sync |
| 163 | acct | acct(2) | sys_acct |
| 164 | settimeofday | settimeofday(2) | sys_settimeofday |
| 165 | mount | mount(2) | sys_mount |
| 166 | umount2 | umount2(2) | sys_umount |
| 167 | swapon | swapon(2) | sys_swapon |
| 168 | swapoff | swapoff(2) | sys_swapoff |
| 169 | reboot | reboot(2) | sys_reboot |
| 170 | sethostname | sethostname(2) | sys_sethostname |
| 171 | setdomainname | setdomainname(2) | sys_setdomainname |
| 172 | iopl | iopl(2) | sys_iopl |
| 173 | ioperm | ioperm(2) | sys_ioperm |
| 174 | create_module | create_module(2) | - |
| 175 | init_module | init_module(2) | sys_init_module |
| 176 | delete_module | delete_module(2) | sys_delete_module |
| 177 | get_kernel_syms | get_kernel_syms(2) | - |
| 178 | query_module | query_module(2) | - |
| 179 | quotactl | quotactl(2) | sys_quotactl |
| 180 | nfsservctl | nfsservctl(2) | - |
| 181 | getpmsg | getpmsg(2) | - |
| 182 | putpmsg | putpmsg(2) | - |
| 183 | afs_syscall | afs_syscall(2) | - |
| 184 | tuxcall | tuxcall(2) | - |
| 185 | security | security(2) | - |
| 186 | gettid | gettid(2) | sys_gettid |
| 187 | readahead | readahead(2) | sys_readahead |
| 188 | setxattr | setxattr(2) | sys_setxattr |
| 189 | lsetxattr | lsetxattr(2) | sys_lsetxattr |
| 190 | fsetxattr | fsetxattr(2) | sys_fsetxattr |
| 191 | getxattr | getxattr(2) | sys_getxattr |
| 192 | lgetxattr | lgetxattr(2) | sys_lgetxattr |
| 193 | fgetxattr | fgetxattr(2) | sys_fgetxattr |
| 194 | listxattr | listxattr(2) | sys_listxattr |
| 195 | llistxattr | llistxattr(2) | sys_llistxattr |
| 196 | flistxattr | flistxattr(2) | sys_flistxattr |
| 197 | removexattr | removexattr(2) | sys_removexattr |
| 198 | lremovexattr | lremovexattr(2) | sys_lremovexattr |
| 199 | fremovexattr | fremovexattr(2) | sys_fremovexattr |
200-299: futexes, timers, and event-driven I/O
This block is where a lot of “how does concurrency actually work on Linux” questions get answered. futex (202) is the single most important syscall almost nobody calls directly. It stands for “fast userspace mutex,” and the “fast” part is the whole point: acquiring an uncontended lock never touches the kernel at all. A mutex implementation like glibc’s pthread_mutex_t does its locking and unlocking with atomic compare-and-swap instructions entirely in user space, and only calls the futex syscall when it actually needs to sleep (because the lock is held by someone else) or wake another thread (because it just released a lock someone was waiting on). So in the overwhelmingly common uncontended case, “using a mutex” costs a couple of atomic CPU instructions and zero syscalls; the syscall only shows up under real contention, which is exactly when you want the kernel’s help arbitrating who runs next.
The io_setup/io_submit/io_getevents/io_cancel/io_destroy cluster (206-210) is the original Linux AIO interface, and it’s a useful lesson in API design tradeoffs. It works, but it’s notoriously narrow in practice: it plays nicely with O_DIRECT file I/O and not much else, so it never became the general-purpose async I/O story Linux needed. That gap is a big part of why io_uring exists (more on that later in this post), and Linux AIO stuck around mostly for backward compatibility with the software that already depends on it.
epoll_create, epoll_ctl, and epoll_wait (213, 233, 232) are the mechanism that made Linux comfortable hosting servers with tens of thousands of open connections at once. select and poll, back in the 0-99 block, both require the kernel to walk the entire list of file descriptors you’re interested in on every single call, which is fine for a handful of sockets and a real bottleneck at scale, an O(n) cost paid over and over. epoll flips that around: you register your interest once with epoll_ctl, and the kernel maintains a ready list that epoll_wait can drain in time proportional to the number of descriptors that actually have something to report, not the number you’re watching. That difference is the entire reason event loops in nginx, Node.js, and just about every other high-concurrency network server on Linux are built on epoll rather than poll.
exit_group (231) deserves a callout because it’s a genuine gotcha for anyone poking at raw syscalls. There are two ways to terminate on Linux: exit (60, back in the first block) kills only the calling thread, leaving the rest of the process’s threads running. exit_group kills every thread in the whole thread group, i.e., the entire process. When you call the C library’s exit(), it invokes exit_group under the hood, not the syscall that happens to share its name. If you ever hand-write a “hello world” in raw x86-64 assembly and reach for syscall number 60 expecting a clean process exit, in a multi-threaded context it won’t behave the way you’d expect; exit_group is almost always the one you actually want.
A few others worth knowing by name rather than just by number: mbind, set_mempolicy, and get_mempolicy (237-239) control NUMA memory placement policy, letting a process or allocation prefer memory attached to a particular CPU socket, which matters a lot on big multi-socket servers where “far” memory can be meaningfully slower to access than “near” memory. The mq_* family (240-245) implements POSIX message queues, a different, priority-ordered IPC mechanism from the older SysV msgget/msgsnd/msgrcv calls you saw in the first block. add_key, request_key, and keyctl (248-250) are the interface to the in-kernel keyring, a secure place to cache credentials and cryptographic material that’s used by things like NFSv4’s Kerberos authentication and disk encryption key handling, so sensitive material doesn’t have to live in ordinary, swappable process memory. And inotify_init, inotify_add_watch, and inotify_rm_watch (253-255) are the filesystem change-notification API that every file watcher, from systemd to your editor’s “reload on external change” feature, is ultimately built on.
You’ll notice this table jumps straight from 280 to 300, skipping numbers 281 through 299. That’s not a real gap in the kernel’s own numbering, it’s this reference skipping a stretch that’s mostly incremental siblings of syscalls covered elsewhere: epoll_pwait, signalfd, timerfd_create/timerfd_settime/timerfd_gettime, eventfd, fallocate, accept4, signalfd4, eventfd2, epoll_create1, dup3, pipe2, inotify_init1, preadv, pwritev, rt_tgsigqueueinfo, perf_event_open, and recvmmsg. A lot of that stretch is “the same syscall, but atomic” (dup3, pipe2, and accept4 all add flag arguments, most notably O_CLOEXEC, so you can set the close-on-exec bit at creation time instead of in a separate fcntl call, closing a real race window in multi-threaded programs that fork). perf_event_open is the odd one out in that list and worth remembering on its own: it’s the single syscall behind the entire perf profiling toolchain and hardware performance counters on Linux.
| %rax | Name | Manual | Entry Point |
|---|---|---|---|
| 200 | tkill | tkill(2) | sys_tkill |
| 201 | time | time(2) | sys_time |
| 202 | futex | futex(2) | sys_futex |
| 203 | sched_setaffinity | sched_setaffinity(2) | sys_sched_setaffinity |
| 204 | sched_getaffinity | sched_getaffinity(2) | sys_sched_getaffinity |
| 205 | set_thread_area | set_thread_area(2) | - |
| 206 | io_setup | io_setup(2) | sys_io_setup |
| 207 | io_destroy | io_destroy(2) | sys_io_destroy |
| 208 | io_getevents | io_getevents(2) | sys_io_getevents |
| 209 | io_submit | io_submit(2) | sys_io_submit |
| 210 | io_cancel | io_cancel(2) | sys_io_cancel |
| 211 | get_thread_area | get_thread_area(2) | - |
| 212 | lookup_dcookie | lookup_dcookie(2) | - |
| 213 | epoll_create | epoll_create(2) | sys_epoll_create |
| 214 | epoll_ctl_old | epoll_ctl_old(2) | - |
| 215 | epoll_wait_old | epoll_wait_old(2) | - |
| 216 | remap_file_pages | remap_file_pages(2) | sys_remap_file_pages |
| 217 | getdents64 | getdents64(2) | sys_getdents64 |
| 218 | set_tid_address | set_tid_address(2) | sys_set_tid_address |
| 219 | restart_syscall | restart_syscall(2) | sys_restart_syscall |
| 220 | semtimedop | semtimedop(2) | sys_semtimedop |
| 221 | fadvise64 | fadvise64(2) | sys_fadvise64 |
| 222 | timer_create | timer_create(2) | sys_timer_create |
| 223 | timer_settime | timer_settime(2) | sys_timer_settime |
| 224 | timer_gettime | timer_gettime(2) | sys_timer_gettime |
| 225 | timer_getoverrun | timer_getoverrun(2) | sys_timer_getoverrun |
| 226 | timer_delete | timer_delete(2) | sys_timer_delete |
| 227 | clock_settime | clock_settime(2) | sys_clock_settime |
| 228 | clock_gettime | clock_gettime(2) | sys_clock_gettime |
| 229 | clock_getres | clock_getres(2) | sys_clock_getres |
| 230 | clock_nanosleep | clock_nanosleep(2) | sys_clock_nanosleep |
| 231 | exit_group | exit_group(2) | sys_exit_group |
| 232 | epoll_wait | epoll_wait(2) | sys_epoll_wait |
| 233 | epoll_ctl | epoll_ctl(2) | sys_epoll_ctl |
| 234 | tgkill | tgkill(2) | sys_tgkill |
| 235 | utimes | utimes(2) | sys_utimes |
| 236 | vserver | vserver(2) | - |
| 237 | mbind | mbind(2) | sys_mbind |
| 238 | set_mempolicy | set_mempolicy(2) | sys_set_mempolicy |
| 239 | get_mempolicy | get_mempolicy(2) | sys_get_mempolicy |
| 240 | mq_open | mq_open(2) | sys_mq_open |
| 241 | mq_unlink | mq_unlink(2) | sys_mq_unlink |
| 242 | mq_timedsend | mq_timedsend(2) | sys_mq_timedsend |
| 243 | mq_timedreceive | mq_timedreceive(2) | sys_mq_timedreceive |
| 244 | mq_notify | mq_notify(2) | sys_mq_notify |
| 245 | mq_getsetattr | mq_getsetattr(2) | sys_mq_getsetattr |
| 246 | kexec_load | kexec_load(2) | sys_kexec_load |
| 247 | waitid | waitid(2) | sys_waitid |
| 248 | add_key | add_key(2) | sys_add_key |
| 249 | request_key | request_key(2) | sys_request_key |
| 250 | keyctl | keyctl(2) | sys_keyctl |
| 251 | ioprio_set | ioprio_set(2) | sys_ioprio_set |
| 252 | ioprio_get | ioprio_get(2) | sys_ioprio_get |
| 253 | inotify_init | inotify_init(2) | sys_inotify_init |
| 254 | inotify_add_watch | inotify_add_watch(2) | sys_inotify_add_watch |
| 255 | inotify_rm_watch | inotify_rm_watch(2) | sys_inotify_rm_watch |
| 256 | migrate_pages | migrate_pages(2) | sys_migrate_pages |
| 257 | openat | openat(2) | sys_openat |
| 258 | mkdirat | mkdirat(2) | sys_mkdirat |
| 259 | mknodat | mknodat(2) | sys_mknodat |
| 260 | fchownat | fchownat(2) | sys_fchownat |
| 261 | futimesat | futimesat(2) | sys_futimesat |
| 262 | newfstatat | newfstatat(2) | sys_newfstatat |
| 263 | unlinkat | unlinkat(2) | sys_unlinkat |
| 264 | renameat | renameat(2) | sys_renameat |
| 265 | linkat | linkat(2) | sys_linkat |
| 266 | symlinkat | symlinkat(2) | sys_symlinkat |
| 267 | readlinkat | readlinkat(2) | sys_readlinkat |
| 268 | fchmodat | fchmodat(2) | sys_fchmodat |
| 269 | faccessat | faccessat(2) | sys_faccessat |
| 270 | pselect6 | pselect6(2) | sys_pselect6 |
| 271 | ppoll | ppoll(2) | sys_ppoll |
| 272 | unshare | unshare(2) | sys_unshare |
| 273 | set_robust_list | set_robust_list(2) | sys_set_robust_list |
| 274 | get_robust_list | get_robust_list(2) | sys_get_robust_list |
| 275 | splice | splice(2) | sys_splice |
| 276 | tee | tee(2) | sys_tee |
| 277 | sync_file_range | sync_file_range(2) | sys_sync_file_range |
| 278 | vmsplice | vmsplice(2) | sys_vmsplice |
| 279 | move_pages | move_pages(2) | sys_move_pages |
| 280 | utimensat | utimensat(2) | sys_utimensat |
300-334: path-safe I/O, zero-copy tricks, and the arrival of BPF
Numbers 257-269, just before this block, already introduced the *at family (openat, mkdirat, unlinkat, and so on), and this section leans on the same idea heavily enough that it’s worth pausing on why that pattern exists at all. An ordinary open("some/relative/path") resolves relative to the process’s current working directory, which is global, mutable state. If another thread changes the working directory between when you decide what to open and when the open call actually runs, or if an attacker can race a symlink into place at just the right moment, you can end up operating on a completely different file than you intended. That’s a real, exploitable class of bug called a TOCTOU (time-of-check to time-of-use) race. The *at variants sidestep it by resolving the path relative to a directory file descriptor you already hold open, instead of a mutable global. That fd can’t be swapped out from under you the way a path string can, which makes these calls the foundation for anything that needs to walk a filesystem safely and concurrently, including how container runtimes and sandboxes construct filesystem views without trusting string paths.
splice, tee, and vmsplice (275, 276, 278) are Linux’s answer to a different problem: moving data between file descriptors without ever copying it through user space. Normally, moving bytes from one file descriptor to another means read-ing them into a user space buffer and then write-ing that buffer back out, two copies and two context switches for data your program never actually needed to look at. splice moves data directly between two file descriptors through an in-kernel pipe buffer, tee duplicates data between two pipes without consuming it, and vmsplice maps user space memory directly into a pipe. This is the machinery behind efficient implementations of things like sendfile-style network transfers and high-throughput proxying, where the CPU and memory bandwidth saved by skipping redundant copies can matter a lot at scale.
A handful of entries here are genuinely load-bearing for modern Linux security and observability, and worth knowing on sight. getrandom (318) fixed a real, longstanding footgun: reading /dev/urandom early in boot, before the kernel’s entropy pool had actually been seeded, used to silently hand back low-quality randomness with no indication anything was wrong, which is exactly the kind of bug that produces predictable cryptographic keys. getrandom blocks (or fails, depending on flags) until the CSPRNG is actually properly seeded, which is why “use getrandom, not /dev/urandom, if you’re writing new code that needs cryptographic randomness” became standard advice almost as soon as it shipped. memfd_create (319) creates an anonymous, unlinkable file descriptor backed entirely by memory, with no path on any filesystem at all; combined with its sealing feature, it lets you build a buffer that can be written once, sealed against further modification, and then safely shared with another process (by passing the fd itself) with a guarantee that neither side can tamper with it afterward. bpf (321) is the single entry point for essentially everything the BPF subsystem does: loading verified programs into the kernel, creating and updating BPF maps, attaching programs to network hooks, tracepoints, or syscalls themselves. Every bpftrace script, every XDP-based load balancer, and every seccomp-bpf filter eventually goes through this one syscall. seccomp (317) itself is the sandboxing primitive that lets a process restrict the set of syscalls it, or its children, are allowed to make at all, which is exactly how Chrome’s renderer sandbox and most container runtimes’ default profiles limit the blast radius of a compromised process: even if an attacker gets arbitrary code execution inside the sandbox, most of this very table becomes unreachable to them. And userfaultfd (323) lets user space handle page faults itself instead of the kernel resolving them internally, which sounds obscure until you realize it’s the mechanism that makes live process migration and checkpoint/restore tools like CRIU possible: the kernel can hand off “what should live at this memory address” as a question your own code gets to answer, on demand, mid-fault.
After rseq (334), the table goes quiet for a while. Numbers 335 through 423 are genuinely unused on x86-64, and this isn’t an omission in this post, it’s really empty in the kernel’s own table. The most commonly cited explanation is architectural bookkeeping: newer architectures (arm64, RISC-V, and so on) use a leaner, shared “generic” syscall table defined once in asm-generic/unistd.h, which dropped a lot of the older, overlapping calls that x86-64 still carries for backward compatibility (separate stat/fstat/lstat instead of just statx, for instance). To keep syscall numbers roughly comparable across architectures without repurposing an x86-64 number that already means something else, that whole range was simply left empty here rather than reused. The next real numbers resume at 424.
| %rax | Name | Manual | Entry Point |
|---|---|---|---|
| 300 | fanotify_init | fanotify_init(2) | sys_fanotify_init |
| 301 | fanotify_mark | fanotify_mark(2) | sys_fanotify_mark |
| 302 | prlimit64 | prlimit64(2) | sys_prlimit64 |
| 303 | name_to_handle_at | name_to_handle_at(2) | sys_name_to_handle_at |
| 304 | open_by_handle_at | open_by_handle_at(2) | sys_open_by_handle_at |
| 305 | clock_adjtime | clock_adjtime(2) | sys_clock_adjtime |
| 306 | syncfs | syncfs(2) | sys_syncfs |
| 307 | sendmmsg | sendmmsg(2) | sys_sendmmsg |
| 308 | setns | setns(2) | sys_setns |
| 309 | getcpu | getcpu(2) | sys_getcpu |
| 310 | process_vm_readv | process_vm_readv(2) | sys_process_vm_readv |
| 311 | process_vm_writev | process_vm_writev(2) | sys_process_vm_writev |
| 312 | kcmp | kcmp(2) | sys_kcmp |
| 313 | finit_module | finit_module(2) | sys_finit_module |
| 314 | sched_setattr | sched_setattr(2) | sys_sched_setattr |
| 315 | sched_getattr | sched_getattr(2) | sys_sched_getattr |
| 316 | renameat2 | renameat2(2) | sys_renameat2 |
| 317 | seccomp | seccomp(2) | sys_seccomp |
| 318 | getrandom | getrandom(2) | sys_getrandom |
| 319 | memfd_create | memfd_create(2) | sys_memfd_create |
| 320 | kexec_file_load | kexec_file_load(2) | sys_kexec_file_load |
| 321 | bpf | bpf(2) | sys_bpf |
| 322 | execveat | execveat(2) | sys_execveat |
| 323 | userfaultfd | userfaultfd(2) | sys_userfaultfd |
| 324 | membarrier | membarrier(2) | sys_membarrier |
| 325 | mlock2 | mlock2(2) | sys_mlock2 |
| 326 | copy_file_range | copy_file_range(2) | sys_copy_file_range |
| 327 | preadv2 | preadv2(2) | sys_preadv2 |
| 328 | pwritev2 | pwritev2(2) | sys_pwritev2 |
| 329 | pkey_mprotect | pkey_mprotect(2) | sys_pkey_mprotect |
| 330 | pkey_alloc | pkey_alloc(2) | sys_pkey_alloc |
| 331 | pkey_free | pkey_free(2) | sys_pkey_free |
| 332 | statx | statx(2) | sys_statx |
| 333 | io_pgetevents | io_pgetevents(2) | sys_io_pgetevents |
| 334 | rseq | rseq(2) | sys_rseq |
424-456: pidfds, io_uring, and the current sandboxing frontier
The syscalls in this last block are almost all quite recent, and they mostly share a common motive: fixing sharp edges in older interfaces rather than adding brand new capability from scratch. That makes them a good window into what the kernel community currently considers unfinished business.
pidfd_open, pidfd_send_signal, and pidfd_getfd (434, 424, 438) solve a subtle but real problem with plain numeric PIDs: PIDs get recycled. If your program remembers a child’s PID, waits a while, and then calls kill() on it, there’s a real (if narrow) window where that PID could have already been reaped and reassigned to a completely unrelated process, so your signal goes to the wrong target. A pidfd is a file descriptor that refers to one specific process, not a number that might get reused; it can be poll()-ed to learn when that exact process exits, and signals sent through pidfd_send_signal are guaranteed to reach the process you actually meant, not whatever happens to hold that PID now. clone3 (435) modernizes process creation the same way: instead of cramming an ever-growing pile of flags into a handful of syscall arguments, it takes a single extensible struct, which makes it much easier for the kernel to add new creation-time options later (including, notably, requesting a pidfd for the new child at creation time, closing another race window between fork and pidfd_open).
io_uring_setup, io_uring_enter, and io_uring_register (425-427) are the syscalls behind io_uring, which is probably the biggest structural change to Linux’s I/O model in a long time. Every syscall interface this post has covered so far shares one assumption: one syscall, one operation, one kernel/user-space round trip. io_uring breaks that assumption by setting up a pair of ring buffers, a submission queue and a completion queue, shared directly between user space and the kernel. A program can queue up dozens or hundreds of I/O requests by just writing into the shared submission ring, and with a single io_uring_enter call (or, in polling mode, with no syscall at all) tell the kernel to go process them, later draining finished results from the completion ring the same way. That amortizes the cost of the user/kernel transition across many operations instead of paying it per operation, which matters enormously for I/O-heavy workloads at scale, and it’s flexible enough to cover far more than the narrow O_DIRECT-only world that the older io_setup/io_submit AIO interface (206-210, back in the 200-299 block) was stuck in.
The Landlock trio, landlock_create_ruleset, landlock_add_rule, and landlock_restrict_self (444-446), is worth calling out because it represents a genuinely different sandboxing philosophy from seccomp. Most Linux sandboxing tools, seccomp filters, SELinux, AppArmor, are either applied by a privileged supervisor or require system-wide policy configuration. Landlock is designed to be usable by an unprivileged process on itself: any program can restrict its own filesystem access before doing something risky (parsing an untrusted file, say), with no root and no external policy file required, which makes it practical for ordinary applications, not just container runtimes, to sandbox themselves defensively.
A last handful worth naming individually: close_range (436) closes a whole range of file descriptors in one call, which matters more than it sounds like for daemons that fork and need to make sure no inherited descriptors leak into the child, previously requiring a loop calling close (or worse, fcntl) one descriptor at a time. openat2 (437) hardens openat with explicit path-resolution flags (things like refusing to follow symlinks or refusing to cross mount points mid-resolution), giving sandboxes stronger guarantees about exactly what a path lookup is and isn’t allowed to do. futex_waitv, futex_wake, futex_wait, and futex_requeue (449, 454-456) are the newer “futex2” API, splitting the single, heavily-overloaded futex syscall from the 200-299 block into dedicated operations, echoing the exact same design argument that gave x86-64 separate socket syscalls instead of a multiplexed socketcall: one job per syscall number is easier to trace, verify, and reason about than one syscall with a dozen behaviors selected by an opcode argument. cachestat (451) lets a process ask, cheaply and without needing root, how much of a given file is currently resident in the page cache, useful for tuning caching-sensitive software without guessing. map_shadow_stack (453) is part of Intel’s Control-flow Enforcement Technology (CET) support, setting up a hardware-protected shadow stack that the CPU itself checks return addresses against, a mitigation aimed squarely at return-oriented-programming exploitation techniques. And memfd_secret (447) takes memfd_create’s idea a step further: the memory it hands back isn’t just unlinkable, it’s excluded from the kernel’s own direct map, so even a kernel bug or another kernel subsystem can’t accidentally read it, which is exactly the property you want for holding things like cryptographic private keys in memory.
| %rax | Name | Manual | Entry Point |
|---|---|---|---|
| 424 | pidfd_send_signal | pidfd_send_signal(2) | sys_pidfd_send_signal |
| 425 | io_uring_setup | io_uring_setup(2) | sys_io_uring_setup |
| 426 | io_uring_enter | io_uring_enter(2) | sys_io_uring_enter |
| 427 | io_uring_register | io_uring_register(2) | sys_io_uring_register |
| 428 | open_tree | open_tree(2) | sys_open_tree |
| 429 | move_mount | move_mount(2) | sys_move_mount |
| 430 | fsopen | fsopen(2) | sys_fsopen |
| 431 | fsconfig | fsconfig(2) | sys_fsconfig |
| 432 | fsmount | fsmount(2) | sys_fsmount |
| 433 | fspick | fspick(2) | sys_fspick |
| 434 | pidfd_open | pidfd_open(2) | sys_pidfd_open |
| 435 | clone3 | clone3(2) | sys_clone3 |
| 436 | close_range | close_range(2) | sys_close_range |
| 437 | openat2 | openat2(2) | sys_openat2 |
| 438 | pidfd_getfd | pidfd_getfd(2) | sys_pidfd_getfd |
| 439 | faccessat2 | faccessat2(2) | sys_faccessat2 |
| 440 | process_madvise | process_madvise(2) | sys_process_madvise |
| 441 | epoll_pwait2 | epoll_pwait2(2) | sys_epoll_pwait2 |
| 442 | mount_setattr | mount_setattr(2) | sys_mount_setattr |
| 443 | quotactl_fd | quotactl_fd(2) | sys_quotactl_fd |
| 444 | landlock_create_ruleset | landlock_create_ruleset(2) | sys_landlock_create_ruleset |
| 445 | landlock_add_rule | landlock_add_rule(2) | sys_landlock_add_rule |
| 446 | landlock_restrict_self | landlock_restrict_self(2) | sys_landlock_restrict_self |
| 447 | memfd_secret | memfd_secret(2) | sys_memfd_secret |
| 448 | process_mrelease | process_mrelease(2) | sys_process_mrelease |
| 449 | futex_waitv | futex_waitv(2) | sys_futex_waitv |
| 450 | set_mempolicy_home_node | set_mempolicy_home_node(2) | sys_set_mempolicy_home_node |
| 451 | cachestat | cachestat(2) | sys_cachestat |
| 452 | fchmodat2 | fchmodat2(2) | sys_fchmodat2 |
| 453 | map_shadow_stack | map_shadow_stack(2) | sys_map_shadow_stack |
| 454 | futex_wake | futex_wake(2) | sys_futex_wake |
| 455 | futex_wait | futex_wait(2) | sys_futex_wait |
| 456 | futex_requeue | futex_requeue(2) | sys_futex_requeue |
Closing thoughts
Reading through this table front to back is basically reading a history of what Linux has had to worry about over three decades: first, “how do you move bytes and manage memory,” then “how do you keep multiple users and processes from stepping on each other,” then “how do you scale to tens of thousands of concurrent connections,” and now, increasingly, “how do you sandbox untrusted code without giving up performance, and how do you avoid an entire syscall per I/O operation when you’re doing millions of them a second.” The dead entries (the bare dashes scattered throughout) are just as informative as the live ones; they’re proof that this ABI genuinely never breaks its promises, even to features nobody uses anymore.
If you want to see any of this in action rather than just as a table, strace -f -T on a running program is the fastest way to watch these numbers turn into real behavior, and the kernel’s own Documentation/process/adding-syscalls.rst is worth a read if you’re curious how conservative the process of adding a new one actually is today.
