Chapter 57: Executing New Programs: The exec() Family of Functions

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Understand the fundamental role of the exec() family in Linux process management.
  • Differentiate between the various functions in the exec() family (execlexecvexecleexecveexeclpexecvp) and select the appropriate one for a given task.
  • Implement C programs that use exec() functions to launch new applications on an embedded system.
  • Configure and pass command-line arguments and environment variables to new programs.
  • Debug common errors related to path resolution, argument lists, and permissions when using exec().
  • Integrate fork() and exec() to create robust multi-process applications on a Raspberry Pi 5.

Introduction

In the world of embedded Linux, managing processes is a cornerstone of building robust and efficient systems. While the fork() system call, which we explored previously, is adept at creating a new process by duplicating an existing one, it only accomplishes half the task of launching a new application. The newly created child process is initially just a clone, running the exact same code as its parent. To truly launch a different program, the system needs a mechanism to replace the current process’s memory image with that of a new program. This is the crucial role of the exec() family of functions.

The exec() functions are the engine of program execution in Linux. They are responsible for loading a new program into the current process’s address space, effectively transforming the process into a new one. This concept is fundamental to how shells operate, how system services are started, and how complex, multi-process applications are orchestrated. On an embedded device like the Raspberry Pi 5, where resources are managed carefully, understanding how to control program execution is paramount. Whether you are designing a custom user interface, a background service that launches helper scripts, or a complex control system, the exec() family provides the necessary tools. This chapter will demystify these powerful functions, exploring their variations and providing the practical knowledge needed to wield them effectively in your embedded projects.

Technical Background

To fully appreciate the exec() family, we must first revisit the concept of a process in Linux. A process is more than just executing code; it is a complete execution context managed by the kernel. This context includes the program code (text segment), global and static variables (data and BSS segments), the heap for dynamic memory allocation, the stack for function calls and local variables, and a set of processor registers, including the program counter (PC) and stack pointer (SP). When you run a command like ls in your shell, the shell first calls fork() to create a child process. This child is a near-exact copy of the shell. Then, the child process calls one of the exec() functions to load the ls executable into its memory space. The kernel overwrites the child’s existing memory segments with the new program’s segments, resets the registers, and starts execution from the new program’s entry point (_start, which eventually calls main()).

graph TD
    subgraph Parent Process
        A[Start: Shell Process PID: 500]
    end

    subgraph Child Process
        B[Cloned Shell Process PID: 501] -- "execve(/bin/ls, ...)" --> C{Transformed into New Program<br><i>/bin/ls</i><br>PID remains 501}
    end

    A -- "pid = fork()" --> B;
    A -- "fork() returns Child PID (501)" --> D["Parent Continues Execution<br>e.g., calls waitpid(501, ...)"];
    C -- "Program completes" --> E[Child Process Terminates];
    D -- "Waits for child" --> F[Parent Resumes after Child Terminates];

    classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
    classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    classDef systemNode fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
    classDef endNode fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff

    class A startNode
    class B,D,F processNode
    class C systemNode
    class E endNode

The original process ID (PID) remains the same after an exec() call. This is a critical point: exec() does not create a new process; it transforms the existing one. The old program is completely gone, replaced by the new one. If the exec() call is successful, it never returns to the calling program. The only way code after an exec() call can be reached is if the call fails.

The exec() Family: A Tour of Variations

The “exec” in exec() stands for “execute.” However, there isn’t a single function named exec(). Instead, the C library provides a family of related functions, each offering a slightly different way to specify the program to execute and its arguments. The naming convention of these functions provides clues to their behavior. The letters lve, and p are suffixes added to the exec base name.

  • l (List): The command-line arguments are passed as a variable-length list of string pointers (const char *) directly to the function. This list must be terminated by a NULL pointer. This form is convenient when the number of arguments is known at compile time.
  • v (Vector): The command-line arguments are passed as a NULL-terminated array of string pointers (char *const argv[]). This is more flexible, as the argument array can be constructed dynamically at runtime.
  • e (Environment): This variant allows the caller to explicitly specify the environment variables for the new program. The environment is passed as a NULL-terminated array of strings, where each string is in the format "NAME=value". If this suffix is absent, the new program inherits the environment of the calling process.
  • p (Path): This variant instructs the function to search for the executable file in the directories listed in the PATH environment variable, just like a command shell would. If the filename contains a slash (/), the path search is skipped, and it is treated as a relative or absolute path. If this suffix is absent, a full, absolute path to the executable must be provided.

These suffixes can be combined, leading to the six main functions in the family. Let’s explore each one in detail.

execl() and execv(): The Basic Duo

The simplest members of the family are execl() and execv(). They differ only in how they accept command-line arguments.

int execl(const char *path, const char *arg0, ... /*, (char *) NULL */);

The execl() function requires the full path to the executable as its first argument. Subsequent arguments are the command-line arguments for the new program. By convention, the first argument (arg0) should be the name of the executable itself. This is what the program sees as its name, which is important for programs that change their behavior based on how they are invoked. The list of arguments must be terminated by a NULL pointer.

Imagine you want to execute the command /bin/ls -l /home/pi. Using execl(), the call would look like this:

execl("/bin/ls", "ls", "-l", "/home/pi", NULL);

This is straightforward and readable, but its static nature makes it unsuitable for situations where the number of arguments is determined at runtime.

int execv(const char *path, char *const argv[]);

The execv() function also requires the full path to the executable. However, it takes the command-line arguments as a single array of strings (a vector). This array must also have the executable name as its first element (argv[0]) and be terminated by a NULL pointer.

To execute the same command, /bin/ls -l /home/pi, you would first construct the argv array:

C
char *const argv[] = {"ls", "-l", "/home/pi", NULL};
execv("/bin/ls", argv);

This approach is far more versatile. You can dynamically allocate and populate the argv array based on user input, configuration files, or other runtime conditions, making it a staple in more complex applications.

graph TD
    subgraph "Goal: Execute /bin/ls -l /home/pi"
        Start("Start")
    end

    Start --> P1{"How are arguments known?"};
    P1 -- "Fixed at compile time" --> L1["Use <b>execl()</b><br><i>(l for List)</i>"];
    L1 --> L2["Arguments are separate C strings in the function call, followed by NULL."];
    L2 --> L3["<pre><code>execl(\/bin/ls\, <br>      \ls\, <br>      \-l\, <br>      \/home/pi\, <br>      NULL);</code></pre>"];

    P1 -- "Determined at runtime" --> V1["Use <b>execv()</b><br><i>(v for Vector)</i>"];
    V1 --> V2["Build a <code>char*</code> array (vector) dynamically, terminated by NULL."];
    V2 --> V3["<pre><code>char *args[] = {<br>  \ls\, <br>  \-l\, <br>  \/home/pi\, <br>  NULL<br>};<br>execv(\/bin/ls\, args);</code></pre>"]

    L3 --> End("Execution");
    V3 --> End("Execution");

    classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
    classDef decisionNode fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
    classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    classDef endNode fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    
    class Start startNode
    class P1 decisionNode
    class L1,L2,V1,V2 processNode
    class L3,V3 processNode
    class End endNode

execlp() and execvp(): Adding the PATH Search

A significant inconvenience of execl() and execv() is the need to provide the full path to the executable. In a standard shell, you can simply type ls without knowing it resides in /bin. The execlp() and execvp() functions replicate this convenient behavior.

int execlp(const char *file, const char *arg0, ... /*, (char *) NULL */);

int execvp(const char *file, char *const argv[]);

These functions are identical to their non-p counterparts, except for how they handle the first argument, file. If file does not contain a slash (/), they search the directories specified in the PATH environment variable to find the executable. If PATH is not set, a default path (often "/bin:/usr/bin") is used. If file contains a slash, the PATH search is skipped, and it’s treated as a direct path.

So, to run ls -l /home/pi, you can now write:

execlp("ls", "ls", "-l", "/home/pi", NULL);

Or, using the vector form:

C
char *const argv[] = {"ls", "-l", "/home/pi", NULL};
execvp("ls", argv);

The kernel does the work of finding ls in /bin/usr/bin, or wherever it is located according to the PATH. This is immensely useful for writing portable scripts and applications that don’t rely on hardcoded paths, a common requirement in embedded systems where the filesystem layout might vary.

execle() and execve(): Full Environmental Control

The final pair of functions, execle() and execve(), provide the ultimate level of control by allowing you to specify a custom environment for the new program. The environment of a process is a set of key-value pairs (e.g., PATH=/usr/bin:/binHOME=/home/pi) that can influence its behavior. By default, a new program inherits the environment of its parent. However, for security or configuration reasons, you might want to provide a minimal or modified environment.

int execle(const char *path, const char *arg0, ... /*, (char *) NULL, char *const envp[] */);

int execve(const char *path, char *const argv[], char *const envp[]);

These functions take an additional argument, envp, which is a NULL-terminated array of strings defining the new environment.

Suppose you want to run a custom script, /usr/local/bin/my_script, and provide it with a specific SENSOR_ID and a restricted PATH.

Using execle():

C
char *const envp[] = {"SENSOR_ID=A42", "PATH=/bin:/usr/bin", NULL};
execle("/usr/local/bin/my_script", "my_script", NULL, envp);

Using execve(), the most fundamental of all:

C
char *const argv[] = {"my_script", NULL};
char *const envp[] = {"SENSOR_ID=A42", "PATH=/bin:/usr/bin", NULL};
execve("/usr/local/bin/my_script", argv, envp);

In fact, execve() is the only true system call in the family. All other exec() functions are library wrappers that ultimately call execve() after rearranging the arguments and possibly searching the PATH. This makes execve() the most powerful and flexible, as it gives the programmer direct control over all inputs to the new program. In security-sensitive embedded applications, sanitizing the environment by providing a custom envp is a critical best practice. It prevents potentially malicious or misconfigured variables from the parent process from affecting the new program’s execution.

Function Argument Passing PATH Search Environment Use Case Example
execl List (arg0, arg1, ...) No Inherited Running a command with a fixed number of arguments and a known path, like execl("/bin/ls", "ls", "-l", NULL);
execv Vector (argv[]) No Inherited Executing a command with a dynamic number of arguments built at runtime.
execlp List (arg0, arg1, ...) Yes Inherited Conveniently running a common command without specifying its full path, e.g., execlp("ls", "ls", "-l", NULL);
execvp Vector (argv[]) Yes Inherited The workhorse for most shells; dynamically builds arguments for a command found in the PATH.
execle List (arg0, arg1, ...) No Custom (envp[]) Running a sensitive process with a minimal, sanitized environment for security.
execve Vector (argv[]) No Custom (envp[]) The underlying system call; offers complete control over arguments and environment. All other exec functions call this one.

Practical Examples

Theory provides the foundation, but true understanding comes from hands-on practice. In this section, we will apply our knowledge to the Raspberry Pi 5. We will write, compile, and run C programs that demonstrate the use of the exec() family.

For these examples, we assume you have a standard cross-compilation toolchain set up for the Raspberry Pi’5s architecture (AArch64). If you are developing directly on the Pi, you can use the native gcc compiler instead.

Example 1: Basic Execution with execlp()

Our first goal is to create a simple C program that acts like a launcher. It will use fork() to create a child process and then the child will use execlp() to run the uname -a command, which prints system information.

Code Snippet (launcher_uname.c)

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

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

    if (pid < 0) {
        // Fork failed
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // This is the child process
        printf("Child process (PID: %d) is executing 'uname -a'...\n", getpid());

        // Replace the child process with the 'uname' program
        // 'p' in execlp means we search the PATH for 'uname'
        // 'l' means we pass arguments as a list
        execlp("uname", "uname", "-a", NULL);

        // If execlp() returns, it must have failed
        perror("execlp failed");
        exit(EXIT_FAILURE); // Exit child with an error code
    } else {
        // This is the parent process
        printf("Parent process (PID: %d) waiting for child (PID: %d)...\n", getpid(), pid);

        int status;
        waitpid(pid, &status, 0); // Wait for the child to terminate

        if (WIFEXITED(status)) {
            printf("Parent: Child exited with status %d\n", WEXITSTATUS(status));
        } else {
            printf("Parent: Child terminated abnormally\n");
        }
    }

    printf("Parent process finished.\n");
    return EXIT_SUCCESS;
}

Code Explanation:

  • The program starts by calling fork().
  • The if (pid == 0) block contains the child’s logic. It first prints its PID and then calls execlp().
  • execlp("uname", "uname", "-a", NULL) tells the system to find the uname executable in the PATH, and run it with the arguments uname (as argv[0]) and -a. The list is terminated by NULL.
  • Crucially, if execlp() is successful, the code that follows it (perrorexit) is never executed. The uname program takes over.
  • The parent process (else block) waits for the child to complete using waitpid() and then reports its exit status.

Build and Run Steps

  1. Cross-Compile the Code:
    aarch64-linux-gnu-gcc -o launcher_uname launcher_uname.c
    (Or gcc -o launcher_uname launcher_uname.c if compiling on the Pi).
  2. Transfer to Raspberry Pi 5:Use scp to copy the compiled binary to your Pi.
    scp launcher_uname pi@<raspberrypi_ip>:/home/pi/
  3. Execute on the Pi:Connect to your Pi via SSH and run the program.
    ssh pi@<raspberrypi_ip> cd /home/pi ./launcher_uname

Expected Output

Plaintext
Parent process (PID: 2450) waiting for child (PID: 2451)...
Child process (PID: 2451) is executing 'uname -a'...
Linux raspberrypi 6.1.0-rpi7-rpi-v8 #1 SMP PREEMPT Debian 1:6.1.63-1+rpt1 (2023-11-24) aarch64 GNU/Linux
Parent: Child exited with status 0
Parent process finished.

This output clearly shows the parent creating the child, the child announcing its intent, and then the output of the uname -a command appearing, followed by the parent confirming the child’s successful completion.

Example 2: Dynamic Arguments with execvp()

Now, let’s create a more flexible launcher that takes command-line arguments and executes them. This demonstrates the power of the v variants. Our program will execute whatever command and arguments are passed to it.

Code Snippet (flexi_launcher.c)

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <command> [args...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // The arguments for execvp must start from the command name,
    // not our launcher's name. We create a new argv array for this.
    // argv[1] is the command, so it will be the new argv[0].
    char **new_argv = &argv[1];

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        printf("Child executing: %s\n", new_argv[0]);

        // 'v' for vector, 'p' for PATH search
        execvp(new_argv[0], new_argv);

        // This code only runs if execvp fails
        perror("execvp failed");
        fprintf(stderr, "Could not execute command: %s\n", new_argv[0]);
        exit(127); // Standard exit code for "command not found"
    } else {
        // Parent process
        int status;
        waitpid(pid, &status, 0);
        printf("Parent: Child process for '%s' finished.\n", new_argv[0]);
    }

    return EXIT_SUCCESS;
}

Code Explanation:

graph LR
    subgraph "Original argv from ./flexi_launcher ls -l"
        A0["argv[0]<br>./flexi_launcher"] --> A1["argv[1]<br>ls"];
        A1 --> A2["argv[2]<br>-l"];
        A2 --> A3["argv[3]<br>NULL"];
    end

    subgraph "new_argv = &argv[1]"
        B0["new_argv[0]<br>ls"] --> B1["new_argv[1]<br>-l"];
        B1 --> B2["new_argv[2]<br>NULL"];
    end

    subgraph "Passed to execvp"
        C["execvp(<b>new_argv[0]</b>, <b>new_argv</b>)"]
    end
    
    P["<b>char **new_argv</b><br><i>Pointer to argv[1]</i>"]
    
    A1 -- "  " --> P;
    P -- "Points to" --> B0;
    
    A1 -- "Becomes" --> B0
    A2 -- "Becomes" --> B1
    A3 -- "Becomes" --> B2
    
    B0 -- "File to execute" --> C
    B0 -- "Argument Vector" --> C

    classDef original fill:#fef3c7,stroke:#f59e0b,color:#92400e
    classDef new fill:#d1fae5,stroke:#10b981,color:#065f46
    classDef pointer fill:#e0e7ff,stroke:#4f46e5,color:#3730a3
    classDef exec fill:#cffafe,stroke:#0e7490,color:#155e75
    
    class A0,A1,A2,A3 original;
    class B0,B1,B2 new;
    class P pointer;
    class C exec;
  • The program checks if it was given at least one argument (the command to run).
  • The key line is char **new_argv = &argv[1];. This clever trick creates a new argument vector that starts from the second element of the original argv. For example, if you run ./flexi_launcher ls -l, the original argv is {"./flexi_launcher", "ls", "-l", NULL}new_argv becomes {"ls", "-l", NULL}, which is exactly what execvp() needs.
  • The child process calls execvp(new_argv[0], new_argv). It uses the first element of the new vector as the file to find and the entire new vector as the argument list.

Build and Run Steps

  1. Compile:
    aarch64-linux-gnu-gcc -o flexi_launcher flexi_launcher.c
  2. Transfer:
    scp flexi_launcher pi@<raspberrypi_ip>:/home/pi/
  3. Execute on the Pi:Try it with different commands.
Bash
./flexi_launcher ls -l
./flexi_launcher python3 -c "import os; print(f'Hello from Python, PID={os.getpid()}')"
./flexi_launcher not_a_real_command

Expected Output

For ./flexi_launcher ls -l:

Plaintext
Child executing: ls
total 8
-rwxr-xr-x 1 pi pi 7560 Dec 15 10:20 flexi_launcher
-rwxr-xr-x 1 pi pi 7320 Dec 15 10:15 launcher_uname
Parent: Child process for 'ls' finished.

For the Python command:

Plaintext
Child executing: python3
Hello from Python, PID=2510
Parent: Child process for 'python3' finished.

For the non-existent command:

Plaintext
Child executing: not_a_real_command
execvp failed: No such file or directory
Could not execute command: not_a_real_command
Parent: Child process for 'not_a_real_command' finished.

This last example is important as it shows the error handling in action. execvp fails, the perror message is printed, and the child exits with an error code.

Example 3: Custom Environment with execle()

Our final example demonstrates how to provide a sanitized, custom environment to a new program. We will write a launcher that executes a simple Python script. The launcher will set a specific environment variable, CUSTOM_GREETING, which the Python script will then read and print.

Python Script (show_env.py)

Python
#!/usr/bin/env python3
import os
import sys

# Get the custom environment variable
greeting = os.getenv("CUSTOM_GREETING", "Default Greeting")
path_var = os.getenv("PATH", "PATH not set")

print(f"Python Script (PID: {os.getpid()}) starting.")
print(f"Message: {greeting}")
print(f"My PATH is: '{path_var}'")
print(f"My arguments are: {sys.argv}")

Tip: Remember to make the Python script executable on the Pi: chmod +x show_env.py.

C Launcher (env_launcher.c)

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

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

    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        printf("Child process preparing to execute script with custom environment.\n");

        // Define the custom environment for the new program
        char *const custom_envp[] = {
            "CUSTOM_GREETING=Hello from the C launcher!",
            "PATH=/bin:/usr/bin", // Provide a minimal PATH
            NULL // The environment array must be NULL-terminated
        };

        // We must provide the full path to the script for execle
        // The 'e' means we provide a custom environment
        execle("./show_env.py", "MyPyScript", "arg1", "arg2", NULL, custom_envp);

        // This code only runs if execle fails
        perror("execle failed");
        exit(EXIT_FAILURE);
    } else {
        // Parent process
        wait(NULL); // Simple wait for child to finish
        printf("Parent: Child finished.\n");
    }

    return EXIT_SUCCESS;
}

Code Explanation:

  • The custom_envp array defines the entire environment for the new process. Note that we explicitly include a PATH variable. If we didn’t, the Python script’s os.getenv("PATH") would return None.
  • The call to execle passes the path to the script, the argv list ("MyPyScript""arg1""arg2"NULL), and finally the custom environment array.
  • The Python script uses os.getenv() to retrieve the variables we set.

Build and Run

1. Compile the C code and transfer both files:

Bash
aarch64-linux-gnu-gcc -o env_launcher env_launcher.c
scp env_launcher show_env.py pi@<raspberrypi_ip>:/home/pi/

2. Prepare and run on the Pi:

Bash
ssh pi@<raspberrypi_ip>
cd /home/pi
chmod +x show_env.py
./env_launcher

Expected Output

Plaintext
Child process preparing to execute script with custom environment.
Python Script (PID: 2588) starting.
Message: Hello from the C launcher!
My PATH is: '/bin:/usr/bin'
My arguments are: ['MyPyScript', 'arg1', 'arg2']
Parent: Child finished.

The output confirms that the Python script received the custom greeting and the minimal PATH variable we defined in the C code, not the parent’s shell environment. It also shows how custom arguments were passed and received.

Common Mistakes & Troubleshooting

The exec() family is powerful, but with power comes the potential for error. Here are some common pitfalls encountered by developers, especially in an embedded context.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Missing NULL Terminator Program crashes with a Segmentation Fault. Or, it behaves erratically, passing garbage data as arguments to the new program. Ensure the argument list/vector is properly terminated.

For lists (execl, etc.):
execlp("ls", "ls", "-l", NULL);

For vectors (execv, etc.):
char *args[] = {"ls", "-l", NULL};
Path Resolution Failure exec call fails. perror() prints: “No such file or directory“. The child process continues running the original code instead of the new program. 1. For non-‘p’ variants, verify the absolute path is correct.
2. For ‘p’ variants, ensure the command is in a directory listed in the PATH environment variable.
3. Use which <command> on the target to find the correct path.
Permission Denied exec call fails. perror() prints: “Permission denied“. The file must have execute permissions for the current user.

Check permissions: ls -l /path/to/executable
Fix permissions: chmod +x /path/to/executable

For scripts, also ensure the shebang line (e.g., #!/bin/bash) points to a valid interpreter.
Ignoring exec() Failure The child process doesn’t transform. It continues executing the original program’s code that came after the exec call, leading to confusing duplicate behavior or logic errors. A successful exec never returns. Always treat code after it as an error path.

Always add error handling:
execvp(args[0], args);
// This code ONLY runs on failure
perror("execvp failed");
exit(127);
Incorrect argv[0] The new program runs but may print incorrect usage info (e.g., “Usage: (null) [options]”) or fail to work if it inspects its own name. By convention, the first argument (arg0 or argv[0]) must be the filename of the program being run.

Example:
execlp("ls", "ls", "-F", NULL);
The first “ls” is the file to find, the second is the value for argv[0].
Leaked File Descriptors The new program unexpectedly holds locks on files, keeps network connections open, or interferes with parent process I/O. Can cause subtle race conditions or resource exhaustion. Close unnecessary file descriptors in the child before calling exec.

Better: When opening files in the parent that should not be inherited, use the O_CLOEXEC flag.
int fd = open("log.txt", O_WRONLY | O_CREAT | O_CLOEXEC);

Exercises

  1. Simple Command Runner:Write a C program named run_ping that uses fork() and execl() to execute the command ping -c 4 8.8.8.8. The parent process should wait for the ping command to complete and print a “Ping test finished.” message. You must use execl() and provide the full path to the ping executable (hint: use which ping to find it).
  2. Environment Inspector:Write a C program named env_setter that launches the /usr/bin/env command. Use fork() and execle(). In your C code, create a custom environment for the env command that contains only three variables: USER=embedded_student, TERM=vt100, and DEVICE=rpi5. The parent should wait for the child and then exit. Verify that the output of your program only shows these three environment variables.
  3. Dynamic grep Tool:Create a program named find_text that acts as a simplified grep. It should be invoked like this: ./find_text <search_term> <file_path>. Your program should use fork() and execvp() to run the grep command with the provided search term and file path. For example, running ./find_text main /etc/passwd should execute grep main /etc/passwd. Implement error handling for incorrect argument counts.
  4. Daemon Launcher:Write a program that launches a “daemonized” process. The parent process should fork(), and then immediately exit, printing “Daemon launched.” The child process should continue running, print its new PID, and then use execlp() to replace itself with the top command. This simulates how a system service might launch a background task. (Note: The child becomes an orphan, and init or systemd will become its new parent).
  5. Building a Mini-Shell:This is a more advanced exercise. Create a program that reads a single line of input from the user, splits it into a command and arguments, and then executes it using fork() and execvp(). The shell should loop, allowing the user to enter multiple commands. For simplicity, you don’t need to handle pipes or redirection. The shell should exit if the user types “exit”. This exercise combines string manipulation, process creation, and execution.

Summary

  • The exec() family of functions replaces the current process image with a new one. It does not create a new process; it transforms the existing one.
  • The standard model for running new programs in Linux is the fork()-then-exec() pattern.
  • The different exec() variants provide flexibility in how arguments and the environment are specified:
    • l vs. v: Pass arguments as a list or a vector (array).
    • p: Search the PATH environment variable to find the executable.
    • e: Pass a custom environment variable array to the new program.
  • A successful exec() call never returns. Code following an exec() call is only executed if the call fails.
  • Proper error handling, including checking the NULL terminator on argument lists and handling exec()‘s potential failure, is critical for robust programs.
  • Controlling the environment and open file descriptors passed to a new program is a key security and stability consideration in embedded systems.

Further Reading

  1. execve(2) Linux Programmer’s Manual: The canonical source for the execve system call and its family. Accessible via man 2 execve on a Linux system.
  2. Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago: Chapter 8, “Process Control,” provides an authoritative and in-depth explanation of the forkexecwait, and exit functions.
  3. The Linux Programming Interface by Michael Kerrisk: Chapters 24 through 27 cover process creation and program execution in exhaustive detail.
  4. Buildroot User Manual: While not directly about exec(), understanding how an entire embedded Linux filesystem is constructed provides context for where executables reside and how PATH is configured. (https://buildroot.org/downloads/manual/manual.html)
  5. Yocto Project Mega-Manual: Similar to Buildroot, this provides essential context on the structure of embedded Linux systems. (https://docs.yoctoproject.org)
  6. Raspberry Pi Documentation – The Linux kernel: Official documentation on the Linux kernel used by Raspberry Pi, useful for understanding the underlying system. (https://www.raspberrypi.com/documentation/computers/linux_kernel.html)
  7. “How programs get run” – LWN.net: A detailed article explaining the low-level details of how the kernel loads and runs an executable. (https://lwn.net/Articles/631631/)

Leave a Comment

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

Scroll to Top