Running a Different Program: exec
Chapter Nineteen
Syllabus topic Module 1, "Processes - Operations on Processes"
Pages 73 to 76 of 452
In one line
exec throws away the program a process is running and loads a different one in its place, keeping the same process.
Why it is not "start a program"
The name misleads every beginner, so put it plainly: exec does not create anything. There is one process before and one process after, with the same process id, the same parent, and the same open files. What changed is the program inside it.
A successful exec never returns. There is nothing to return to: the instructions that would have received the return value have been replaced. So the line after an exec runs only if the exec failed, and that is why every correct program treats reaching it as an error.
What survives an exec and what does not
| Survives | Replaced | |
|---|---|---|
| Process id | yes | |
| Parent, and the parent's knowledge of it | yes | |
| Open file descriptors | yes, unless marked close on exec | |
| Current directory, user, group | yes | |
| Text, data, heap, stack | all replaced by the new program's | |
| Registers and program counter | set to the new program's start | |
| Signal handlers | reset: the new program has none of the old one's |
The surviving descriptors are the point. They are how redirection works: the child opens a file as descriptor 1, then execs, and the new program, which knows nothing about any of this, writes to descriptor 1 and the bytes land in the file.
The six forms, and how to remember them
There is one system call, execve, and the C library offers six front doors to it. The names look arbitrary until you learn that the letters after exec are the answer to three questions.
| Letter | Means |
|---|---|
l | the arguments are a list, written out one by one, ending with a null pointer |
v | the arguments are a vector, an array of strings you built |
p | the program is looked for on the path, so a bare name such as ls works |
e | you supply the environment yourself |
| Form | Arguments | Searches the path? | Environment |
|---|---|---|---|
execl | list | no | inherited |
execlp | list | yes | inherited |
execle | list | no | you supply it |
execv | vector | no | inherited |
execvp | vector | yes | inherited |
execve | vector | no | you supply it |
So execlp("ls", "ls", "-l", NULL) is a list, searched on the path. execv("/bin/ls", args) is a vector, with a full path. execve is the real system call, which is why the trace in Chapter ten showed that name whatever the program wrote.
The first argument is the program's own name and must be given twice. execlp("ls", "ls", NULL) looks like a mistake and is not: the first is what to run, the second becomes the new program's argument zero. Leaving the second out gives a program that does not know its own name, and some programs behave differently depending on it.
Running a Different Program: exec
fork then exec, in one program
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
printf("shell : I am %d, about to run wc\n", getpid());
fflush(stdout); /* see the note below */
pid_t child = fork();
if (child < 0) {
perror("fork");
return 1;
}
if (child == 0) {
printf("child : I am %d, and I am about to become wc\n", getpid());
fflush(stdout);
execlp("wc", "wc", "-l", "lines.txt", NULL);
perror("execlp"); /* reached only if exec failed */
_exit(127);
}
int status = 0;
wait(&status);
printf("shell : child %d finished with status %d\n", child,
WEXITSTATUS(status));
return 0;
}one
two
three$ gcc -std=c17 -Wall -Wextra -o forkexec forkexec.c
$ ./forkexec
shell : I am 24, about to run wc
child : I am 25, and I am about to become wc
3 lines.txt
shell : child 25 finished with status 0Three observations, and each is examinable.
- The child printed a line and then stopped being itself. The
printfafterexeclpnever
ran, and neither did perror, because the exec succeeded and the program was gone.
wcprinted as process 21, the same process the child was. The parent waited for 21 and
got it.
fflushis not decoration. Chapter nine showed thatprintfbuffers. A buffer is part of
the program's data, so exec throws it away unflushed, and the line would have been lost. The same happens across fork, where the buffer is copied and the line is printed twice.
That third point is the bug this chapter exists to prevent. A printf before a fork or an exec, with output going to a file, is printed twice or not at all. Flush, or use write.
Redirection, which is the whole reason for the gap
Now the thing Chapter eight promised. The child changes descriptor 1 before exec, and the new program never knows.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t child = fork();
if (child == 0) {
int fd = open("count.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); /* make the file descriptor 1 */
close(fd);
execlp("wc", "wc", "-l", "lines.txt", NULL);
_exit(127);
}
wait(NULL);
printf("wc wrote nothing to the screen. the file holds:\n");
fflush(stdout);
execlp("cat", "cat", "count.txt", NULL);
return 0;
}$ gcc -std=c17 -Wall -Wextra -o redirect redirect.c
$ ./redirect
wc wrote nothing to the screen. the file holds:
3 lines.txtwc was given no file to write to and no flag. It wrote to descriptor 1, as it always does. The child had quietly made descriptor 1 be the file, with dup2, before becoming wc. That is exactly what the shell does when you type wc -l lines.txt > count.txt, and it is why fork and exec are two calls.
Running a Different Program: exec
Notice the last line of the parent: it execs cat, so the parent becomes cat and never returns. There is no return 0 reached. The process ends as cat.
Distinctions that carry marks
fork | exec | |
|---|---|---|
| Creates a process | yes | no |
| Processes afterwards | two | one |
| Returns | twice | never, if it succeeds |
| The program | unchanged in both | replaced |
| Process id | the child gets a new one | unchanged |
execl family | execv family | |
|---|---|---|
| Arguments written as | a list in the call, ending in NULL | an array you built |
| Use when | you know the arguments while writing | the arguments are computed |
What it does not mean
exec is not slow because it loads a program. On a paged system it maps the file rather than reading it, and pages arrive as they are touched (Chapter eighty one).
A failed exec is not a crash. It returns -1 like any call, and the program is still the old one, which is why the error must be handled: usually by exiting, because the child has nothing useful left to do.
exec does not close your files. They survive, on purpose. A descriptor that should not survive is marked close on exec when it is opened.
Quick revision
execreplaces the program running in a process. It creates nothing, and
a successful exec never returns.
- Survives: process id, parent, open descriptors, current directory, user. Replaced: text, data,
heap, stack, registers, signal handlers.
- Six forms, from three letters:
llist,vvector,psearch the path,esupply the
environment. execve is the real system call.
- The first argument is given twice: what to run, and what the program's own argument zero should
be.
- Flush before
forkorexec, or a bufferedprintfis lost or printed twice. - Redirection is done in the gap between
forkandexec, withdup2, and the new program
never knows.
Test yourself
- What does
execdo, and how many processes exist afterwards? It replaces the program
running in the calling process. One process exists, with the same id.
- Why does a successful
execnever return? Because the code that would receive the return
value has been replaced by the new program.
- What does the line after
execmean? That theexecfailed. It is an error path, and
should report and exit.
- Explain the letters in
execlp.lfor a list of arguments written out in the call,p
for searching the path so that a bare program name works.
- How does the shell implement
command > file? It forks; in the child it opens the file
Running a Different Program: exec
and makes it descriptor 1 with dup2; then it execs the command, which writes to descriptor 1 as usual and knows nothing about the redirection.
- Why must you flush before forking? Because the C library's output buffer is part of the
process's data. After a fork both copies hold it and both print it; after an exec it is thrown away unprinted.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.