Linux|Linux系统的exec函数族浅浅解析

exec 函数族是 Linux 和其他类 Unix 操作系统中的一组系统调用,用于在当前进程的上下文中执行新的程序。这些函数包括 execl, execle, execlp, execv, execve, execvp 和 execvpe 等。使用这些函数可以替换当前进程的地址空间,使其执行一个新的程序

基础背景

什么是exec函数族?

exec 函数族是一组系统调用,它们可以在当前进程的上下文中执行新的程序。这意味着新的程序会完全替换当前进程的地址空间,并从新程序的入口点开始执行。exec 函数族并不会创建新进程,而是用新的程序替换当前进程,因此新的程序会继承旧程序的进程 ID 和环境变量等信息。

这里是一个示例:

#include <unistd.h>
#include <stdio.h>

int main() {
    char *args[] = {"/bin/ls", "-l", "/home", NULL}; // 要执行的程序及其参数
    printf("Before execvp\n");
    
    // 使用 execvp 替换当前进程
    if (execvp(args[0], args) == -1) {
        perror("execvp failed"); // 错误处理
    }
    
    printf("This will not be printed if execvp succeeds\n");
    return 0;
}
  1. execvp 函数调用替换了当前进程的地址空间,用新的程序(即 ls -l /home)替换当前进程。
  2. 如果 execvp 成功执行,当前进程的地址空间将被 ls 程序替换,因此 “This will not be printed if execvp succeeds” 这行代码不会被执行。
  3. 如果 execvp 失败,将会执行 perror 打印错误信息。

那么为什么使用 exec 函数族?

使用 exec 函数族的主要目的是启动一个新的程序,同时保持当前进程的属性(例如进程 ID)。他最典型的应用就是编写shell。这种方法在编写 shell 或其他需要启动外部程序的系统软件时特别有用。例如,在实现 shell 命令解释器时,exec 函数族可以用来执行用户输入的命令。

主要函数成员

  1. execl:
int execl(const char *path, const char *arg, ... /* (char *) NULL */);

使用指定路径执行新程序,并通过一系列可变参数传递给新程序。

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

与 execl 类似,但允许传递一个新的环境变量数组。

  1. execlp:
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

类似于 execl,但可以在 PATH 环境变量指定的目录中搜索可执行文件。

  1. 重要----execv:
int execv(const char *path, char *const argv[]);

使用指定路径执行新程序,并通过数组传递参数。

  1. execve
int execve(const char *filename, char *const argv[], char *const envp[]);

类似于 execv,但允许传递一个新的环境变量数组。

  1. ⭐️execvp:
int execvp(const char *file, char *const argv[]);

类似于 execv,但可以在 PATH 环境变量指定的目录中搜索可执行文件

  1. execvpe:
int execvpe(const char *file, char *const argv[], char *const envp[]);

类似于 execve 和 execvp,但允许传递新的环境变量数组并搜索 PATH。

主要应用场景

1. 实现 Shell

Shell 通过 fork 一个子进程来处理用户命令。子进程可以使用 exec 函数族之一来执行具体的命令。例如,ls 命令可以通过 execvp(“ls”, args) 来执行。
这里和上面的举例类似

2. 服务器程序

在服务器编程中,exec 函数族常用于执行外部程序或脚本。例如,在处理 CGI(Common Gateway Interface)请求时,Web 服务器需要执行外部脚本来生成动态内容。exec 函数族可以在现有进程中执行这些外部程序,避免创建新的进程,提升效率。

简单的 CGI 处理

假设我们有一个简单的 HTTP 服务器,它需要处理 CGI 请求,并执行一个外部的 CGI 脚本。下面是一个基本的实现示例,在这个示例中,服务器监听端口 8080。当收到一个请求时,如果请求 URL/cgi-bin/script.cgi,服务器会 fork 一个子进程来执行 script.cgi 脚本。子进程使用 execv 函数执行脚本,脚本的输出会直接发送给客户端。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080

void handle_request(int client_socket) {
    char buffer[1024];
    read(client_socket, buffer, sizeof(buffer) - 1);
    printf("Received request: %s\n", buffer);

    // 简单地处理 GET 请求
    if (strncmp(buffer, "GET /cgi-bin/script.cgi", 23) == 0) {
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程执行 CGI 脚本
            char *args[] = {"script.cgi", NULL};
            execv("/path/to/cgi-bin/script.cgi", args);
            perror("execv"); // 如果 execv 返回,说明出现错误
            exit(EXIT_FAILURE);
        } else if (pid > 0) {
            wait(NULL); // 等待子进程结束
        } else {
            perror("fork");
        }
    }

    close(client_socket);
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // 接受客户端连接并处理请求
    while ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
        handle_request(client_fd);
    }

    close(server_fd);
    return 0;
}

3. 编写守护进程(Daemon)

守护进程(Daemon)是一种在后台运行的服务进程,通常在系统启动时启动,并持续运行以提供服务。守护进程可能需要在运行过程中重新启动自己以应用配置更改或更新。这时可以使用 exec 函数族来替换当前进程,从而重新启动进程并应用新版本或新配置。

下面是一个守护进程的示例,它在接收到特定信号时重新启动自己,在这个示例中:

  1. 守护进程在启动时通过 fork 创建一个子进程并终止父进程,使其在后台运行。
  2. 守护进程设置了一个信号处理器来处理 SIGHUP 信号。当接收到 SIGHUP 信号时,调用 restart_daemon 函数。
  3. restart_daemon 函数使用 execv 函数重新启动守护进程自身。这样可以在不改变进程 ID 的情况下重新启动守护进程,并应用新的配置或更新。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void restart_daemon() {
    char *args[] = {"mydaemon", NULL};
    execv("/path/to/mydaemon", args);
    perror("execv");
    exit(EXIT_FAILURE);
}

void handle_signal(int sig) {
    if (sig == SIGHUP) {
        restart_daemon();
    }
}

int main() {
    pid_t pid, sid;

    // 创建子进程,终止父进程
    pid = fork();
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    // 设置文件权限掩码
    umask(0);

    // 创建新会话
    sid = setsid();
    if (sid < 0) {
        exit(EXIT_FAILURE);
    }

    // 改变工作目录
    if ((chdir("/")) < 0) {
        exit(EXIT_FAILURE);
    }

    // 关闭标准文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 打开日志文件
    int log_fd = open("/var/log/mydaemon.log", O_RDWR | O_CREAT, 0600);
    if (log_fd < 0) {
        exit(EXIT_FAILURE);
    }
    dup2(log_fd, STDOUT_FILENO);
    dup2(log_fd, STDERR_FILENO);

    // 设置信号处理器
    signal(SIGHUP, handle_signal);

    // 守护进程的主循环
    while (1) {
        // 执行守护进程的任务
        sleep(10);
    }

    close(log_fd);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值