The simple breakdown is:
- Anything that performs input/output involes a series of system calls.
- Anything that's purely computational doesn't involve system calls.
Both of these statements have exceptions (I/O through memory-mapped files or peripherals is I/O without syscalls, calling a hardware accelerator or allocating memory is syscalls without externally visible I/O) but they're a good approximation.
So here are some examples of operations that don't involve system calls:
- Getting or changing the value of a variable.
- Arithmetic operations on integers and floats.
- Conditionals and loops whose body doesn't involve system calls.
- Operations on strings such as searching, joining, splitting, etc.
- Manipulations of data structures such as lists, arrays, matrices, trees, etc.
- Memory management, as long as the program doesn't need to obtain more memory from the OS.
- Printing to a buffered stream, if the buffer doesn't get flushed during this print call. Likewise, reading from a buffered stream, if enough bytes are available.
It would help to demonstrate by showing the system calls made by some programs, e.g. with strace
on Linux, with dtruss
on macOS or with Systrace under Windows. Compare a program that does a lot of I/O with one that does one big computation and prints the result.
The case of buffered I/O, suggested by ctrl+alt+delor, is an interesting one because the same library functions may or may not trigger a system call depending on the state of the system.
There is sometimes possible confusion between a library call and a system call. Unix programmers in particular tend to believe that what is documented in section 2 of the manual is system calls, but this is not true on a modern Unix systems. Every function callable from C is a C library function that's a wrapper (possibly very simple, possibly even a macro rather than a function) around a system call that usually, but not always, has the same name and takes more or less the same parameters. You can observe that on a modern Linux/x86_32 system, where many system calls have been upgraded from 32-bit arguments to 64-bit arguments, e.g. strace ls
will show calls of getdents64
and fstat64
rather than getdents
and fstat
. Under the hood, system calls and library calls have a different calling convention. At the very least, a library call involes a jump/branch instruction while a system call involves a privilege change instruction. Depending on the system, the rules for placing arguments in registers may be different.
You can discuss how a program could make as few system calls as possible by starting from strace /bin/true
. Most of its syscalls are due to dynamic linking, so write that one-line program, compile it statically and look at the remaining system calls. Reduce it even further with a more minimalistic libc such as dietlibc.
# Install dietlibc, e.g. apt-get install dietlibc-dev on Debian
$ cat a.c
int main(void) {return 0;}
$ diet gcc a.c
$ strace ./a.out
execve("./a.out", ["./a.out"], [/* 82 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7fff98099fe0) = 0
_exit(0) = ?
+++ exited with 0 +++
int main(){int i = 0; return 0;}
has to implicitly request resources from the system to store the variable. $\endgroup$strlen
andstrcmp
, which are not and do not result in syscalls. Then you haveprintf
andmalloc
which are not syscalls, but which may result in syscalls. $\endgroup$