Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in
Lecture notes · chapter 6
Processes
Launching programs from your own code, as a shell does - fork to duplicate a process, wait to wait for it to end, exec to make it run another program. Orphans and zombies, redirections and pipes between processes, and signals.
Programs and processes
This chapter shows how a program launches other programs. That is exactly what a shell does every time you type a command, and we will end up writing a small one.
The program is unique and passive: it is a file on the disk. The process is active, and one program can give rise to several processes, even at the same time: if three people run vim on the same machine, there is one vim program and three processes.
Identifiers
Each process has a number, its pid (process identifier), unique among living processes. It also knows the pid of its parent, the process that created it: the ppid.
It also has an owner, a user and a group, which determine its rights: which files it may read, which processes it may send a signal to. There are two sets of them:
- the real identity (
uid,gid): the user who launched the process; - the effective identity (
euid,egid): the one whose rights the system checks.
Usually, the two are the same. They differ when the executable carries the setuid bit: the process then takes as its effective identity that of the owner of the file, while the real identity remains that of the user who launched it. This is how passwd, which belongs to root and carries the setuid bit, can modify the password file on behalf of an ordinary user. Such a program can switch from one identity to the other with seteuid, so as to keep the owner's rights only for as long as necessary.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("pid = %d\n", (int)getpid());
printf("ppid = %d\n", (int)getppid());
printf("uid = %d, euid = %d\n", (int)getuid(), (int)geteuid());
return 0;
}
$ ./ids
pid = 344620
ppid = 344490
uid = 1000, euid = 1000
The numbers change at each run. The parent, here, is the shell that launched the program.
The functions of this chapter belong to the POSIX standard, not to the C standard. With -std=c11, gcc does not declare all of them: so we compile the examples with gcc -std=c11 -Wall -Wextra -Werror -D_DEFAULT_SOURCE. The -D_DEFAULT_SOURCE option asks the library to make the POSIX and BSD functions visible.
pid_t, the type of pids, is an integer whose size depends on the system. To print it with %d, we convert it to int.
The command line
main can receive the command-line arguments:
#include <stdio.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; i++)
printf("argv[%d] = \"%s\"\n", i, argv[i]);
return 0;
}
$ ./args one "two words" 3
argv[0] = "./args"
argv[1] = "one"
argv[2] = "two words"
argv[3] = "3"
argc is the number of arguments, argv an array of argc strings. argv[0] is the name under which the program was launched, and argv[argc] is always NULL. It is the shell that split the line into words: the quotes grouped two words into a single argument, and disappeared.
Creating a process: fork
Duplicating
Under Unix, there is only one way to create a process: duplicate an existing one. The fork system call makes a copy of the calling process, memory and descriptors included, and both copies resume their execution at the same point: on return from fork.
#include <unistd.h>
pid_t fork(void);
The original is called the parent, the copy the child. They have the same code and, at the start, the same memory; they differ only in their pid, and in what fork returns to them:
- in the parent,
forkreturns the pid of the child; - in the child,
forkreturns0; - on failure (too many processes, out of memory),
forkreturns-1in the parent, and there is no child.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t x = fork();
printf("x = %d\n", (int)x);
return 0;
}
$ ./fork1
x = 345272
x = 0
Two lines for a single printf: there are now two processes executing it. The first output comes from the parent, which received the pid of its child; the second, from the child. The order is not guaranteed: it depends on the system's scheduler.
The parent / child pattern
The return value lets us give each one a different job:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int n = 10;
pid_t pid = fork();
switch (pid) {
case -1:
perror("fork");
return EXIT_FAILURE;
case 0:
n = n + 1;
printf("child: n = %d\n", n);
break;
default:
n = n * 2;
printf("parent: n = %d, my child is %d\n", n, (int)pid);
break;
}
return 0;
}
$ ./fork3
parent: n = 20, my child is 344627
child: n = 11
The child leaves with a copy of n: the two processes each have their own variable, and what one writes into it, the other does not see. Two processes do not share their memory. To communicate, they need a pipe (chapter 5, and below), a file, or a signal.
The buffer trap
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("pid = %d\n", (int)getpid());
pid_t x = fork();
printf("x = %d, pid = %d\n", (int)x, (int)getpid());
return 0;
}
In a terminal:
$ ./fork2
pid = 345267
x = 345268, pid = 345267
x = 0, pid = 345268
The first line appears only once, since it is printed before the fork. But if we redirect the output to a file:
$ ./fork2 > sortie.txt
$ cat sortie.txt
pid = 344624
x = 344625, pid = 344624
pid = 344624
x = 0, pid = 344625
It appears twice! printf does not write directly: it fills a buffer in memory, which is flushed (by a write) only from time to time (chapter 5). Towards a terminal, the buffer is flushed at each end of line. Towards a file or a pipe, it is flushed only when it is full, or at the end of the program. At the time of the fork, the first line was therefore still in the buffer, that is, in the memory of the process, and fork copied it. Each of the two processes wrote it out when terminating.
Without a newline, the text stays in the buffer even towards a terminal: printf("before "); fork(); printf("after\n"); prints before after twice, in every case.
The life cycle of processes
Every process therefore has a parent, except the very first one: init (pid 1), launched by the kernel at boot, from which all the others descend. On a current Linux, it is systemd.
Orphans
What becomes of a process whose parent dies before it?
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (fork() != 0) {
sleep(1); /* the parent dies after one second */
return 0;
}
printf("(1) my parent: %d\n", (int)getppid());
sleep(2); /* the child outlives it */
printf("(2) my parent: %d\n", (int)getppid());
return 0;
}
$ ./orphelin
(1) my parent: 344633
$ (2) my parent: 2426
The shell took back control as soon as the parent died: the second line arrives after the prompt. In the meantime, the child has been adopted. Traditionally, it is init, process 1, that adopts orphans, and that is what you will see on a server or in a container. On a recent Linux workstation, you get something else:
$ ps -o pid,ppid,cmd -p 2426
PID PPID CMD
2426 1 /usr/lib/systemd/systemd --user
This is the user's session manager. Linux lets a process declare itself a subreaper: the orphans of its descendants go to it, instead of going all the way up to init. In every case, a process always has a parent.
This is how a program is detached from the terminal to turn it into a daemon, a service that runs in the background: it duplicates itself, and the parent dies immediately.
Zombies
When a process terminates, it does not disappear completely: the system keeps its exit status (and a few statistics), until its parent comes to collect it. In the meantime, the process is a zombie: it is dead, it no longer occupies memory, but it still has an entry in the process table.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0)
return 0; /* the child dies immediately */
printf("child %d\n", (int)pid);
sleep(30); /* the parent does not reap it */
return 0;
}
During the thirty seconds, ps shows it, with state Z and the mention <defunct>:
$ ./zombie &
child 344761
$ ps -o pid,ppid,stat,cmd
PID PPID STAT CMD
344761 344759 Z [zombie] <defunct>
A zombie costs almost nothing, but the process table has a limited size. A server that launches thousands of children without ever reaping them ends up unable to create any more: fork fails. When the parent dies, its zombies are adopted, and their adoptive parent reaps them at once.
Waiting for a child: wait and waitpid
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait blocks the process until one of its children terminates (or returns immediately if a child is already a zombie), reaps it, and returns its pid. If there is no child at all, it returns -1. waitpid waits for a specific child; with -1 as first argument, it waits for any of them, and the WNOHANG option makes it return 0 instead of waiting if no child has finished.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
if (fork() == 0) {
printf(">> child: I'm done\n");
return 0;
}
wait(NULL);
printf("> parent: my child is dead\n");
return 0;
}
$ ./attente
>> child: I'm done
> parent: my child is dead
Here the order is guaranteed: the parent prints nothing before the child ends.
If we pass wait the address of an integer, it stores there how the child died. This integer is decoded with macros:
| Macro | Meaning |
|---|---|
WIFEXITED(status) | true if the child terminated normally (return from main, or exit) |
WEXITSTATUS(status) | its exit status, from 0 to 255 |
WIFSIGNALED(status) | true if the child was killed by a signal |
WTERMSIG(status) | the number of that signal |
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0)
return 42; /* dies immediately, with status 42 */
int status;
pid_t mort = waitpid(pid, &status, 0);
printf("child %d ", (int)mort);
if (WIFEXITED(status))
printf("exited with status %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("was killed by signal %d\n", WTERMSIG(status));
return 0;
}
$ ./statut
child 344767 exited with status 42
If we replace return 42; with abort();, which sends the process the signal SIGABRT (number 6):
$ ./statut
child 344769 was killed by signal 6
Running another program: exec
Replacing the program
fork creates a process, but it is a copy of the same program. To launch another program, a second mechanism is needed: the functions of the exec family replace the program of the current process with another. The process keeps its pid, its parent and its open descriptors; but its code, its memory and its stack are replaced by those of the new program, which starts at its main.
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
int execlp(const char *file, const char *arg, ...); /* ..., (char *)NULL */
fileis the program to launch. If it does not contain a/, it is looked up in the directories of thePATHvariable, as the shell does (that is thepofexecvp). Otherwise, it is a path to the executable.argvis the array of arguments, the one themainof the new program will receive. By convention,argv[0]is the name of the program, and the last cell must containNULL, which marks the end of the array.execlptakes the arguments one by one, and the last one must also beNULL.
The other variants (execv, execl, execve…) differ in the way arguments are passed, the use of PATH and the environment. They all end up in the execve system call.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *arg[] = { "ls", "-l", NULL };
execvp(arg[0], arg);
perror("execvp"); /* we only get here on failure */
return 127;
}
This program prints the same thing as ls -l: it is now ls that runs in the process.
fork, exec, wait: launching a command
It is the combination of the three that lets us launch a program and carry on: we create a child with fork, the child replaces itself with the desired program with exec, and the parent waits for it to end with wait. That is exactly what a shell does.
flowchart LR
S[shell] -->|fork| F[copy of the shell]
F -->|exec| L[ls]
L -->|exit| Z[zombie]
S -->|wait| ZHere is a minimal shell. It reads a line, splits it into words with strtok, and launches the command:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
/* Splits ligne into words, in place; argv ends with NULL. */
static void decouper(char ligne[], char *argv[])
{
int n = 0;
for (char *mot = strtok(ligne, " \t\n"); mot != NULL && n < MAX_ARGS - 1;
mot = strtok(NULL, " \t\n"))
argv[n++] = mot;
argv[n] = NULL;
}
int main(void)
{
char ligne[1024];
char *argv[MAX_ARGS];
for (;;) {
printf("$ ");
fflush(stdout);
if (fgets(ligne, sizeof ligne, stdin) == NULL)
break; /* end of file: Ctrl-D */
decouper(ligne, argv);
if (argv[0] == NULL)
continue; /* empty line */
if (strcmp(argv[0], "exit") == 0)
break;
pid_t pid = fork();
if (pid == -1) {
perror("fork");
continue;
}
if (pid == 0) {
execvp(argv[0], argv);
perror(argv[0]);
exit(127);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
printf("[code %d]\n", WEXITSTATUS(status));
}
printf("\n");
return 0;
}
$ ./minishell
$ echo hello world
hello world
$ foo
foo: No such file or directory
[code 127]
$ exit
The fflush(stdout) after the prompt is necessary: "$ " does not end with a newline, and would otherwise stay in the buffer. It also flushes the buffer before the fork, for the reason seen above.
Timing a command
A fuller example: chrono command arguments… launches the command and, on exit, prints its real execution time and the processor time it consumed, like the time command. We measure real time with the CLOCK_MONOTONIC clock, which never goes backwards (unlike the system time, which can be adjusted), and we obtain the child's processor time with wait4, a variant of waitpid that also fills in a struct rusage.
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static double secondes(struct timeval t)
{
return t.tv_sec + 1e-6 * t.tv_usec;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "usage: %s command [arguments]\n", argv[0]);
return 1;
}
struct timespec debut, fin;
clock_gettime(CLOCK_MONOTONIC, &debut);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
execvp(argv[1], argv + 1);
fprintf(stderr, "chrono: cannot launch ");
perror(argv[1]);
exit(127);
}
int status;
struct rusage ru;
wait4(pid, &status, 0, &ru);
clock_gettime(CLOCK_MONOTONIC, &fin);
double reel = (fin.tv_sec - debut.tv_sec) + 1e-9 * (fin.tv_nsec - debut.tv_nsec);
fprintf(stderr, "real %.3f s\n", reel);
fprintf(stderr, "user %.3f s\n", secondes(ru.ru_utime));
fprintf(stderr, "system %.3f s\n", secondes(ru.ru_stime));
if (WIFEXITED(status))
return WEXITSTATUS(status);
return 128 + WTERMSIG(status);
}
$ ./chrono sleep 1
real 1.001 s
user 0.001 s
system 0.000 s
Three details:
argv + 1is the array of arguments minus its first cell:argv[1]becomes its first element, and it still ends withNULL. That is exactly the array the command expects.- The measurements are written to the error output, so as not to mix with the command's output, which can thus be redirected.
chronoreturns the command's exit status, or128 +the number of the signal that killed it: that is the shell's convention.
Redirecting before exec
Since exec keeps the open descriptors, the child can prepare them between fork and exec. This is how the shell carries out ls -l > liste.txt: in the child, it opens the file, puts it on the standard output with dup2 (chapter 5), then launches ls, which writes to its standard output without knowing it has changed.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
/* Like the shell for: ls -l > liste.txt */
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
int fd = open("liste.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("liste.txt");
exit(1);
}
dup2(fd, STDOUT_FILENO); /* the standard output becomes the file */
close(fd);
char *arg[] = { "ls", "-l", NULL };
execvp(arg[0], arg);
perror("ls");
exit(127);
}
waitpid(pid, NULL, 0);
printf("ls wrote into liste.txt\n");
return 0;
}
The redirection takes place in the child only: the parent's standard output has not changed.
A pipe between two commands
ls | wc -l follows the same principle, with a pipe (chapter 5) and two children: the first puts the write end of the pipe on its standard output and becomes ls; the second puts the read end of the pipe on its standard input and becomes wc -l.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
/* Like the shell for: ls | wc -l */
int main(void)
{
int t[2];
if (pipe(t) == -1) {
perror("pipe");
return 1;
}
if (fork() == 0) { /* first child: ls, writes into the pipe */
dup2(t[1], STDOUT_FILENO);
close(t[0]);
close(t[1]);
execlp("ls", "ls", NULL);
perror("ls");
exit(127);
}
if (fork() == 0) { /* second child: wc -l, reads the pipe */
dup2(t[0], STDIN_FILENO);
close(t[0]);
close(t[1]);
execlp("wc", "wc", "-l", NULL);
perror("wc");
exit(127);
}
close(t[0]); /* the parent does not use the pipe */
close(t[1]);
wait(NULL);
wait(NULL);
return 0;
}
Signals
Software interrupts
A signal is a notification sent to a process, which interrupts what it is doing. Each signal has a number and a name; kill -l lists them. The most common ones:
| Signal | Number | Sent when | Default effect |
|---|---|---|---|
SIGINT | 2 | you type Ctrl-C in the terminal | terminates the process |
SIGKILL | 9 | you want to kill the process for sure | terminates the process, with no way out |
SIGSEGV | 11 | invalid memory access | terminates the process (and “Segmentation fault”) |
SIGPIPE | 13 | writing into a pipe that nobody reads any more | terminates the process |
SIGTERM | 15 | the process is politely asked to stop | terminates the process |
SIGCHLD | 17 | a child has terminated | ignored |
SIGABRT | 6 | abort() | terminates the process |
The numbers are those of Linux on x86; they may change from one system to another, the names do not. Always use the names.
Some signals come from the kernel, in reaction to what the program does (SIGSEGV, SIGPIPE) or to an event (SIGCHLD, SIGINT when you type Ctrl-C). Others are sent by one process to another. The kill command and the system call of the same name are used for this:
$ kill 12345 # sends SIGTERM to process 12345
$ kill -INT 12345 # sends SIGINT
$ kill -9 12345 # sends SIGKILL
#include <signal.h>
int kill(pid_t pid, int sig);
Despite its name, kill does not necessarily kill: it sends a signal, and killing is the default effect of most of them. You can only send signals to your own processes, unless you are root.
#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
for (;;)
pause(); /* waits for a signal, forever */
}
sleep(1);
kill(pid, SIGTERM);
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status))
printf("child %d killed by signal %d\n", (int)pid, WTERMSIG(status));
return 0;
}
$ ./tuer
child 344825 killed by signal 15
And a segmentation fault, as seen by the parent:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
if (fork() == 0) {
int *p = NULL;
*p = 42; /* write at address 0 */
return 0;
}
int status;
wait(&status);
if (WIFSIGNALED(status))
printf("child killed by signal %d\n", WTERMSIG(status));
return 0;
}
$ ./segv
child killed by signal 11
Catching a signal: sigaction
For most signals, a process can replace the default effect: ignore the signal, or run a function of its choice, a handler. SIGKILL and SIGSTOP are the exceptions: they can be neither caught nor ignored. That is what guarantees that a process can always be stopped.
A handler is installed with sigaction. (There is also a simpler signal function, but its behaviour varies from one system to another; it is no longer used.)
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static volatile sig_atomic_t interrompu = 0;
static void gestionnaire(int sig)
{
(void)sig;
interrompu = 1; /* nothing else: see below */
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = gestionnaire;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
int tours = 0;
while (!interrompu) {
printf("round %d\n", tours++);
sleep(1);
}
printf("Ctrl-C received after %d rounds: tidying up and leaving\n", tours);
return 0;
}
$ ./sigint
round 0
round 1
round 2
^CCtrl-C received after 3 rounds: tidying up and leaving
Ctrl-C no longer kills the program: it calls gestionnaire, which raises a flag. The loop sees it on the next round, and the program terminates cleanly. (sleep, incidentally, is interrupted by the signal: we do not wait for the end of the second.)
What a handler is allowed to do
A signal can arrive at any time, between any two instructions of the program: in the middle of a printf, while malloc is reorganising its internal lists. The handler then runs, and the program resumes where it was. If the handler itself calls printf or malloc, it finds their data half-modified, and the result is unpredictable.
The POSIX standard therefore lists the functions that can safely be called from a handler: the async-signal-safe functions. Among them are write, _exit, kill, waitpid… but not printf, nor malloc, nor exit.
SIGCHLD and zombies
When a child terminates, the parent receives SIGCHLD. A server that launches children without wanting to wait for each of them can use it to reap them as they go: the handler (or the main loop, alerted by a flag) calls waitpid(-1, &status, WNOHANG) in a loop, as long as it returns a pid. The loop is necessary: if several children die at the same time, the parent may receive only one SIGCHLD, because a signal that arrives while it is already pending is not counted twice.
SIGPIPE
Writing into a pipe whose read ends are all closed sends SIGPIPE to the writer, which dies. This is what stops yes | head -3: when head has read its three lines and terminated, the next write by yes kills it. A program that wants to handle the error itself ignores SIGPIPE (with sa.sa_handler = SIG_IGN): write then returns -1, with errno equal to EPIPE.
Conclusion
With three system calls, we now know how to do everything:
forkcreates a process, a copy of the parent;execmakes it run another program, keeping its descriptors;waitwaits for it to end and collects its exit status.
Between fork and exec, we prepare the child's descriptors: redirections and pipes. Signals, finally, make it possible to interrupt a process, or to notify it of an event.
Two processes share nothing, except what they open together: they are well isolated, but communicating is expensive for them. The next chapter presents the other way of doing several things at once: several threads of execution in a single process, sharing all the memory, with the difficulties that entails.