█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Programming Languages/C/Intro to Systems Programming
50 minAdvanced

Intro to Systems Programming

After this lesson, you will be able to: Understand the systems-programming foundations C exposes: system calls, file descriptors, processes (fork/exec/wait), and signals, the layer every server, shell, and OS is built on.

C is the language of systems programming because it talks almost directly to the operating system. This lesson covers the core OS concepts C gives you access to: what a system call actually is, why everything is a file descriptor, how processes are created with fork and exec, and how signals interrupt a running program. These are the foundations behind every web server, shell, and container you have used, and they come up directly in systems-engineering interviews.

Prerequisites:C and Security

System calls: the boundary between your code and the kernel

Your program runs in user mode with limited privileges. Anything that touches hardware or shared resources, reading a file, opening a network socket, creating a process, must ask the operating system kernel to do it via a system call (syscall). Library functions like `printf` and `fopen` are convenient wrappers that ultimately make syscalls like `write` and `open`. The transition from user mode to kernel mode has a cost, which is why high-performance code tries to minimize syscalls (for example, buffering output instead of writing one byte at a time). On Linux the low-level syscall wrappers live in `unistd.h`.

File descriptors: everything is a small integer

Open files, sockets, and pipes are all file descriptors. The low-level I/O syscalls work on them directly.

tsx
#include <fcntl.h>
#include <unistd.h>
int main(void) {
// open() returns a file descriptor: a small int the kernel uses to track the file
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) { perror("open"); return 1; }
const char *msg = "hello syscalls\n";
write(fd, msg, 15); // write() is the syscall under fputs/printf
close(fd); // always close; leaked fds eventually exhaust the limit
return 0;
}
// fd 0 = stdin, 1 = stdout, 2 = stderr; they're just pre-opened file descriptors.
// Sockets and pipes are also file descriptors, which is why the same read()/write()
// calls work on files AND network connections. 'Everything is a file' in action.

Processes: fork, exec, and wait

This trio is how every shell launches programs and how servers spawn workers.

tsx
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main(void) {
pid_t pid = fork(); // duplicates the process; returns twice
if (pid == 0) {
// child: replace this process image with a new program
execlp("ls", "ls", "-l", NULL);
perror("exec"); // only reached if exec fails
_exit(1);
} else {
// parent: wait for the child to finish and read its exit status
int status;
waitpid(pid, &status, 0);
printf("child exited with %d\n", WEXITSTATUS(status));
}
return 0;
}
// fork() = copy the current process; exec() = become a different program;
// wait() = parent collects the child's result. Every shell command you run is fork+exec+wait.

Signals: asynchronous interrupts

A signal is an asynchronous notification the OS (or another process) sends to your program: `SIGINT` when you press Ctrl+C, `SIGTERM` when something asks you to shut down, `SIGSEGV` on an invalid memory access, `SIGKILL` to force-kill (which you cannot catch). You register a handler with `signal()` or, more robustly, `sigaction()`. The professional use is graceful shutdown: catch `SIGTERM`, finish in-flight work, close file descriptors, then exit, exactly what a container runtime sends when stopping your process. Signal handlers run in a constrained context, so they should do as little as possible (set a flag, then handle it in the main loop).

Common mistakes only experienced engineers catch

Leaking file descriptors by not calling `close()`, eventually hitting the per-process fd limit and failing to open anything. Ignoring the return value of syscalls like `read`/`write`, which can return fewer bytes than requested or fail; always check. Doing non-trivial work inside a signal handler; only async-signal-safe functions are allowed there. Set a flag and handle it in the main loop. Forgetting to `wait()` on children, creating zombie processes that linger in the process table. Assuming a single `write()` sends everything; for sockets especially, loop until all bytes are written.

Quick Check

How does a shell run the command you type, like `ls -l`?

Pick the correct sequence.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←C and Security
Back to C
Passion Project: C CLI Tool→