将命令输出解析为带有文件描述符的变量

问题描述

我想让我的程序执行 md5sum 命令以生成给定文件的哈希值,然后将哈希值存储到数组 (char *) 变量中。

我已经了解了 popen(),但它涉及 FILE * 变量,我只想使用文件描述符。

有没有办法做到这一点?

解决方法

正如我在我的 comment 中所指出的,完全有可能实现 popen() 的功能,但让函数返回一个文件描述符而不是像 popen() 那样的文件流。该任务没有标准库函数。您需要创建一个管道和叉子。孩子会做管道,以便命令的标准输出进入管道的写端(读端关闭),然后执行命令。父进程将关闭管道的写入端,从管道的读取端读取响应,然后关闭它。并不是真的那么难——只是有点繁琐,仅此而已。

对应于 pclose() 的代码有点棘手。代码应该等待孩子死亡,还是至少尝试收集僵尸?如果是这样,它如何知道哪个 PID 适合等待?很容易只说“使用返回的文件描述符调用 close()”,但这可能会留下僵尸。它应该等待孩子死,还是如果孩子死了就直接收集尸体,让其他代码处理僵尸?下面代码中实现的解决方案:

  • 将文件描述符限制为 128 个(包括标准 I/O 通道)。
  • 在固定大小的数组 pids 中记录与文件描述符关联的 PID。
  • 使用 waitpid() 和已保存的与文件描述符关联的 PID,以 0(无条件等待)或 WNOHANG 等待子进程。
  • 如果孩子已经退出,则报告孩子的状态。
  • 否则报告成功 - 0。

改变设计以便动态分配每个文件描述符的 PID 值数组是可行的。您可以控制 dpclose() 函数是等待子进程退出,还是在它尚未退出时不等待。

该代码不进行信号处理。这是另一层复杂性。

/* SO 6557-1879 */

/* #include "dpopen.h" */
#ifndef DPOPEN_H_INCLUDED
#define DPOPEN_H_INCLUDED

#include <fcntl.h>      /* O_RDONLY or O_WRONLY for mode in dpopen() */
#include <sys/wait.h>   /* WNOHANG for options in dpclose() */

/* dpopen() - similar to popen(),but returning a file descriptor */
/* The value in mode must be O_RDONLY or O_WRONLY */
extern int dpopen(char *cmd,int mode);

/* dpclose() - similar to pclose(),but working with a file descriptor returned by dpopen() */
/* The value in options must be 0 or WNOHANG */
/* The return value is the exit status of the child if available,0 if not,or -1 if there is a problem */
extern int dpclose(int fd,int options);

#endif /* DPOPEN_H_INCLUDED */

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX_PDOPEN_FD = 128 };
static pid_t pids[MAX_PDOPEN_FD];

int dpopen(char *cmd,int mode)
{
    if (cmd == 0 || (mode != O_RDONLY && mode != O_WRONLY))
    {
        errno = EINVAL;
        return -1;
    }

    int fd[2];
    if (pipe(fd) != 0)
        return -1;

    /*
    ** Avoid embarrassment in debug builds if someone closed file
    ** descriptors too enthusiastically,and double check at run-time
    ** for non-debug builds.  In some ways,it isn't very necessary as a
    ** runtime check - the circumstances are implausible.  It is
    ** possible to code around fd[0] == STDIN_FILENO and fd[1] ==
    ** STDERR_FILENO,etc,but it is very messy to do so (having to
    ** avoid closing file descriptors,etc).  It is simpler to close the
    ** two new file descriptors and return -1 with errno set to EINVAL
    ** if they overlap with the standard I/O descriptors.  If this
    ** problem is detected,the program is already screwed up because at
    ** least one of standard input,standard output or standard error
    ** was closed.
    */
    assert(fd[0] > STDERR_FILENO && fd[1] > STDERR_FILENO);
    if (fd[0] <= STDERR_FILENO || fd[1] <= STDERR_FILENO)
    {
        close(fd[0]);
        close(fd[1]);
        errno = EINVAL;
        return -1;
    }
    if (fd[0] >= MAX_PDOPEN_FD || fd[1] >= MAX_PDOPEN_FD)
    {
        close(fd[0]);
        close(fd[1]);
        errno = EMFILE;
        return -1;
    }

    /*
    ** Prepare for forking - minimal step.  See SO 5011-0992
    ** (https://stackoverflow.com/q/50110992 and
    ** https://stackoverflow.com/a/50112169/): "Why does forking my
    ** process cause the file to be read infinitely?"
    ** See also SO 0297-9209 (https://stackoverflow.com/q/2979209 and
    ** https://stackoverflow.com/a/34247021) "Using fflush(stdin)",** noting that Standard C and POSIX diverge somewhat; POSIX mandates
    ** behaviour that the C standard does not.  It would be possible to
    ** ensure standard input is 'clean' using code such as:
    **
    **     if (lseek(fileno(stdin),0L,SEEK_CURR) >= 0)
    **         fflush(stdin);
    **
    ** Standard error is normally not a problem; by default,it is not
    ** fully buffered.
    */
    fflush(stdout);

    pid_t pid = fork();
    if (pid < 0)
    {
        close(fd[0]);
        close(fd[1]);
        return -1;
    }

    if (pid == 0)
    {
        /* Child */
        if (mode == O_RDONLY)
            dup2(fd[1],STDOUT_FILENO);
        else
            dup2(fd[0],STDIN_FILENO);
        close(fd[0]);
        close(fd[1]);
        char *argv[] = { "/bin/sh","-c",cmd,0 };
        execv(argv[0],argv);
        exit(EXIT_FAILURE);
    }

    /* Parent */
    if (mode == O_RDONLY)
    {
        close(fd[1]);
        pids[fd[0]] = pid;
        return fd[0];
    }
    else
    {
        close(fd[0]);
        pids[fd[1]] = pid;
        return fd[1];
    }
}

int dpclose(int fd,int options)
{
    if (fd <= STDERR_FILENO || fd >= MAX_PDOPEN_FD || pids[fd] == 0 ||
        (options != 0 && options != WNOHANG))
    {
        errno = EINVAL;
        return -1;
    }
    if (close(fd) != 0)
        return -1;
    pid_t corpse;
    int status;
    pid_t child = pids[fd];
    pids[fd] = 0;
    if ((corpse = waitpid(child,&status,options)) == child)
        return status;
    return 0;
}

int main(void)
{
    int fd1 = dpopen("ls -ltr",O_RDONLY);
    int fd2 = dpopen("cat > ls.out; sleep 10",O_WRONLY);

    if (fd1 < 0 || fd2 < 0)
    {
        fprintf(stderr,"failed to create child processes\n");
        exit(EXIT_FAILURE);
    }

    char buffer[256];
    ssize_t rbytes;
    while ((rbytes = read(fd1,buffer,sizeof(buffer))) > 0)
    {
        ssize_t wbytes = write(fd2,rbytes);
        if (wbytes != rbytes)
        {
            fprintf(stderr,"Failed to write data\n");
            close(fd1);
            close(fd2);
            exit(EXIT_FAILURE);
        }
    }

    if (dpclose(fd1,WNOHANG) < 0 || dpclose(fd2,0) < 0)
    {
        fprintf(stderr,"failed to close pipes correctly\n");
        exit(EXIT_FAILURE);
    }

    return 0;
}