还用system?这个命令执行函数更安全好用

引言

在 C 语言开发中,我们经常会遇到需要执行外部命令并获取其输出的场景。且使用system函数又过于粗暴,所以应该采用子进程方式去执行,同时为了避免命令执行时间过长导致程序阻塞,引入超时机制是很有必要的。本文将详细介绍一个实现了命令执行并带有超时处理功能的 C 语言函数 executeCMD。

代码示例

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

// 定义错误码
typedef enum {
    CMD_SUCCESS = 0,
    CMD_NULL_INPUT,
    CMD_BUFFER_TOO_SMALL,
    CMD_POPEN_FAILED,
    CMD_EXECUTION_TIMEOUT
} CommandError;

volatile sig_atomic_t timeout_flag = 0;

// 超时处理信号函数
void timeout_handler(int signum) {
    timeout_flag = 1;
}

CommandError executeCMD(const char* cmd, char** result) {
    if (cmd == NULL) {
        return CMD_NULL_INPUT;
    }

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = timeout_handler;
    sigaction(SIGALRM, &sa, NULL);
    alarm(10); // 设置 10 秒超时

    FILE* ptr = popen(cmd, "r");
    if (ptr == NULL) {
        alarm(0); // 取消超时闹钟
        return CMD_POPEN_FAILED;
    }

    size_t buffer_size = 1024;
    *result = (char*)malloc(buffer_size);
    if (*result == NULL) {
        pclose(ptr);
        alarm(0);
        return CMD_BUFFER_TOO_SMALL;
    }
    (*result)[0] = '\0';

    char line[1024];
    while (!timeout_flag && fgets(line, sizeof(line), ptr) != NULL) {
        size_t current_length = strlen(*result);
        size_t new_length = current_length + strlen(line);
        if (new_length >= buffer_size) {
            // 动态扩展缓冲区
            size_t new_buffer_size = buffer_size * 2;
            char* temp = (char*)realloc(*result, new_buffer_size);
            if (temp == NULL) {
                pclose(ptr);
                free(*result);
                alarm(0);
                return CMD_BUFFER_TOO_SMALL;
            }
            *result = temp;
            buffer_size = new_buffer_size;
        }
        strcat(*result, line);
    }

    pclose(ptr);
    alarm(0); // 取消超时闹钟

    if (timeout_flag) {
        free(*result);
        *result = NULL;
        return CMD_EXECUTION_TIMEOUT;
    }

    return CMD_SUCCESS;
}
    

代码详细分析

错误码定义

typedef enum {
    CMD_SUCCESS = 0,
    CMD_NULL_INPUT,
    CMD_BUFFER_TOO_SMALL,
    CMD_POPEN_FAILED,
    CMD_EXECUTION_TIMEOUT
} CommandError;

这里使用枚举类型定义了一系列错误码,用于表示 executeCMD 函数在执行过程中可能出现的不同错误情况。通过返回不同的错误码,调用者可以方便地判断函数执行的结果,并进行相应的处理。

  • 超时处理机制
volatile sig_atomic_t timeout_flag = 0;

// 超时处理信号函数
void timeout_handler(int signum) {
    timeout_flag = 1;
}

timeout_flag 是一个 volatile sig_atomic_t 类型的全局变量,用于标记是否发生了超时。volatile 关键字确保该变量在多线程或信号处理的环境下能够被正确访问,sig_atomic_t 类型保证了对该变量的读写操作是原子的。
timeout_handler 是一个信号处理函数,当接收到 SIGALRM 信号时,会将 timeout_flag 设置为 1,表示发生了超时。
命令执行函数 executeCMD
输入检查

if (cmd == NULL) {
    return CMD_NULL_INPUT;
}

在函数开始时,会检查输入的命令字符串是否为 NULL。如果是,则直接返回 CMD_NULL_INPUT 错误码。

  • 信号处理设置
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = timeout_handler;
sigaction(SIGALRM, &sa, NULL);
alarm(10); // 设置 10 秒超时

使用 sigaction 函数设置 SIGALRM 信号的处理函数为 timeout_handler。sigaction 相比 signal 函数提供了更可靠的信号处理机制。然后使用 alarm 函数设置一个 10 秒的定时器,10 秒后会触发 SIGALRM 信号。
命令执行

FILE* ptr = popen(cmd, "r");
if (ptr == NULL) {
    alarm(0); // 取消超时闹钟
    return CMD_POPEN_FAILED;
}

使用 popen 函数执行输入的命令,并以读取模式打开一个管道。如果 popen 调用失败,会取消之前设置的定时器,并返回 CMD_POPEN_FAILED 错误码。(popen 函数属于 C 标准库,其作用是创建一个管道,并且调用一个子进程来执行指定的命令)
输出缓冲区管理

size_t buffer_size = 1024;
*result = (char*)malloc(buffer_size);
if (*result == NULL) {
    pclose(ptr);
    alarm(0);
    return CMD_BUFFER_TOO_SMALL;
}
(*result)[0] = '\0';

为存储命令输出分配一个初始大小为 1024 字节的缓冲区。如果内存分配失败,会关闭管道,取消定时器,并返回 CMD_BUFFER_TOO_SMALL 错误码。

  • 读取命令输出
char line[1024];
while (!timeout_flag && fgets(line, sizeof(line), ptr) != NULL) {
    size_t current_length = strlen(*result);
    size_t new_length = current_length + strlen(line);
    if (new_length >= buffer_size) {
        // 动态扩展缓冲区
        size_t new_buffer_size = buffer_size * 2;
        char* temp = (char*)realloc(*result, new_buffer_size);
        if (temp == NULL) {
            pclose(ptr);
            free(*result);
            alarm(0);
            return CMD_BUFFER_TOO_SMALL;
        }
        *result = temp;
        buffer_size = new_buffer_size;
    }
    strcat(*result, line);
}

使用 fgets 函数从管道中逐行读取命令输出,并将其追加到缓冲区中。如果缓冲区空间不足,会使用 realloc 函数动态扩展缓冲区大小。如果内存重新分配失败,会进行相应的清理工作并返回错误码。

  • 清理工作
pclose(ptr);
alarm(0); // 取消超时闹钟

if (timeout_flag) {
    free(*result);
    *result = NULL;
    return CMD_EXECUTION_TIMEOUT;
}

return CMD_SUCCESS;

读取完命令输出后,关闭管道,取消定时器。如果发生了超时,会释放之前分配的缓冲区,并返回 CMD_EXECUTION_TIMEOUT 错误码;否则,返回 CMD_SUCCESS 表示命令执行成功。

executeCMD 函数相较于 system 函数的优势

  • 输出结果获取
    system 函数:system 函数用于执行一个 shell 命令,它仅返回命令执行后的状态码,一般是命令退出时的返回值。若要获取命令的输出结果,还需借助其他复杂的手段,例如将命令输出重定向到文件,再读取文件内容。
    executeCMD 函数:此函数能够直接把命令的输出结果存储在指定的缓冲区中,调用者可方便地获取和处理这些输出,无需额外的文件操作。
  • 错误处理与控制
    system 函数:system 函数返回的状态码只能反映命令是否正常执行结束,无法提供详细的错误信息。若命令执行失败,很难明确具体的失败原因。
    executeCMD 函数:通过自定义错误码,能够为调用者提供更详细的错误信息,便于调用者针对不同的错误情况进行相应的处理。
  • 资源管理
    system 函数:system 函数会创建一个新的 shell 进程来执行命令,在执行过程中会消耗一定的系统资源。而且,system 函数执行完毕后,对于子进程的资源回收等操作,调用者难以进行精细控制。
    executeCMD 函数:使用 popen 函数打开一个管道来执行命令,能够更灵活地管理资源。popen 函数返回一个文件指针,调用者可以通过操作这个文件指针来读取命令输出,并且在使用完后使用 pclose 函数关闭管道,确保资源的正确释放

总结

通过本文的介绍,我们详细了解了 executeCMD 函数的实现原理和代码逻辑。该函数通过使用 popen 函数执行外部命令,结合 sigaction 和 alarm 函数实现了超时处理机制,并通过动态内存管理确保了能够处理不同长度的命令输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值