﻿# exec, system and waiting for children

> waitpid and the status macros, exec that puts a new program into the same process, the fork + exec + wait pattern every shell uses, what survives exec, and system() as the shortcut through /bin/sh.

Source: https://laszlonemes.com/university/operating-systems/exec-wait · Author: László Nemes

Session 3 · Operating Systems

waitpid and the status macros, exec that puts a new program into the same process, the fork + exec + wait pattern every shell uses, what survives exec, and system() as the shortcut through /bin/sh.

17 min read `wait, waitpid and WEXITSTATUS` `Concurrent parent and child, zombies` `The exec family: l, v, p, e` `fork + exec + wait` `What survives exec` `system() and its costs`

[The previous session](https://laszlonemes.com/university/operating-systems/processes-fork) created processes with `fork`, but every child ran the same program as its parent. This session completes the picture. `wait` and `waitpid` let the parent find out when and how a child ended. `exec` replaces the program running in a process with another one – which is how the shell starts every command you type. And `system` wraps all of it into one call, with a shell in between.

> **The model in one breath:** `fork` makes a new process, `exec` puts a new program into it, `exit` ends it with a code, and `wait` collects that code in the parent. The PID and the open files survive `exec`; the memory does not.

After this session you can

- Wait for a specific child with waitpid, and decode the status with WIFEXITED/WEXITSTATUS and WIFSIGNALED/WTERMSIG.
- Explain the output of a parent and a child running at the same time, and when a child is a zombie.
- Start another program with execv or execlp, pass its arguments correctly, and handle a failed exec.
- Write the fork + exec + wait pattern and say what a process keeps and loses across exec.
- Use system() for a quick command, and know what it costs compared with fork + exec.

## wait, waitpid and the exit status

`wait(&status)` blocks until one of the caller's children ends, then returns that child's PID and removes its zombie. `waitpid(pid, &status, options)` does the same for one specific child. Both return `-1` on an error – most often `ECHILD`: there is no child left to wait for. Both live in `sys/wait.h`.

| Call | Waits for | Notes |
| --- | --- | --- |
| `wait(NULL)` | any one child | Throws the status away – fine when you only need the child to be finished. |
| `wait(&status)` | any one child | Stores how it ended in status. |
| `waitpid(pid, &status, 0)` | the child with that PID | Use the PID that `fork` returned. `pid = -1` means any child, like `wait`. |
| `waitpid(pid, &status, WNOHANG)` | nobody – it only checks | Returns `0` immediately if the child is still running. |

The `status` is not the exit code. It packs several facts into one `int`: whether the child exited normally or was killed by a signal, and the exit code or the signal number. The macros of `sys/wait.h` take it apart (Fig. 1).

> **Fig. 1** · status is an encoded int. Ask the macros: did it exit (and with what code), or was it killed (and by which signal)?
>
> *Diagram:* waitpid fills an int status that encodes how the child ended. Do not print it directly: WIFEXITED tells whether the child called exit, and WEXITSTATUS gives the exit code; WIFSIGNALED tells whether a signal killed it, and WTERMSIG gives the signal number.

fork2_mashogy.c – waiting for one child and reading its status

```c
if (waitpid(child1_pid, &status, 0) == -1)
{
    perror("waitpid child1 failed");
}
else if (WIFEXITED(status))
{
    printf("Child1 terminated normally with exit status: %d\n", WEXITSTATUS(status));
}
else
{
    printf("Child1 terminated abnormally\n");
}
```

The `else` branch covers the other way to end: a signal. To say which one, add a `WIFSIGNALED` branch – signals are the topic of [the next session](https://laszlonemes.com/university/operating-systems/signals):

```c
else if (WIFSIGNALED(status))
{
    printf("killed by signal %d\n", WTERMSIG(status));   // e.g. 9 after kill -9
}
```

| Macro | Meaning |
| --- | --- |
| `WIFEXITED(status)` | True if the child ended with `exit` or by returning from `main`. |
| `WEXITSTATUS(status)` | The exit code, 0–255. Only valid if `WIFEXITED` is true. |
| `WIFSIGNALED(status)` | True if a signal killed the child. |
| `WTERMSIG(status)` | The number of that signal. Only valid if `WIFSIGNALED` is true. |

Common mistake

**Printing status as the exit code**

A child that calls `exit(1)` gives a `status` of 256 on Linux, not 1. Printing `status` directly – or comparing it with 1 – is a classic bug. Always go through `WIFEXITED` and `WEXITSTATUS`. And remember that only 8 bits travel: `exit(-1)` arrives as 255, so keep exit codes between 0 and 255 – 0 for success, small positive numbers for errors.

### Waiting for all children

When you started several children and don't care about the order, loop until `wait` reports that there are none left. Each round collects one child:

```c
pid_t pid;
int status;
while ((pid = wait(&status)) > 0)          // any child; -1 (ECHILD) when none is left
{
    if (WIFEXITED(status))
        printf("child %i ended with %d\n", pid, WEXITSTATUS(status));
}
```

With `WNOHANG`, `waitpid` becomes a question instead of a wait: has it ended yet? The parent can keep working and check from time to time:

```c
pid_t r = waitpid(pid, &status, WNOHANG);
if (r == 0)
    printf("still running\n");       // WNOHANG: don't block, just look
else if (r == pid)
    printf("ended\n");               // status is filled, the zombie is gone
```

## Parent and children at the same time

Nothing makes a parent wait for its child unless it calls `wait`. In the counter example both processes count, one line per second – the child to 10, the parent to 20 – and their lines interleave on the terminal:

counter.c – two loops at the same time

```c
child1();                       // the child counts 0..9, one per second, then exit(0)
int count_parent = 0;
while (count_parent < 20)       // meanwhile the parent counts 0..19
{
    printf("Parent Process: %i\n", count_parent);
    sleep(1);
    count_parent++;
}
wait(NULL);                     // the child has been a zombie since second 10
```

```text
Parent Process: 0
This is a Child Process!
Child Process: 0
Parent Process: 1
Child Process: 1
...
Child Process: 9
Parent Process: 10
Parent Process: 11
...
```

The child finishes at second 10, but the parent is still in its loop and calls `wait` only at second 20. For those ten seconds the child is a zombie – run `ps -u $USER -o pid,ppid,stat,cmd` from another terminal and you see it with state `Z` and `<defunct>` (Fig. 2).

> **Fig. 2** · The child ends at 10 s and waits, as a zombie, for the parent's wait at 20 s.
>
> *Diagram:* In the counter example the child counts for 10 seconds and the parent for 20. The child ends at second 10 but its parent is still busy, so it stays a zombie until the parent calls wait at second 20.

### Two children: nested or in functions

The week's examples start two children in two styles. One nests the second `fork` inside the parent's branch of the first, so only the parent forks again:

fork2.c – nested branches

```c
pid_t child = fork();
if (child > 0)                          // only the parent forks the second time
{
    pid_t child2 = fork();
    if (child2 > 0)
    {
        waitpid(child, &status, 0);
        waitpid(child2, &status, 0);
        printf("The value is %i in parent process\n", notacommonvalue);
    }
    else { notacommonvalue = 16; /* child2 */ }
}
else { notacommonvalue = 5; /* child 1 */ }
```

The other puts each child into a function that forks, lets the child do its job and `exit`, and returns the PID to the parent – the style of [the previous session](https://laszlonemes.com/university/operating-systems/processes-fork). Nesting gets unreadable beyond two children; the function style scales, and it keeps each child's code in one place. Either way, the parent waits with `waitpid` for each PID it got, because a plain `wait` returns whichever child ends first (Fig. 3).

> **Fig. 3** · wait takes the first child that ends; waitpid takes the one you name.
>
> *Diagram:* wait returns as soon as any child ends, so with two children it may return the one you did not mean. waitpid with a PID waits for exactly that child; waitpid with -1 behaves like wait.

## exec: a new program in the same process

`fork` copies the running program. To run a *different* program, a process calls one of the `exec` functions. `exec` does not create a process: it loads the new program into the calling one, replacing its code, data, heap and stack. The PID stays, the parent stays, the open files stay (Fig. 4).

> **Fig. 4** · Same PID before and after exec – but the program, its memory and its stdio buffer are new.
>
> *Diagram:* exec does not create a process. It loads a new program into the calling process: the code, data, heap, stack and stdio buffers of the old program are thrown away, while the PID, the parent and the open file descriptors stay the same.

Because the old program is gone, a successful `exec` never returns. The line after it runs only if `exec` failed – the file does not exist, is not executable, or the path is wrong. That line must report the error and end the child; otherwise the child continues with the parent's code, as with a forgotten `exit` after `fork`.

exec.c – the child runs ./write

```c
char *cmd = "./write";
char *arg[] = {"./write", "Operating Systems", "5", NULL};   // NULL ends the list
printf("./write program will be called by execv function\n");
fflush(stdout);              // the buffer would be thrown away with the old program
execv(cmd, arg);
perror("execv");             // only reached if execv failed
exit(1);
```

The arguments go into an array of strings that ends with `NULL`. By convention the first element is the program's name: it becomes `argv[0]` of the new program, and the real arguments follow. The started program is the small `write.c`, which prints its first argument as many times as the second says:

write.c – check argc, then print

```c
if (argc < 3)
{
    fprintf(stderr, "Usage: %s text count\n", argv[0]);
    return 1;
}
for (i = 0; i < atoi(argv[2]); i++)
    printf("%s\n", argv[1]);
```

Both programs have to be compiled, and the binary must be called exactly what `execv` looks for:

```shell
gcc -Wall write.c -o write     # the program exec will start – name it exactly "write"
gcc -Wall exec.c -o exec
./exec
```

```text
$ ./exec
./write program will be called by execv function
Operating Systems
Operating Systems
Operating Systems
Operating Systems
Operating Systems
The end of parent process
```

the same, without the write binary

```text
$ ./exec            # write was not compiled
./write program will be called by execv function
execv: No such file or directory
The end of parent process
```

Common mistake

**Unflushed output disappears at exec**

The stdio buffer is part of the old program's memory, so `exec` throws it away. To a terminal you won't notice – the line was flushed at its `'\n'`. Into a file or a pipe, the line printed just before `exec` is simply lost. `fflush(stdout)` before `exec`, just as before `fork`.

### The exec family

There are six variants, and the letters after `exec` say how to call them (Fig. 5). They all end up in the same system call, `execve`.

> **Fig. 5** · Read the name: l or v for the argument format, p for PATH search, e for an explicit environment.
>
> *Diagram:* The name of an exec function tells how to call it: l means the arguments are listed one by one ending in NULL, v means they are in a NULL-terminated array, p means the program is searched for in PATH, and e means the environment is passed explicitly.

```c
execl("/bin/ls", "ls", "-l", NULL);          // l: arguments listed, full path
execlp("ls", "ls", "-l", NULL);              // p: look the program up in PATH

char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);                      // v: arguments in an array
execvp("ls", args);                          // v + p
execve("/bin/ls", args, envp);               // e: pass the environment explicitly
```

| Letter | Meaning | Use it when |
| --- | --- | --- |
| `l` | Arguments listed one by one, ending with `NULL`. | The number of arguments is fixed in the code. |
| `v` | Arguments in a `NULL`-terminated array (vector). | The arguments are built at run time, e.g. from user input. |
| `p` | Search the directories of `PATH` for the program name. | Starting a command like `ls` or `grep` without its full path. |
| `e` | Pass the environment variables explicitly. | The new program needs a different environment; without e it inherits the current one. |

Common mistake

**The missing NULL**

`execl("/bin/ls", "ls", "-l")` without the final `NULL` compiles (at most with a warning) and then reads past the arguments, picking up garbage as further arguments. The array for `execv` needs its `NULL` element just the same – it is how `exec` knows where the list ends.

- **exec** – A family of calls that replace the program of the calling process. Same PID, new code and memory.
- **argv[0]** – The first argument passed to exec: by convention the program name, not a real argument.
- **PATH** – The list of directories the shell – and the p variants of exec – search for a command.
- **exit code** – The number a process ends with (0–255); 0 means success. The parent reads it with WEXITSTATUS.

## fork + exec + wait

Put together, the three calls give the pattern every shell uses to run a command: `fork` a child, `exec` the program in the child, `wait` in the parent (Fig. 6). The parent survives – it was never replaced – and learns how the command went from the exit status.

> **Fig. 6** · The parent waits; the child becomes the other program; its exit code comes back through waitpid.
>
> *Diagram:* The fork, exec, wait pattern: the parent forks, the child replaces itself with another program using exec, the program runs and exits, and the parent, blocked in waitpid, wakes up with the child's exit status. The shell runs every command this way.

```c
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid == 0)
{
    execlp("ls", "ls", "-l", NULL);   // the child becomes ls
    perror("execlp");                 // exec returned: it failed
    exit(127);
}
int status;
waitpid(pid, &status, 0);            // the parent waits, like the shell does
if (WIFEXITED(status))
    printf("ls ended with %d\n", WEXITSTATUS(status));
```

This is why `echo $?` in the shell shows the exit code of the last command: bash got it from `waitpid` through `WEXITSTATUS`. The `127` after a failed `exec` follows the shells' convention for "command not found".

## What survives exec

`exec` replaces the program, not the process. Everything the kernel keeps about the process stays; everything that lives in the old program's memory goes:

| Kept across exec | Replaced or reset |
| --- | --- |
| PID and parent PID | Code, global variables, heap, stack |
| Open file descriptors (unless opened with `O_CLOEXEC`) | stdio buffers – unflushed output is lost |
| Current directory, user, umask | Signal handlers – reset to the default (ignored signals stay ignored) |
| Environment variables (unless an `e` variant passes new ones) | Memory from `malloc`, without `free` – it simply disappears |

The kept file descriptors are the key to redirection. A shell running `ls > out.txt` forks, opens `out.txt` in the child, puts it on descriptor 1, and then execs `ls`. `ls` writes to descriptor 1 as always and never learns that it is a file. The call that moves a descriptor is `dup2`:

in the child – redirect, then exec

```c
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1);                     // descriptor 1 now refers to out.txt
close(fd);
execlp("ls", "ls", "-l", NULL);  // ls writes to descriptor 1 – into the file
```

Good to know

**Signals across exec**

A handler function is part of the old program's code, so after `exec` it no longer exists – the kernel resets handled signals to their default action. A signal set to be ignored stays ignored. More on this in [the signals session](https://laszlonemes.com/university/operating-systems/signals).

## system(): the shortcut through the shell

`system("command")` from `stdlib.h` does the whole fork + exec + wait in one call. It forks, execs the shell `/bin/sh -c "command"` in the child, waits for it, and returns – so, unlike `exec`, your program continues after it (Fig. 7).

system.c – run a command line, then continue

```c
printf("Call ./write program by calling system function\n");
fflush(stdout);                                   // keep the order of the output
int rc = system("./write 'Operating Systems' 5");  // returns when the command has ended
if (rc == -1)
    perror("system");                             // the child could not be created
else if (WIFEXITED(rc))
    printf("exit code: %d\n", WEXITSTATUS(rc));
```

> **Fig. 7** · exec replaces the child; system adds a shell and waits for it, then the caller continues.
>
> *Diagram:* exec turns the child itself into ./write. system creates more: it forks a shell, /bin/sh -c with the command line, the shell starts ./write, and system waits for the shell before it returns to the caller, which then continues its own code.

Because a shell reads the command, you can use everything the shell offers: quotes (`'Operating Systems'` stays one argument), pipes, redirection, wildcards. The return value is a status like the one `waitpid` fills – decode it with the same macros – or `-1` if no child could be created. If the shell cannot find the command, the exit code is 127.

|  | fork + exec + wait | system() |
| --- | --- | --- |
| Lines of code | about ten | one |
| Processes | one child | a child running a shell, which starts the command |
| Arguments | an exact array – no parsing, no surprises | one string, split and interpreted by the shell |
| Shell features (\|, >, *) | no – do it yourself (dup2, pipe) | yes |
| Control | PID, parallel children, WNOHANG, signals | blocks until the command ends |

Common mistake

**User input in a system() command**

The string goes to a shell, and the shell interprets `;`, `|`, `&` and quotes. If part of it comes from the user, the user can append a command of their own:

```c
char cmd[200];
snprintf(cmd, sizeof(cmd), "ls %s", name);   // name typed by the user: "x; rm -r ~"
system(cmd);                                 // the shell runs ls x – and then rm -r ~
```

For anything built from input, use `fork` and `execvp` with an argument array – each element reaches the program as one argument, and nothing is interpreted.

Good to know

**Flush before system too**

The command writes straight to the terminal or file, while your `printf` output may still be in your buffer. Without a `fflush(stdout)` before `system`, output redirected to a file shows the command's lines first and your line after them.

In your assignment

**When your program starts processes**

- Every `fork` has a matching `wait`/`waitpid` – keep the PIDs that `fork` returned and wait for those.
- A child that should end ends with `exit` and a meaningful code; the parent checks it with `WIFEXITED`/`WEXITSTATUS`.
- After every `exec`: `perror` and `exit`. Before every `fork`, `exec` and `system`: `fflush(stdout)`.
- Prefer `fork` + `exec` over `system` when the program takes any part of the command from the user.

## Practice

Compile with `-Wall -Wextra`. For the exec tasks, run `which ls` first to see where the program lives – you need the full path for the variants without `p`.

Task 1

**Exit codes travel**

The child exits with the number given as the program's argument; the parent waits and prints the code it received. Try 0, 3, 255, 256 and -1. Explain the last two results. Then print the raw `status` as well and compare.

**Approach:**

`exit` in the child, `waitpid` + the macros in the parent. Only the lowest 8 bits arrive, so 256 becomes 0 and -1 becomes 255. The raw status is the code multiplied by 256.

```c
pid_t pid = fork();
if (pid == 0)
    exit(atoi(argv[1]));             // the child's whole job
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
    printf("%d\n", WEXITSTATUS(status));
```

Task 2

**Killed or exited?**

Let the child sleep for a minute. While it sleeps, end it from another terminal with `kill -9 <PID>`. The parent should print either "exited with N" or "killed by signal N". Repeat with a plain `kill`: which signal number appears now?

**Approach:**

Print the child's PID from the parent so you know what to kill. `WIFSIGNALED` + `WTERMSIG` gives 9 for `kill -9` and 15 (`SIGTERM`) for a plain `kill`.

```c
if (pid == 0)
{
    sleep(60);                       // time to run kill -9 <pid> from another terminal
    exit(0);
}
```

Task 3

**ls in a child**

Run `ls -l` in a child process; the parent waits and then prints `done, exit code N`. Write it once with `execl` and once with `execvp`. Then misspell the program name and check that your error message appears and that the parent still prints its line.

**Approach:**

The fork + exec + wait fragment on this page. With `execl` the path must be complete (`/bin/ls` or `/usr/bin/ls`); with `execvp` the name is enough. Both need `"ls"` as the first argument and `NULL` at the end.

Task 4

**A minimal shell**

Read a line with `fgets`, run it as a program without arguments in a child, wait, and read the next line – until the user types `exit` or presses Ctrl+D. Test with `ls`, `date`, `pwd` and a name that does not exist. Extension: split the line at spaces into an array and use `execvp`, so that `ls -l` works.

**Approach:**

Cut the `'\n'` with `strcspn`, compare with `"exit"`, then fork. The child part is three lines; the parent part is a `waitpid`. For the extension, `strtok(line, " ")` in a loop fills the array – end it with `NULL`.

```c
if (pid == 0)
{
    execlp(line, line, NULL);        // one word: program name = argv[0]
    perror(line);                    // e.g. "foo: No such file or directory"
    exit(127);                       // what shells use for "command not found"
}
```

Task 5

**system versus exec**

Run `ls -l | wc -l` with `system` and print the exit code. Then try to do the same with `execlp("ls", "ls", "-l", "|", "wc", "-l", NULL)`. Why does the second one fail, and what does `ls` complain about?

**Explanation:**

The pipe is a shell feature. `system` hands the line to `/bin/sh`, which builds the pipeline. `exec` starts `ls` directly, and `ls` receives `|`, `wc` and `-l` as file names it cannot find. Building a pipeline yourself takes `pipe` and `dup2`.

Task 6

**Poll instead of waiting**

Start a child that sleeps 5 seconds. The parent prints a dot every half second until the child has ended, then prints its exit code. Use `waitpid` with `WNOHANG` and `usleep`.

**Approach:**

Loop while `waitpid(pid, &status, WNOHANG)` returns 0; after the loop the status is filled. Print each dot with `fflush(stdout)`, since it has no `'\n'`.

## Summary

- `wait` takes any child, `waitpid(pid, …)` a specific one; both remove the zombie. `WNOHANG` only checks.
- Decode the status with `WIFEXITED`/`WEXITSTATUS` and `WIFSIGNALED`/`WTERMSIG` – never print it raw. Exit codes are 0–255.
- Parent and child run at the same time; a child that ended before the parent's `wait` is a zombie until then.
- `exec` replaces the program, keeps the process: same PID, same open file descriptors, new memory. It returns only on failure – handle that.
- The exec name says the calling convention: `l`/`v` arguments, `p` PATH search, `e` environment. Argument lists start with the program name and end with `NULL`.
- fork + exec + wait is how a shell runs a command; `system` does the same through `/bin/sh -c` – convenient, but not for user input.
- Flush stdout before `fork`, `exec` and `system`.
