JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

exec(): Becoming a Different Program

fork() gave you a second copy of yourself; exec() does something stranger — it throws away your program entirely and starts running a different one, without changing who you are. Watch the process keep its identity while its whole inside is replaced, and see why fork-then-exec is the move behind every command your shell runs.

fork() copies you; exec() replaces you

From the previous guide you know what fork() does: it splits one process into two near-identical ones, a parent and a child, each with its own copy of the same memory, running the same program. That is powerful, but notice the limit — both halves are still the same program. fork() alone can only ever give you more copies of the code you are already running. It has no way to start, say, the `ls` program or the `gcc` compiler. Yet running other programs is precisely what a shell, or any launcher, must do. That gap is exactly the gap exec() fills.

Here is the idea, stated plainly. exec() takes the process you are in right now and replaces the program inside it. It discards your current code, your current data, your heap, your stack — the whole process image — and in their place loads a completely different executable from disk and begins running that program from its start. The crucial, strange part: the process does not die and a new one is not born. It is the same process the whole way through. Think of it less like being replaced and more like a single actor walking offstage in one costume and walking back on as an entirely different character — same body, new role.

What survives, and what is wiped

If exec() throws away the whole program inside the process, what exactly is left standing? The answer is the part that belongs to the process, not to the program. The process ID is unchanged — call getpid() before and after a successful exec() and you get the same number, which is the cleanest proof that it is one continuous process. The parent–child relationship is unchanged, so whoever was waiting on this process is still waiting on it. And, by default, the open files survive: every file descriptor you had open stays open across the exec, pointing at the same file or pipe.

That last point is not a curiosity — it is the mechanism behind shell redirection and pipes, and we will lean on it in a moment. Everything else, though, is gone. The new program gets a brand-new, freshly initialized memory image: a new code segment loaded from the new executable, fresh global data, an empty heap, and a fresh stack. Any variable you set, any memory you malloc()'d, any function you were halfway through — all of it vanishes the instant exec() succeeds, because that memory belonged to the old program, and the old program no longer exists. There is no "return to where we were"; there is nowhere to return to.

This is why a successful exec() never returns. Ordinary functions return to their caller; exec() has no caller to return to, because the code that called it has just been overwritten. So in your source, any line written after a call to exec() runs only if exec failed. If the executable does not exist or you lack permission, exec returns -1 and sets errno; if it succeeds, control simply never comes back. That inverted logic — "the code after exec is the error path" — surprises everyone the first time, so let it surprise you now rather than in a debugger later.

fork-then-exec: how every command actually runs

Now put the two together. A shell wants to run `ls` without destroying itself — it must launch the new program but still be there afterward, ready for your next command. fork() alone keeps the shell alive but only runs more shell. exec() alone would run `ls` but obliterate the shell in doing so. The answer is the classic fork-then-exec pattern: fork() first to make a child copy, then have the child call exec() to become `ls`, while the parent shell stays itself and waits. The child is the costume-change actor; the parent never leaves the stage.

pid_t pid = fork();
if (pid < 0) {
    perror("fork");          /* fork itself failed */
    return 1;
}
if (pid == 0) {
    /* --- child: become a different program --- */
    char *args[] = { "ls", "-l", NULL };   /* argv, NULL-terminated */
    execvp("ls", args);
    /* only reached if exec FAILED: */
    perror("execvp");
    _exit(127);              /* child must not fall back into parent's code */
}
/* --- parent: still itself, wait for the child to finish --- */
int status;
waitpid(pid, &status, 0);
The fork-then-exec skeleton: the child execs and never returns on success; the lines after execvp() run only on failure.

Walk the picture once. After fork(), two processes run this code. In the child, pid is 0, so it enters the branch, builds an argument list, and calls execvp("ls", args) — at which point the child stops being your program and starts being ls, with the same PID it had a microsecond ago. In the parent, pid is the child's PID, so it skips the child branch and goes straight to waitpid(), pausing until the child (now `ls`) finishes. Notice how the open files survive the exec: the child inherited the shell's standard output across both fork and exec, which is why `ls` prints to the same terminal — and why redirecting that descriptor before the exec is exactly how `ls > out.txt` works.

The exec family: passing the name, the arguments, and the environment

There is no single function literally named exec; there is a small family of them — execl, execv, execlp, execvp, execle, execve — and the letters after "exec" are a tidy code for what each one expects. The base of the family is execve(), and underneath, every other variant is just a friendlier wrapper that eventually calls it. The suffixes mean: `l` (list) takes the arguments as separate parameters spelled out one by one; `v` (vector) takes them as a single array of strings; `p` (path) searches the directories in your PATH so you can say "ls" instead of a full path; and `e` (environment) lets you hand the new program a custom set of environment variables.

Two of those arguments are the same two that arrive in the new program's main(), the `int argc` count and the `argv` array of strings — this is the moment that connection finally closes. The string array you pass to execvp() becomes the argv the new program sees, and its length becomes argc. By a long-standing convention, the first element is the program's own name, which is why `args[0]` above is "ls" even though we already named "ls" as the file to run. The array, like argv itself, must be terminated by a NULL pointer so the new program knows where its argument list ends. (The exact C declaration of that parameter belongs in code, not prose — it is the `char ** argv` you see in the snippet above.)

The `e` variants and the surviving-by-default environment deserve a clear word. The environment is a second, parallel list of "name=value" strings — PATH, HOME, LANG, and so on — that every program inherits and can read. Most exec variants pass your current environment through to the new program automatically, which is why a child sees the same PATH you do. execve() and execle() instead let you supply an explicit environment, so you can launch a program with a deliberately controlled set of variables. This is how a shell exports a variable to the commands it runs, and how you can run one program with, say, a different LANG without disturbing your own.

Getting it right: the failure modes worth knowing

Because exec() is the doorway between a process and a new program, the ways it can go wrong are worth naming honestly rather than hiding. The first and most common: exec can simply fail, and you must handle the lines after it. exec fails when the file does not exist, is not executable, is not a valid program, or PATH cannot find it. When this happens in a forked child, the child is still running your program — so it must call _exit() (not just continue) right after the failed exec, or it will fall through and start executing the parent's logic as a rogue duplicate. The skeleton above does this; copying code that omits it is a real and nasty bug.

A second honest caveat: file descriptors surviving exec by default is usually what you want, but not always. Sometimes a child should not inherit a particular open file — leaking a sensitive descriptor into an unrelated program is a real security concern. For that there is a per-descriptor flag, close-on-exec, which tells the kernel to automatically close that descriptor the instant any exec succeeds; you set it with fcntl() or by opening with O_CLOEXEC. The point worth carrying away is just that "descriptors survive exec" is a default, not a law, and careful programs choose deliberately which ones cross the boundary.

Finally, hold the mental model that ties this rung together. fork() answers "make another process"; exec() answers "run a different program"; together they are how new programs start at all. The very next guide picks up the thread we left dangling at waitpid() above — how the parent collects the child's exit status once the exec'd program finishes, and what goes wrong if it forgets. Keep the costume-change image in mind: the child changed character, ran its scene, and the parent is now waiting in the wings to hear how it went.