Syscalls
Subtopic 2
Code Examples
The Building Blocks of Process Management
Linux process management revolves around four system calls. Think of them as a lifecycle: a process is born (fork), may transform (execve), does its work, and eventually dies (exit). The parent oversees this with wait. Understanding each call in detail is essential for any systems programmer.
Here is how the four system calls fit together in the process lifecycle:
Program A
and continues
running Program A
(or still A)
#include <unistd.h>
pid_t fork(void);
/* Parent gets: child's PID (positive)
Child gets : 0
Error : -1 */
- Creates an almost exact duplicate of the calling process
- Child gets copies of stack, data, heap; text is shared (read-only)
- Both processes resume from the instruction after fork()
- Takes no arguments — all state is inherited automatically
- Child can call
getppid()to get parent’s PID
#include <stdlib.h>
void exit(int status);
/* status is 0-255. Convention:
0 = success
Non-zero = failure (specific error code)
EXIT_SUCCESS = 0 (defined in stdlib.h)
EXIT_FAILURE = 1 (defined in stdlib.h) */
What exit() does, step by step:
| Step | Action |
|---|---|
| 1 | Runs functions registered with atexit() |
| 2 | Flushes and closes all open stdio streams |
| 3 | Removes temporary files created with tmpfile() |
| 4 | Calls the kernel’s _exit() system call |
| 5 | Kernel releases all process resources |
| 6 | Sends SIGCHLD to parent; process becomes zombie until parent calls wait() |
#include <sys/wait.h>
pid_t wait(int *status);
/* Returns PID of terminated child, or -1 on error.
Pass NULL if you don't care about exit status. */
Decoding the status value with macros:
int status;
pid_t pid = wait(&status);
if (WIFEXITED(status)) {
/* Child called exit() or returned from main() */
printf("Exit code: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
/* Child was killed by a signal */
printf("Killed by signal: %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
/* Child was stopped (e.g., by SIGSTOP) */
printf("Stopped by signal: %d\n", WSTOPSIG(status));
}
#include <unistd.h>
int execve(const char *pathname, char *const argv[],
char *const envp[]);
/* Returns: -1 on error. On SUCCESS it never returns.
pathname : full path to the executable
argv[] : argument list (argv[0] = program name, NULL terminated)
envp[] : environment variables (NULL terminated) */
- Completely replaces the current process: code, stack, heap, data
- The PID remains the same
- Open file descriptors are preserved (unless close-on-exec is set)
- If execve() returns, it failed — always check the return
- Wrapper functions:
execl(), execlp(), execle(), execv(), execvp()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int status;
/* Step 1: fork() - create child */
pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/* CHILD */
printf("[Child PID=%d] About to exec /bin/date\n", getpid());
/* Step 2: execve() - run a new program */
char *argv[] = { "date", NULL };
char *envp[] = { NULL };
execve("/bin/date", argv, envp);
/* Only reached if execve fails */
perror("execve");
exit(EXIT_FAILURE); /* Step 3a: exit() on failure */
} else {
/* PARENT */
printf("[Parent PID=%d] Waiting for child...\n", getpid());
/* Step 4: wait() - collect child */
wait(&status);
if (WIFEXITED(status))
printf("[Parent] Child exited with code %d\n",
WEXITSTATUS(status));
/* Step 3b: parent exits */
exit(EXIT_SUCCESS);
}
}
/bin/date (prints current date/time), then the parent collects the exit status.#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* Cleanup function registered with atexit() */
void cleanup(void)
{
printf("[PID=%d] cleanup() called by exit()\n", getpid());
}
int main(void)
{
/* Register cleanup function */
if (atexit(cleanup) != 0) {
fprintf(stderr, "atexit failed\n");
exit(1);
}
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(1); }
if (pid == 0) {
/* Child: exit() WILL call cleanup */
printf("[CHILD] Calling exit()\n");
exit(0);
/* cleanup() is called here automatically */
} else {
/* Parent: also calls exit() eventually */
wait(NULL);
printf("[PARENT] Calling exit()\n");
exit(0);
/* cleanup() is called here too */
}
}
exit(). This is why after fork(), children should call _exit() (not exit()) to avoid running the parent’s cleanup functions twice.Besides execve(), there are several convenient wrappers:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* Helper to fork + exec a command */
void run_command(const char *msg, int use_which)
{
pid_t pid = fork();
if (pid == -1) { perror("fork"); return; }
if (pid == 0) {
printf("[Child] %s\n", msg);
switch (use_which) {
case 1:
/* execl: list arguments directly */
execl("/bin/echo", "echo", "Hello from execl", NULL);
break;
case 2:
/* execlp: searches PATH, no full path needed */
execlp("echo", "echo", "Hello from execlp", NULL);
break;
case 3: {
/* execvp: array of args, searches PATH */
char *args[] = { "echo", "Hello from execvp", NULL };
execvp("echo", args);
break;
}
}
perror("exec failed");
exit(EXIT_FAILURE);
} else {
wait(NULL);
}
}
int main(void)
{
run_command("using execl", 1);
run_command("using execlp", 2);
run_command("using execvp", 3);
return 0;
}
| Function | Args style | Searches PATH? | Custom env? |
|---|---|---|---|
execve |
array | No | Yes (envp) |
execl |
list | No | No |
execlp |
list | Yes | No |
execle |
list | No | Yes (envp) |
execv |
array | No | No |
execvp |
array | Yes | No |
exit() is a C library function: it flushes stdio buffers, runs atexit() handlers, then calls _exit(). _exit() is a direct system call: terminates immediately, no flushing. After fork(), the child should use _exit() to avoid double-flushing the parent’s stdio buffers.
It extracts the exit code (0–255) from the status integer returned by wait(). You should only call it after confirming WIFEXITED(status) is true, otherwise the result is undefined.
No. On success, execve() completely replaces the process image and never returns. Any code after execve() only executes if execve() failed. Always check the return and call perror() after execve() in production code.
SIGCHLD is sent by the kernel to the parent process when one of its children changes state: exits, is stopped, or is continued. By default it is ignored. Servers often install a SIGCHLD handler that calls wait() inside it to reap children without blocking.
execve() requires the full path to the executable and a custom environment array. execvp() searches the PATH environment variable to find the executable (so you can just pass “ls” instead of “/bin/ls”) and inherits the current environment.
