Three Core Methods for Executing Shell Scripts from C Programs in Linux: Mechanisms and Implementation

Dec 08, 2025 · Programming · 9 views · 7.8

Keywords: C Programming | Linux System Calls | Process Management

Abstract: This paper comprehensively examines three primary methods for executing shell scripts from C programs in Linux environments: using the system() function, the popen()/pclose() function pair, and direct invocation of fork(), execve(), and waitpid() system calls. The article provides detailed analysis of each method's application scenarios, working principles, and underlying mechanisms, covering core concepts such as process creation, program replacement, and inter-process communication. By comparing the advantages and disadvantages of different approaches, it offers comprehensive technical selection guidance for developers.

Introduction

In Linux system programming, executing external shell scripts from C programs is a common requirement that involves multiple core concepts including process management, program execution, and inter-process communication. This article systematically introduces three main implementation methods and provides in-depth analysis of their underlying mechanisms.

Executing Shell Scripts Using the system() Function

The system() function provides the simplest approach for executing external commands. This function creates a new shell process (typically /bin/sh) to execute the specified command. Its basic syntax is:

int system(const char *command);

Usage example:

#include <stdlib.h>

int main() {
    int status = system("/usr/local/bin/foo.sh");
    return status;
}

The system() function blocks the current process until command execution completes and returns the command's exit status. While this method is simple and convenient, it has limitations: it always executes commands through a shell interpreter, which may introduce security risks, and it doesn't provide direct control over the child process's input/output.

Inter-Process Communication Using popen() and pclose()

When data interaction with the executed shell script is required, the popen() and pclose() functions offer a more flexible solution. This function pair creates a pipe that allows the parent process to send data to or receive data from the child process.

Basic usage example:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    FILE *fp;
    char buffer[1024];
    
    /* Execute command and read output */
    fp = popen("ls -al", "r");
    if (fp != NULL) {
        while (fgets(buffer, sizeof(buffer), fp) != NULL) {
            /* Process output data */
            printf("%s", buffer);
        }
        pclose(fp);
    }
    return 0;
}

The second parameter of popen() specifies the pipe direction: "r" indicates reading from the child process, while "w" indicates writing to the child process. This method also executes commands through a shell but provides inter-process communication capabilities.

Direct System Call Usage: fork(), execve(), and waitpid()

For scenarios requiring finer control, direct use of underlying system calls is recommended. The core of this approach involves the combined use of three system calls:

The fork() System Call

fork() creates a copy (child process) of the current process. In the parent process, fork() returns the child's PID; in the child process, it returns 0. If creation fails, it returns -1.

#include <unistd.h>
#include <sys/types.h>

pid_t pid = fork();
if (pid == -1) {
    /* Handle error */
} else if (pid == 0) {
    /* Child process code */
} else {
    /* Parent process code */
}

The execve() System Call

execve() replaces the current process's image with a specified program. It doesn't create a new process but reuses the existing process's resources. Basic usage:

#include <unistd.h>

char *args[] = {"/bin/sh", "-c", "/usr/local/bin/foo.sh", NULL};
char *envp[] = {NULL};

execve("/bin/sh", args, envp);

Note: execve() only returns on failure; successful execution doesn't return.

The waitpid() System Call

waitpid() allows the parent process to wait for a specific child process to terminate and obtain its exit status:

#include <sys/types.h>
#include <sys/wait.h>

int status;
waitpid(pid, &status, 0);

Complete Example

The following complete example demonstrates how to combine these system calls to execute a shell script:

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

int main() {
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        /* Child process: execute shell script */
        char *args[] = {"/bin/sh", "-c", "/usr/local/bin/foo.sh", NULL};
        char *envp[] = {NULL};
        
        execve("/bin/sh", args, envp);
        /* If execve returns, an error occurred */
        perror("execve failed");
        exit(EXIT_FAILURE);
    } else {
        /* Parent process: wait for child to finish */
        int status;
        waitpid(pid, &status, 0);
        
        if (WIFEXITED(status)) {
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        }
    }
    
    return 0;
}

Related System Calls and Advanced Features

Beyond the core system calls, several related system calls deserve attention:

pipe() and dup2()

pipe() creates a pipe for inter-process communication:

int pipefd[2];
pipe(pipefd);  /* pipefd[0] for reading, pipefd[1] for writing */

dup2() redirects file descriptors, commonly used to connect pipe endpoints to standard input/output:

dup2(pipefd[0], STDIN_FILENO);  /* Redirect pipe read end to stdin */

clone() and vfork()

clone() provides more flexible process creation than fork(), allowing specification of shared resources. vfork() is a legacy system call generally not recommended in modern systems.

Method Comparison and Selection Guidelines

1. system(): Suitable for simple script execution without data interaction requirements. Advantages include simplicity and ease of use; disadvantages include lower security and limited control capabilities.

2. popen()/pclose(): Suitable for scenarios requiring unidirectional data interaction with scripts. Provides inter-process communication capabilities but still executes commands through a shell.

3. Direct system calls: Suitable for situations requiring fine-grained control, such as:

Security Considerations

1. When executing user-provided commands, avoid using system() due to vulnerability to shell injection attacks.

2. Using execve() allows precise control over arguments and environment variables passed to programs, enhancing security.

3. In child processes, close unnecessary file descriptors to prevent resource leaks.

Conclusion

Multiple methods exist for executing shell scripts from C programs in Linux systems, each with appropriate application scenarios. For simple tasks, the system() function offers the most straightforward solution; for data interaction requirements, popen()/pclose() provides a better choice; and for advanced applications requiring fine-grained control, direct use of fork(), execve(), and waitpid() system calls offers maximum flexibility. Understanding the underlying mechanisms of these methods is crucial for writing robust, secure system programs.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.