【问题标题】:C - How to pipe to a program that read only from fileC - 如何通过管道传输到只从文件中读取的程序
【发布时间】:2018-02-03 16:29:13
【问题描述】:

我想通过管道将一个字符串传递给一个程序,该程序只从文件中读取输入,而不是从标准输入中读取。从 bash 中使用它,我可以做类似的事情

echo "hi" | program /dev/stdin

我想从 C 代码中复制这种行为。我做的是这个

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

int main() {
 pid_t pid;
 int rv;

 int to_ext_program_pipe[2];
 int to_my_program_pipe[2];

 if(pipe(to_ext_program_pipe)) {
    fprintf(stderr,"Pipe error!\n");
    exit(1);
 }
 if(pipe(to_my_program_pipe)) {
    fprintf(stderr,"Pipe error!\n");
    exit(1);
 }

 if( (pid=fork()) == -1) {
    fprintf(stderr,"Fork error. Exiting.\n");
    exit(1);
 }

 if(pid) {
    close(to_my_program_pipe[1]);
    close(to_ext_program_pipe[0]);
    char string_to_write[] = "this is the string to write";

    write(to_ext_program_pipe[1], string_to_write, strlen(string_to_write) + 1);
    close(to_ext_program_pipe[1]);

    wait(&rv);
    if(rv != 0) {
        fprintf(stderr, "%s %d\n", "phantomjs exit status ", rv);
        exit(1);
    }

    char *string_to_read;
    char ch[1];
    size_t len = 0;
    string_to_read = malloc(sizeof(char));
    if(!string_to_read) {

        fprintf(stderr, "%s\n", "Error while allocating memory");

        exit(1);
    }
    while(read(to_my_program_pipe[0], ch, 1) == 1) {
        string_to_read[len]=ch[0];
        len++;
        string_to_read = realloc(string_to_read, len*sizeof(char));
        if(!string_to_read) {
            fprintf(stderr, "%s\n", "Error while allocating memory");
        }
        string_to_read[len] = '\0';
    }
    close(to_my_program_pipe[0]);
    printf("Output: %s\n", string_to_read);
    free(string_to_read);
} else {
    close(to_ext_program_pipe[1]);
    close(to_my_program_pipe[0]);

    dup2(to_ext_program_pipe[0],0);
    dup2(to_my_program_pipe[1],1);

    if(execlp("ext_program", "ext_program", "/dev/stdin" , NULL) == -1) {
        fprintf(stderr,"execlp Error!");
        exit(1);
    }
    close(to_ext_program_pipe[0]);
    close(to_my_program_pipe[1]);
}

 return 0; 
}

它不工作。

编辑 我没有得到ext_program 输出,应该保存在string_to_read 中。该程序只是挂起。我可以看到ext_program 被执行了,但是我什么也没得到

我想知道是否有错误,或者我想要的东西是否无法完成。我也知道另一种方法是使用命名管道。


编辑 2:更多细节

由于我的程序仍然无法运行,我发布了完整的代码

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

int main() {

    pid_t pid;
    int rv;
    int to_phantomjs_pipe[2];
    int to_my_program_pipe[2];

    if(pipe(to_phantomjs_pipe)) {
        fprintf(stderr,"Pipe error!\n");
        exit(1);
    }
    if(pipe(to_my_program_pipe)) {
        fprintf(stderr,"Pipe error!\n");
        exit(1);
    }

    if( (pid=fork()) == -1) {
        fprintf(stderr,"Fork error. Exiting.\n");
        exit(1);
    }

    if(pid) {
        close(to_my_program_pipe[1]);
        close(to_phantomjs_pipe[0]);

        char jsToExectue[] = "var page=require(\'webpage\').create();page.onInitialized=function(){page.evaluate(function(){delete window._phantom;delete window.callPhantom;});};page.onResourceRequested=function(requestData,request){if((/http:\\/\\/.+\?\\\\.css/gi).test(requestData[\'url\'])||requestData.headers[\'Content-Type\']==\'text/css\'){request.abort();}};page.settings.loadImage=false;page.settings.userAgent=\'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36\';page.open(\'https://stackoverflow.com\',function(status){if(status!==\'success\'){phantom.exit(1);}else{console.log(page.content);phantom.exit();}});";

        write(to_phantomjs_pipe[1], jsToExectue, strlen(jsToExectue) + 1);
        close(to_phantomjs_pipe[1]);

        int read_chars;
        int BUFF=1024;
        char *str;
        char ch[BUFF];
        size_t len = 0;
        str = malloc(sizeof(char));
        if(!str) {
            fprintf(stderr, "%s\n", "Error while allocating memory");
            exit(1);
        }
        str[0] = '\0';

        while( (read_chars = read(to_my_program_pipe[0], ch, BUFF)) > 0)
        {
            len += read_chars;
            str = realloc(str, (len + 1)*sizeof(char));
            if(!str) {
                fprintf(stderr, "%s\n", "Error while allocating memory");
            }
            strcat(str, ch);
            str[len] = '\0';
            memset(ch, '\0', BUFF*sizeof(ch[0]));
        }
        close(to_my_program_pipe[0]);
        printf("%s\n", str);
        free(str);

        wait(&rv);
        if(rv != 0) {
            fprintf(stderr, "%s %d\n", "phantomjs exit status ", rv);
            exit(1);
        }
    } else {
        dup2(to_phantomjs_pipe[0],0);
        dup2(to_my_program_pipe[1],1);

        close(to_phantomjs_pipe[1]);
        close(to_my_program_pipe[0]);
        close(to_phantomjs_pipe[0]);
        close(to_my_program_pipe[1]);

        execlp("phantomjs", "phantomjs", "--ssl-protocol=TLSv1", "/dev/stdin" , (char *)NULL);
    }

    return 0;
}

我要做的是向 phantomjs 传递一个脚本以通过管道执行,然后将生成的 HTML 作为字符串读取。我按照说明修改了代码,但 phantomjs 仍然没有从标准输入读取。
我通过创建一个将其写入文件然后正常执行 phantomjs 并且可以正常工作的哑程序来测试脚本字符串。
我还尝试执行
execlp("phantomjs", "phantomjs", "--ssl-protocol=TLSv1", "path_to_script_file" , (char *)NULL);
并且这也有效,显示了输出 HTML。
使用管道时它不起作用。

【问题讨论】:

  • 它不工作。请比这更详细。解释你执行了什么,做了什么,你期望什么。
  • 我相信你应该reallocate(len+1)*sizeof(char) 而不是len*sizeof(char)
  • @lurker 我只是没有得到任何输出。程序挂起并且不做任何事情。用ps awxs可以看到程序执行完毕
  • ext_program 是您的 PATH 环境变量中指定的实际可执行文件吗?这个程序真的在 fd 1 中写了一些东西吗?另外,根据文档,execlp 中的最后一个参数应该转换为(char *) NULL
  • @Hawk 是的,它在路径中

标签: c pipe exec


【解决方案1】:

最后的解释

PhantomJS 的一些实验表明,问题在于在发送到 PhantomJS 的 JavaScript 程序末尾写入了一个空字节。 这突出了两个错误:

  1. 问题中的程序发送了一个不必要的空字节。
  2. PhantomJS 2.1.1(在运行 macOS High Sierra 10.13.3 的 Mac 上)在其他有效程序后跟空字节时挂起

问题中的代码包含:

write(to_phantomjs_pipe[1], jsToExectue, strlen(jsToExectue) + 1);

+ 1 表示终止字符串的空字节也写入phantomjs。写入该空字节会导致phantomjs 挂起。这无异于一个错误——当然不清楚为什么 PhantomJS 在没有检测到 EOF(没有更多数据来)的情况下挂起,也没有给出错误等。

将该行更改为:

write(to_phantomjs_pipe[1], jsToExectue, strlen(jsToExectue));

并且代码按预期工作——至少在运行 macOS High Sierra 10.13.3 的 Mac 上使用 PhantomJS 2.1.1。

初步分析

您没有在子项中关闭足够的文件描述符。

  • 经验法则:如果您 dup2() 管道的一端到标准输入或标准输出,关闭两者 返回的原始文件描述符 pipe() 尽快地。 特别是,您应该在使用任何 exec*() 函数族。

    如果您使用以下任一方式复制描述符,该规则也适用 dup() 或者 fcntl() F_DUPFD

显示的子代码是:

} else {
    close(to_ext_program_pipe[1]);
    close(to_my_program_pipe[0]);

    dup2(to_ext_program_pipe[0],0);
    dup2(to_my_program_pipe[1],1);

    if(execlp("ext_program", "ext_program", "/dev/stdin" , NULL) == -1) {
        fprintf(stderr,"execlp Error!");
        exit(1);
    }
    close(to_ext_program_pipe[0]);
    close(to_my_program_pipe[1]);
}

最后两个close() 语句永远不会执行;它们需要出现在execlp() 之前。

你需要的是:

} else {
    dup2(to_ext_program_pipe[0], 0);
    dup2(to_my_program_pipe[1], 1);
    close(to_ext_program_pipe[0]);
    close(to_ext_program_pipe[1]);
    close(to_my_program_pipe[0]);
    close(to_my_program_pipe[1]);

    execlp("ext_program", "ext_program", "/dev/stdin" , NULL);
    fprintf(stderr, "execlp Error!\n");
    exit(1);
}

您可以重新排序它以拆分 close() 调用,但最好如图所示重新组合它们。

请注意,无需测试execlp() 是否失败。如果返回,则失败。如果成功,则不会返回。


可能还有另一个问题。父进程在从子进程读取任何内容之前等待子进程退出。但是,如果子进程尝试写入的数据超出管道的容量,则进程将挂起,等待某个进程(必须是父进程)读取管道。由于他们都在等待对方做某事,然后才会做对方正在等待的事情,所以这是(或至少可能是)僵局。

您还应该修改父进程以在等待之前进行读取。

if (pid) {
    close(to_my_program_pipe[1]);
    close(to_ext_program_pipe[0]);
    char string_to_write[] = "this is the string to write";

    write(to_ext_program_pipe[1], string_to_write, strlen(string_to_write) + 1);
    close(to_ext_program_pipe[1]);

    char *string_to_read;
    char ch[1];
    size_t len = 0;
    string_to_read = malloc(sizeof(char));
    if(!string_to_read) {
        fprintf(stderr, "%s\n", "Error while allocating memory");
        exit(1);
    }
    while (read(to_my_program_pipe[0], ch, 1) == 1) {
        string_to_read[len] = ch[0];
        len++;
        string_to_read = realloc(string_to_read, len*sizeof(char));
        if (!string_to_read) {
            fprintf(stderr, "%s\n", "Error while allocating memory\n");
            exit(1);
        }
        string_to_read[len] = '\0';
    }
    close(to_my_program_pipe[0]);
    printf("Output: %s\n", string_to_read);
    free(string_to_read);

    wait(&rv);
    if (rv != 0) {
        fprintf(stderr, "%s %d\n", "phantomjs exit status ", rv);
        exit(1);
    }
} …

我还会重写代码以读取大块(1024 字节或更多)。只是不要复制比读取返回更多的数据,仅此而已。反复使用realloc() 为缓冲区再分配一个字节最终会非常缓慢。如果只有几个字节的数据也没关系;是否有千字节或更多数据需要处理。

稍后: 由于 PhantomJS 程序生成超过 90 KiB 的数据来响应它发送的消息,这是问题的一个因素——或者如果不是因为挂起—— PhantomJS 中的空字节错误。

还是有问题 2018-02-03

我将修改后的代码提取到一个程序中(pipe89.c,编译为pipe89)。当分配的空间发生变化时,我遇到了不一致的崩溃。我最终意识到你重新分配了一个字节的空间太少了——它花费的时间比它应该做的要长得多(但如果 Valgrind 可用于 macOS High Sierra 会有所帮助——它还没有)。

这是带有调试信息注释输出的固定代码:

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

int main(void)
{
    pid_t pid;
    int rv;

    int to_ext_program_pipe[2];
    int to_my_program_pipe[2];

    if (pipe(to_ext_program_pipe))
    {
        fprintf(stderr, "Pipe error!\n");
        exit(1);
    }
    if (pipe(to_my_program_pipe))
    {
        fprintf(stderr, "Pipe error!\n");
        exit(1);
    }

    if ((pid = fork()) == -1)
    {
        fprintf(stderr, "Fork error. Exiting.\n");
        exit(1);
    }

    if (pid)
    {
        close(to_my_program_pipe[1]);
        close(to_ext_program_pipe[0]);
        char string_to_write[] = "this is the string to write";

        write(to_ext_program_pipe[1], string_to_write, sizeof(string_to_write) - 1);
        close(to_ext_program_pipe[1]);

        char ch[1];
        size_t len = 0;
        char *string_to_read = malloc(sizeof(char));
        if (string_to_read == 0)
        {
            fprintf(stderr, "%s\n", "Error while allocating memory");
            exit(1);
        }
        string_to_read[len] = '\0';

        while (read(to_my_program_pipe[0], ch, 1) == 1)
        {
            //fprintf(stderr, "%3zu: got %3d [%c]\n", len, ch[0], ch[0]); fflush(stderr);
            string_to_read[len++] = ch[0];
            char *new_space = realloc(string_to_read, len + 1);     // KEY CHANGE is " + 1"
            //if (new_space != string_to_read)
            //    fprintf(stderr, "Move: len %zu old %p vs new %p\n", len, (void *)string_to_read, (void *)new_space);
            if (new_space == 0)
            {
                fprintf(stderr, "Error while allocating %zu bytes memory\n", len);
                exit(1);
            }
            string_to_read = new_space;
            string_to_read[len] = '\0';
        }
        close(to_my_program_pipe[0]);
        printf("Output: %zu (%zu) [%s]\n", len, strlen(string_to_read), string_to_read);
        free(string_to_read);

        wait(&rv);
        if (rv != 0)
        {
            fprintf(stderr, "%s %d\n", "phantomjs exit status ", rv);
            exit(1);
        }
    }
    else
    {
        dup2(to_ext_program_pipe[0], 0);
        dup2(to_my_program_pipe[1], 1);
        close(to_ext_program_pipe[0]);
        close(to_ext_program_pipe[1]);
        close(to_my_program_pipe[0]);
        close(to_my_program_pipe[1]);

        execlp("ext_program", "ext_program", "/dev/stdin", NULL);
        fprintf(stderr, "execlp Error!\n");
        exit(1);
    }

    return 0;
}

它在一个程序上进行了测试,该程序为 27 个字节的输入写入了 5590 个字节。这并不像您的程序中那样大,但它证明了一点。

我仍然认为你最好不要一次重新分配一个额外的字节——扫描循环应该使用一个缓冲区,比如说,1 KiB,一次读取最多 1 KiB,并分配额外的空间立刻。对于内存分配系统来说,这是一项不那么密集的锻炼。

问题在 2018-02-05 继续存在

从 Edit 2 中获取代码,仅将函数定义从 int main() { 更改为 int main(void) {(因为我使用的编译选项不允许旧式非原型函数声明或定义,并且没有 @ 987654351@,那不是原型),代码是 对我来说工作得很好。我创建了一个代理 phantomjs 程序(来自另一个我已经躺过的程序),如下所示:

#include <stdio.h>

int main(int argc, char **argv, char **envp)
{
    for (int i = 0; i < argc; i++)
        printf("argv[%d] = <<%s>>\n", i, argv[i]);
    for (int i = 0; envp[i] != 0; i++)
        printf("envp[%d] = <<%s>>\n", i, envp[i]);
    FILE *fp = fopen(argv[argc - 1], "r");
    if (fp != 0)
    {
        int c;
        while ((c = getc(fp)) != EOF)
            putchar(c);
        fclose(fp);
    }
    else
        fprintf(stderr, "%s: failed to open file %s for reading\n",
                argv[0], argv[argc-1]);
    return(0);
}

此代码回显参数列表、环境,然后打开名为最后一个参数的文件并将其复制到标准输出。 (由于对argv[argc-1] 的特殊处理,它高度专业化,但之前的代码偶尔可用于调试复杂的 shell 脚本。)

当我用这个“phantomjs”运行你的程序时,我得到了我期望的输出:

argv[0] = <<phantomjs>>
argv[1] = <<--ssl-protocol=TLSv1>>
argv[2] = <</dev/stdin>>
envp[0] = <<MANPATH=/Users/jleffler/man:/Users/jleffler/share/man:/Users/jleffler/oss/share/man:/Users/jleffler/oss/rcs/man:/usr/local/mysql/man:/opt/gcc/v7.3.0/share/man:/Users/jleffler/perl/v5.24.0/man:/usr/local/man:/usr/local/share/man:/usr/share/man:/opt/gnu/share/man>>
envp[1] = <<IXH=/opt/informix/12.10.FC6/etc/sqlhosts>>
…
envp[49] = <<HISTFILE=/Users/jleffler/.bash.jleffler>>
envp[50] = <<_=./pipe31>>
var page=require('webpage').create();page.onInitialized=function(){page.evaluate(function(){delete window._phantom;delete window.callPhantom;});};page.onResourceRequested=function(requestData,request){if((/http:\/\/.+?\\.css/gi).test(requestData['url'])||requestData.headers['Content-Type']=='text/css'){request.abort();}};page.settings.loadImage=false;page.settings.userAgent='Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36';page.open('https://stackoverflow.com',function(status){if(status!=='success'){phantom.exit(1);}else{console.log(page.content);phantom.exit();}});

此时,我不得不将矛头指向你的环境中的phantomjs;当您执行以下操作时,它的行为似乎与预期不符:

echo "$JS_PROG" | phantomjs /dev/stdin | cat

当然,我无法再重现您的问题。

  • 您应该使用我的代理 phantomjs 代码并使用它而不是真正的 phantomjs,然后看看您会得到什么。

    • 如果您得到的输出与我展示的类似,那么问题出在真正的 phantomjs 上。
    • 如果您没有得到与我显示的类似的输出,那么您的代码可能存在问题,从更新到问题。

稍后:请注意,因为printf() 使用%s 来打印数据,它不会注意到发送给孩子的无关空字节。

【讨论】:

  • 在执行ext_program完全关闭管道之前不关闭FD?
  • @Hawk;否 — 它关闭了原始描述符,但由于 dup2() 调用,标准输入或标准输出现在也使用打开的文件描述。在dup2() 之后,有两个打开的文件描述符都引用了一个打开的文件描述(很有趣,不是吗!)。代码需要关闭未使用的描述符,但它仍然有两个打开的描述符,一个用于标准输入,一个用于标准输出,它们引用管道。请注意,尾随收盘从未被执行。出错时,程序已经退出。成功后,execlp() 之后没有任何内容被执行。
  • 当然,现在我明白了,我不熟悉dup(),应该先阅读手册页:)
  • 我遵循了所有的旅游提示,但它仍然挂起......我还有什么可以做的吗?
  • 运行echo "Hi" | program /dev/stdin时程序会产生什么数据?是雪崩,还是只有几个字节?
【解决方案2】:

pipe(7) man 中写道,您应该尽快从管道中读取:

如果一个进程试图写入一个 完整的管道(见下文),然后 write(2) 阻塞,直到有足够的数据 从管道中读取以允许写入完成。非阻塞 I/O 可以通过使用 fcntl(2) F_SETFL 操作来启用 O_NONBLOCK 打开文件状态标志。

管道的容量有限。如果管道已满,则 write(2) 将阻塞或失败,取决于是否设置了 O_NONBLOCK 标志 (见下文)。不同的实现有不同的限制 管道容量。应用程序不应依赖于特定的 容量:一个应用程序应该被设计成一个读取过程 在数据可用时立即使用数据,以便写入过程 不会一直被阻止。

在您编写的代码中,等待然后再阅读

write(to_ext_program_pipe[1], string_to_write, strlen(string_to_write) + 1);
close(to_ext_program_pipe[1]);

wait(&rv);
//...
while(read(to_my_program_pipe[0], ch, 1) == 1) {
//...

可能管道已满或ext_program 正在等待读取数据,您应该只在读取之后wait()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-14
    • 2015-10-08
    • 2015-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多