What is Exec Family System Calls in Linux- Free Linux Device Drivers Course In Hyderabad

PREV_LECNEXT_LEC

Exec Family System Calls
How Linux replaces a running process image — a hands-on tour of exec system calls Linux programs rely on to launch new programs

Every shell you have ever typed a command into does the same two-step trick: it forks itself, then replaces the copy with the program you asked for. That second step is the job of the exec family. This lecture, part of EmbeddedPathashala’s free Linux device drivers course track on process management, covers exec system calls Linux developers use every day, updated for the current glibc and kernel behavior rather than any decades-old reference.

If you’ve ever wondered how a shell runs ls, how redirection like > output.txt actually works under the hood, or how container runtimes launch the process a container ultimately runs, the answer runs directly through this lecture.

What You Will Learn

The six exec() variants and when to use each How exec replaces a process image Building a minimal shell with fork + exec Redirecting stdout with dup2() Why fork+exec instead of one combined call

Prerequisites

This lecture builds directly on process termination in Linux (the previous lecture in this series) and assumes you already understand fork(). You’ll need a Linux machine with gcc and a shell — any modern kernel works, since the exec family’s behavior at the API level hasn’t changed in decades.

What exec() Actually Does

Unlike fork(), which duplicates a running process, exec() does the opposite: it discards the current process image entirely — code, data, heap, most of the stack — and loads a new program in its place, in the same process (same PID, same open file descriptors unless marked close-on-exec). If exec() succeeds, it never returns to the calling code; execution resumes at the main() of the newly loaded program. If it fails, it returns -1 and the original program keeps running, which is why every serious use of exec() checks its return value.

The Six exec() Variants

glibc provides six functions in this family, and the differences all come down to three independent choices: do you pass arguments as a list or an array, do you search PATH for the program, and do you supply a custom environment?

FunctionArgumentsSearches PATH?Custom Environment?
execl()Variadic listNoNo (inherits caller’s)
execlp()Variadic listYesNo
execle()Variadic listNoYes
execv()Array (argv[])NoNo
execvp()Array (argv[])YesNo
execvpe()Array (argv[])YesYes

The naming is a mnemonic: l for list-style arguments, v for vector (array) arguments, p for “search PATH,” and e for “takes an explicit environment.” Once you know that, you can reconstruct the whole table from the function name alone.

A Minimal Command Launcher

Here’s an original demo, ep_minishell.c, that reads a command name, forks, and execs it — the same fork+exec pattern every real shell uses:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    char cmdline[128];
    pid_t pid;
    int status;

    while (1) {
        printf("ep-mini$ ");
        if (!fgets(cmdline, sizeof(cmdline), stdin))
            break;
        cmdline[strcspn(cmdline, "\n")] = 0;

        if (strlen(cmdline) == 0)
            continue;
        if (strcmp(cmdline, "quit") == 0)
            break;

        pid = fork();
        if (pid == 0) {
            /* child: replace this process image with the command */
            execlp(cmdline, cmdline, (char *)NULL);
            /* only reached if execlp() failed */
            fprintf(stderr, "ep-mini: %s: command not found\n", cmdline);
            exit(127);
        } else if (pid > 0) {
            waitpid(pid, &status, 0);
        } else {
            perror("ep-mini: fork failed");
        }
    }

    return 0;
}
$ gcc -Wall -o ep_minishell ep_minishell.c
$ ./ep_minishell
ep-mini$ date
Tue Aug 25 09:14:02 IST 2026
ep-mini$ whoami
ravi
ep-mini$ quit

Notice execlp() was chosen specifically because it searches PATH — that’s why typing date works without a full path like /usr/bin/date.

Why Two Calls Instead of One

It’s a fair question: why does Linux split “run a new program” into a duplicate (fork) plus a replace (exec) instead of one combined syscall? The answer is that the gap between the two steps is exactly where a shell does useful work — redirecting file descriptors, setting up pipes, changing the working directory — all without the target program needing to know or care.

Shell Redirection: ls > listing.txt
Shell forks a copy of itself Child opens/truncates listing.txt Child calls dup2() to point fd 1 (stdout) at listing.txt Child calls execvp(“ls”, …) — process image is replaced ls writes its output to fd 1, unaware it is now a file ls terminates, parent shell regains control via waitpid()

Here’s that redirection step in code — ep_redirect_demo.c runs ls with its output sent to a file instead of the terminal, without ls itself changing at all:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid = fork();

    if (pid == 0) {
        int fd = open("listing.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd < 0) {
            perror("ep_redirect_demo: open");
            _exit(1);
        }
        dup2(fd, STDOUT_FILENO);   /* stdout now points at listing.txt */
        close(fd);
        execlp("ls", "ls", "-l", (char *)NULL);
        perror("ep_redirect_demo: exec");
        _exit(1);
    }
    waitpid(pid, NULL, 0);
    printf("ep_redirect_demo: done, check listing.txt\n");
    return 0;
}

Common Mistakes and Troubleshooting

MistakeSymptomFix
Forgetting the NULL terminator in execl()/execlp()Crash or garbage arguments passed to the new programAlways terminate the argument list with (char *)NULL
Using execl() when you meant execlp()“No such file or directory” for commands that clearly existUse the p variant when you want PATH search
Not checking exec’s return valueCode after exec runs unexpectedly on failureTreat any return from a successful exec as impossible; handle the error path explicitly
Leaking unwanted file descriptors into the new programNew program can read/write files it shouldn’t have access toSet FD_CLOEXEC on descriptors that shouldn’t survive exec

Best Practices

  • Prefer the v (array) variants in code that builds argument lists programmatically — it avoids fragile variadic call sites.
  • Always fork before exec unless you genuinely want your current process replaced (rare outside of shells and launchers).
  • Set O_CLOEXEC / FD_CLOEXEC on sensitive file descriptors so they don’t leak into child programs.
  • Check every exec call’s return value — a successful exec never returns, so any return means failure.

Performance consideration: exec is not free — it involves loading and linking a new executable image, so tight loops that spawn many short-lived processes should batch work instead of forking per item where possible.

Security consideration: never pass unsanitized user input to execlp()/execvp() as the command name, and prefer array-based exec calls over building a single shell command string, which avoids a whole class of shell-injection bugs.

Real-World Use Cases

Shells (bash, zsh), build systems invoking compilers, container runtimes launching the containerized entrypoint, and process supervisors like systemd all rely on exec system calls in Linux to hand control to another program while keeping the surrounding process machinery (PID, file descriptors, redirection) intact.

Summary and Key Takeaways

  • exec() replaces the current process image; it doesn’t create a new process.
  • Six variants exist, distinguished by argument style (l/v), PATH search (p), and custom environment (e).
  • The fork-then-exec split is what lets a shell set up redirection and pipes before the target program runs.
  • A successful exec never returns; code after it only runs on failure.

Conclusion

The exec family is deceptively small — six functions differing by three simple axes — but it’s the mechanism underneath nearly every program launch on a Linux system. Combined with what you learned about process termination in the previous lecture, you now have the two halves of the fork/exec/wait pattern that every shell, build tool, and process supervisor is built on. Next in this free Linux kernel development course, we’ll cover daemonizing a process properly.

Frequently Asked Questions

Does exec() create a new process?

No. It replaces the code and data of the calling process in place — same PID, same process table entry, just a different program running inside it.

What happens to open file descriptors across exec()?

They stay open by default, which is exactly what enables redirection tricks with dup2(). Descriptors marked FD_CLOEXEC are the exception — those are automatically closed on exec.

Which exec() variant should I use by default?

execvp() is the most commonly useful default: it takes an argv array (easy to build programmatically) and searches PATH like a shell would.

Why does my program keep running after a failed exec() call?

That’s expected — exec() only fails to return when it fails. Always check the return value and handle the error rather than assuming exec always replaces the process.

Can I exec() a shell script directly?

Yes, as long as it starts with a shebang line like #!/bin/bash — the kernel reads that line and re-execs the specified interpreter with your script as an argument.

Is fork() followed by exec() slower than a hypothetical combined call?

There is some overhead, largely mitigated by copy-on-write fork() implementations on Linux, but the flexibility of doing setup work between fork and exec is considered well worth the cost.

Continue the Free Linux Kernel Development Course

This lecture is part of EmbeddedPathashala’s free embedded Linux course covering process management from first principles.

Browse the Full Course Join the Community

PREV_LECNEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *