【问题标题】:Detecting failure to find program when fork()-ing a'la popen检测 fork()-ing a'la popen 时找不到程序
【发布时间】:2016-04-21 02:42:23
【问题描述】:

当我在我的系统上执行此操作时:

FILE* pipe = popen("something_that_doesnt_exist", "r");
printf("pipe: %p\n", pipe);

尽管该程序不存在,但我得到了一个有效的管道指针。我想写一个 popen 可以检测到它并返回一个指示启动失败的 NULL 指针。但是,我不确定如何在保持 /bin/sh 要求解释的同时实现这一目标。有谁知道我如何检查电话的返回状态,例如:

execl("/bin/sh", "sh", "-c", "something_that_doesnt_exist");

【问题讨论】:

  • pclose() 为您提供所需的退出状态。这还不够好吗?
  • 那行不通,因为我想要在决定是否关闭我认为的管道之前的状态。
  • 首先,如果您打算调用命令,请不要使用sh -c。仅当您使用 POSIX shell 功能(如替换或作业控制)时才需要它。其次,为了从子进程中检索 exec*() 状态,您可以使用额外的 close-on-exec 管道,该管道仅在 exec*() 失败的情况下使用。我在下面的回答详细显示了这种方法。如果您需要 sh shell 行为,那么您可以将您的 something_that_doesnt_exist 包装在错误报告 shell 脚本 sn-p 中,而您不需要我的回答,只需 POSIX shell 脚本。

标签: c linux


【解决方案1】:

为此,您需要使用低级设施。您需要创建一个额外的管道 close-on-exec,当 exec 失败时,子进程使用它来编写错误代码。

由于管道是 close-on-exec,它将在新二进制文件开始执行时被内核关闭。 (我们实际上不知道它是否正在运行;我们只知道 exec 没有失败。因此,不要假设关闭的管道意味着命令已经在运行。它只意味着它还没有失败。)

父进程关闭不必要的管道端,并从控制管道中读取。如果读取成功,则子进程执行命令失败,读取的数据描述了错误。如果管道关闭(读取返回 0),命令将开始执行(没有错误阻止其执行)。

之后,我们可以像往常一样继续从管道中读取。当子进程关闭管道时,我们应该使用waitpid()来获取它。

考虑以下示例程序。它执行命令行中指定的命令——如果你想要与system()popen() 相同的行为,请使用sh -c 'command'。 (即pathname == "sh"argv == { "sh", "-c", "command", NULL }。) 它逐个字符地读取命令的输出,对它们进行计数,直到子进程结束(通过关闭管道)。之后,我们获取子进程,并报告状态。 如果命令无法执行,也会报告原因。 (由于非可执行文件被报告为ENOENT(“没有这样的文件或目录”),exec_subproc() 将该情况修改为EACCESS(“权限被拒绝”)。)

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int reap_subproc(pid_t pid)
{
    int   status;
    pid_t p;

    if (pid < 1) {
        errno = EINVAL;
        return -1;
    }

    do {
        status = 0;
        p = waitpid(pid, &status, 0);
    } while (p == -1 && errno == EINTR);
    if (p == -1)
        return -1;

    errno = 0;
    return status;
}

FILE *exec_subproc(pid_t *pidptr, const char *pathname, char **argv)
{
    char    buffer[1];
    int     datafd[2], controlfd[2], result;
    FILE   *out;
    ssize_t n;
    pid_t   pid, p;

    if (pidptr)
        *pidptr = (pid_t)0;

    if (!pidptr || !pathname || !*pathname || !argv || !argv[0]) {
        errno = EINVAL;
        return NULL;
    }

    if (pipe(datafd) == -1)
        return NULL;
    if (pipe(controlfd) == -1) {
        const int saved_errno = errno;
        close(datafd[0]);
        close(datafd[1]);
        errno = saved_errno;
        return NULL;
    }

    if (fcntl(datafd[0], F_SETFD, FD_CLOEXEC) == -1 ||
        fcntl(controlfd[1], F_SETFD, FD_CLOEXEC) == -1) {
        const int saved_errno = errno;
        close(datafd[0]);
        close(datafd[1]);
        close(controlfd[0]);
        close(controlfd[1]);
        errno = saved_errno;
        return NULL;
    }

    pid = fork();
    if (pid == (pid_t)-1) {
        const int saved_errno = errno;
        close(datafd[0]);
        close(datafd[1]);
        close(controlfd[0]);
        close(controlfd[1]);
        errno = saved_errno;
        return NULL;
    }

    if (!pid) {
        /* Child process. */

        close(datafd[0]);
        close(controlfd[0]);

        if (datafd[1] != STDOUT_FILENO) {
            do {
                result = dup2(datafd[1], STDOUT_FILENO);
            } while (result == -1 && errno == EINTR);
            if (result == -1) {
                buffer[0] = errno;
                close(datafd[1]);
                do {
                    n = write(controlfd[1], buffer, 1);
                } while (n == -1 && errno == EINTR);
                exit(127);
            }
            close(datafd[1]);
        }

        if (pathname[0] == '/')
            execv(pathname, argv);
        else
            execvp(pathname, argv);

        buffer[0] = errno;
        close(datafd[1]);

        /* In case it exists, we return EACCES instead of ENOENT. */
        if (buffer[0] == ENOENT)
            if (access(pathname, R_OK) == 0)
                buffer[0] = EACCES;

        do {
            n = write(controlfd[1], buffer, 1);
        } while (n == -1 && errno == EINTR);
        exit(127);
    }

    *pidptr = pid;

    close(datafd[1]);
    close(controlfd[1]);

    do {
        n = read(controlfd[0], buffer, 1);
    } while (n == -1 && errno == EINTR);
    if (n == -1) {
        close(datafd[0]);
        close(controlfd[0]);
        kill(pid, SIGKILL);
        do {
            p = waitpid(pid, NULL, 0);
        } while (p == (pid_t)-1 && errno == EINTR);
        errno = EIO;
        return NULL;
    } else
    if (n == 1) {
        close(datafd[0]);
        close(controlfd[0]);
        do {
            p = waitpid(pid, NULL, 0);
        } while (p == (pid_t)-1 && errno == EINTR);
        errno = (int)buffer[0];
        return NULL;
    }

    close(controlfd[0]);

    out = fdopen(datafd[0], "r");
    if (!out) {
        close(datafd[0]);
        kill(pid, SIGKILL);
        do {
            p = waitpid(pid, NULL, 0);
        } while (p == (pid_t)-1 && errno == EINTR);
        errno = EIO;
        return NULL;
    }

    errno = 0;
    return out;
}

int main(int argc, char *argv[])
{
    FILE   *cmd;
    pid_t   pid;
    int     c;
    unsigned long bytes = 0UL;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s command [ arguments ... ]\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    cmd = exec_subproc(&pid, argv[1], argv + 1);
    if (!cmd) {
        fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    }

    while ((c = getc(cmd)) != EOF) {
        bytes++;
        putchar(c);
    }
    fflush(stdout);

    fclose(cmd);
    c = reap_subproc(pid);
    if (errno) {
        fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    }

    if (WIFEXITED(c) && !WEXITSTATUS(c)) {
        fprintf(stderr, "%s: %lu bytes; success.\n", argv[1], bytes);
        return 0;
    }
    if (WIFEXITED(c)) {
        fprintf(stderr, "%s: %lu bytes; failed (exit status %d)\n", argv[1], bytes, WEXITSTATUS(c));
        return WEXITSTATUS(c);
    }
    if (WIFSIGNALED(c)) {
        fprintf(stderr, "%s: %lu bytes; killed by signal %d (%s)\n", argv[1], bytes, WTERMSIG(c), strsignal(WTERMSIG(c)));
        return 128 + WTERMSIG(c);
    }

    fprintf(stderr, "%s: %lu bytes; child lost.\n", argv[1], bytes);
    return EXIT_FAILURE;
}

使用例如编译

gcc -Wall -Wextra -O2 example.c -o example

然后运行例如

./example date
./example ./example date -u
./example /bin/sh -c 'date | tr A-Za-z a-zA-Z'

【讨论】:

  • 不幸的是,我认为我正在做的事情需要 posix shell 语义。我基本上是在编写一个提供超级开放功能的库,包括可以有多个读取器的命名管道。看来在 linux 中确实没有什么好的方法可以做到这一点,这既令人惊讶又令人失望。
  • @gct:别傻了 :) 问题是“确实没有什么好的方法可以做到这一点”,因为您将自己限制在语义上,无法提供强大、有用的解决方案根本问题。例如,考虑 Python subprocess 模块,它旨在解决底层问题的一个子集。如果你想要一个类似 popen 的设施,它有多个管道(甚至是套接字)互连进程(包括父进程),请使用结构来描述每个进程,而不是字符串!然后您也可以轻松获取所有错误消息。
【解决方案2】:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    int fd = open("/dev/null", O_WRONLY);

    /* backup */
    int f1 = dup(1);
    int f2 = dup(2);
    dup2(fd, 1);
    dup2(fd, 2);

    int res = system("something_that_doesnt_exist");

    /* recovery */
    dup2(f1, 1);
    dup2(f2, 2);

    FILE* pipe = popen("something_that_doesnt_exist", "r");
    if (res != 0 )
    {
        pipe = NULL;
    }

}

重定向 stdout 和 stderr 以避免异常输出。

【讨论】:

  • 我喜欢它,但不幸的是我的命令要么是长时间运行要么不是幂等的,所以我不能真正运行它们两次......
【解决方案3】:

popen() 是否会成功执行命令,你无法判断。这是因为进程会先调用fork(),所以总会创建管道和子进程。但是如果 fork() 之后的 execv() 调用失败,子进程会死掉,而父进程将无法判断这是由 execv() 失败还是您想要的命令刚刚完成而没有任何输出引起的。

【讨论】:

    【解决方案4】:

    如果你的进程没有其他子进程,也许你可以使用waitpid

    int stat;
    File* fp = popen("something_that_doesnt_exist", "r");
    waitpid(-1, &stat, 0);
    

    那么就可以判断stat的值了,如果popen成功,stat = 0。不太确定这种方式,需要人确认

    【讨论】:

    • 不幸的是,在这种情况下,我会有很多孩子,我不能阻止和等待。
    猜你喜欢
    • 2020-04-01
    • 2012-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-20
    • 1970-01-01
    • 2013-08-17
    • 2015-03-28
    相关资源
    最近更新 更多