【问题标题】:stdin behaves differently when piped and when redirectedstdin 在管道传输和重定向时的行为不同
【发布时间】:2013-05-10 12:38:34
【问题描述】:

我正在尝试将信息传递到不接受来自标准输入的输入的程序中。为此,我使用 /dev/stdin 作为参数,然后尝试输入我的输入。我注意到如果我使用管道字符执行此操作:

[pkerp@comp ernwin]$ cat fess/structures/168d.pdb | MC-Annotate /dev/stdin

我没有输出。但是,如果我使用左脱字符号做同样的事情,它工作正常:

[pkerp@plastilin ernwin]$ MC-Annotate /dev/stdin < fess/structures/168d.pdb
Residue conformations -------------------------------------------
A1 : G C3p_endo anti
A2 : C C3p_endo anti
A3 : G C3p_endo anti

我的问题是,这两个操作有什么区别,为什么它们会给出不同的结果?作为一个额外的问题,是否有使用“

更新:

我目前最好的猜测是正在运行的程序内部的某些东西利用了文件内的查找。下面的答案似乎表明它与文件指针有关,但运行以下小测试程序:

#include <stdio.h>

int main(int argc, char *argv[])
{   
    FILE *f = fopen(argv[1], "r");
    char line[128];

    printf("argv[1]: %s f: %d\n", argv[1], fileno(f));

    while (fgets(line, sizeof(line), f)) {
    printf("line: %s\n", line);
    }

    printf("rewinding\n");
    fseek(f, 0, SEEK_SET);

    while (fgets(line, sizeof(line), f)) {
    printf("line: %s\n", line);
    }
    fclose(f);
}

表示在fseek 函数调用之前,一切都以相同的方式发生:

[pete@kat tmp]$ cat temp | ./a.out /dev/stdin
argv[1]: /dev/stdin f: 3
line: abcd

rewinding
===================
[pete@kat tmp]$ ./a.out /dev/stdin < temp
argv[1]: /dev/stdin f: 3
line: abcd

rewinding
line: abcd

使用 Christopher Neylan 建议的进程替换会导致上面的程序挂起,甚至没有读取输入,这似乎也有点奇怪。

[pete@kat tmp]$ ./a.out /dev/stdin <( cat temp )
argv[1]: /dev/stdin f: 3

查看 strace 输出证实我怀疑尝试了在管道版本中失败的查找操作:

_llseek(3, 0, 0xffffffffffd7c7c0, SEEK_CUR) = -1 ESPIPE (Illegal seek)

并在重定向版本中成功。

_llseek(3, 0, [0], SEEK_CUR)            = 0 

故事的寓意:不要随意尝试用/dev/stdin 替换参数并尝试使用它。它可能有效,但也可能无效。

【问题讨论】:

  • 查看这里的信息:stackoverflow.com/questions/1312922/…
  • 这很有趣,但它似乎没有进行这样的检查。无论如何, isatty() 的输出在此处提到的两种输入情况下不应该相同吗?这与没有重定向输入的链接帖子形成对比。
  • 根据第二个答案 | isatty 返回 YES 而 stackoverflow.com/a/7601564/2269047
  • 这也可能很有趣:stackoverflow.com/questions/1563882/…

标签: bash stdin


【解决方案1】:

这两个命令之间应该没有功能差异。确实,我无法重现您所看到的:

#! /usr/bin/perl
# test.pl
# this is a test Perl script that will read from a filename passed on the command line, and print what it reads.

use strict;
use warnings;

print $ARGV[0], " -> ", readlink( $ARGV[0] ), " -> ", readlink( readlink($ARGV[0]) ), "\n";
open( my $fh, "<", $ARGV[0] ) or die "$!";
while( defined(my $line = <$fh>) ){
        print "READ: $line";
}
close( $fh );

以三种方式运行:

(caneylan@faye.sn: tmp)$ cat input
a
b
c
d

(caneylan@faye.sn: tmp)$ ./test.pl /dev/stdin
/dev/stdin -> /proc/self/fd/0 -> /dev/pts/0
this is me typing into the terminal
READ: this is me typing into the terminal

(caneylan@faye.sn: tmp)$ cat input | ./test.pl /dev/stdin
/dev/stdin -> /proc/self/fd/0 -> pipe:[1708285]
READ: a
READ: b
READ: c
READ: d

(caneylan@faye.sn: tmp)$ ./test.pl /dev/stdin < input
/dev/stdin -> /proc/self/fd/0 -> /tmp/input
READ: a
READ: b
READ: c
READ: d

首先注意/dev/stdin是什么:

(caneylan@faye.sn: tmp)$ ls -l /dev/stdin
lrwxrwxrwx 1 root root 15 Apr 21 15:39 /dev/stdin -> /proc/self/fd/0

(caneylan@faye.sn: tmp)$ ls -l /proc/self
lrwxrwxrwx 1 root root 0 May 10 09:44 /proc/self -> 27565

它始终是/proc/self/fd/0 的符号链接。 /proc/self 本身就是当前进程的/proc 下目录的特殊链接。所以/dev/stdin总是指向当前进程的fd 0。因此,当您运行MC-Annotate(或者,在我的示例中为test.pl)时,无论MC-Annotate 的进程ID 是什么,文件/dev/stdin 都将解析为/proc/$pid/fd/0。这只是/dev/stdin 的符号链接如何工作的结果。

正如您在我的示例中所见,当您使用管道 (|) 时,/proc/self/fd/0 将指向由 shell 设置的 cat 管道的读取端。当您使用重定向 (&lt;) 时,/proc/self/fd/0 将直接指向由 shell 设置的输入文件。

至于为什么你会看到这种奇怪的行为——我猜MC-Annotate 在打开它之前正在对文件类型进行一些检查,它看到 /dev/stdin 指向的是命名管道而不是常规文件,并且正在纾困。您可以通过阅读MC-Annotate 的源代码或使用strace 命令查看内部发生的情况来确认这一点。

请注意,这两种方法在 Bash 中都有点迂回。将进程的输出获取到只会打开文件名的程序中的公认方法是使用process substitution

$ MC-Annotate <(cat fess/structures/168d.pdb)

&lt;(...) 构造将文件描述符返回到管道的读取端,该管道来自 ... 是什么:

(caneylan@faye.sn: tmp)$ echo <(true | grep example | cat)
/dev/fd/63

【讨论】:

    【解决方案2】:

    问题在于打开文件读取的顺序。

    /dev/stdin 不是真实文件;它是当前进程用作标准输入的文件的符号链接。在典型的 shell 中,它链接到终端,并由 shell 启动的任何进程继承。请记住,MC-Annotate 只会从作为参数提供的文件中读取。

    在管道示例中,/dev/stdin 是指向 MC-Annotate 作为标准输入继承的文件的符号链接:终端。它可能在一个新的描述符上打开这个文件(比如说 3,但它可以是任何大于 2 的值)。管道将cat 的输出连接到MC-Annotate's 标准输入(文件描述符0),MC-Annotate 继续忽略它以支持它直接打开的文件。

    在重定向示例中,shell 将fess/structures/168d.pdb 直接连接到文件描述符0 之前 MC-Annotate 运行。当MC-Annotate 启动时,它再次尝试打开/dev/stdin,这一次指向fess/structures/168d.pdb 而不是终端。

    所以答案在于/dev/stdin是执行MC-Annotate的进程中的链接;在进程开始之前设置shell重定向;管道进程开始之后。

    这行得通吗?

    cat fess/structures/168d.pdb | MC-Annotate <( cat /dev/stdin )
    

    类似的命令

    echo foo | cat <( cat /dev/stdin )
    

    似乎可行,但我不会声称情况相同。


    [更新:不起作用。 /dev/stdin 仍然是终端的链接,而不是管道。]

    这可能会提供一种解决方法。现在,MC-Annotate 从子shell 继承其标准输入,而不是当前shell,并且子shell 将cat 的输出作为其标准输入,而不是终端。

    cat fess/structures/168d.pdb | ( MC-Annotate /dev/stdin )
    

    它认为一个简单的命令组也可以工作:

    cat fess/structures/168d.pdb | { MC-Annotate /dev/stdin; }
    

    【讨论】:

    • 哇!很棒的描述。在接收管道输入时有什么方法可以欺骗它不打开新的文件描述符?
    • 查看我的更新。 MC-Annotate 仍然要打开一个新的文件描述符;我不认为该程序正在执行任何类型的内部检测:它只是打开命令行上显示的文件。但是,我的更新显示了一种在继承终端以外的东西作为标准输入的环境中运行 MC-Annotate 的方法。
    • 这是不正确的。 shell 在设置重定向之前 评估管道。这就是(echo stdout ; echo stderr 1&gt;&amp;2) 2&gt;&amp;1 | grep stdout 只打印“stdout”的原因。如果按照这里的建议首先设置了重定向,那么该命令将打印“stdout”和“stderr”,因为重定向的 dup2() 调用将在管道的 dup2() 之前发生 - 将 2 设置为终端和然后将 1 设置为指向 grep 的 0(这样你就可以看到两者)。但实际上,管道的 dup2() 首先发生--1 设置为指向 grep,然后 2 设置为指向 1(将指向 grep)。
    • @ChristopherNeylan 我认为这里的不同之处在于显式打开/dev/stdin 增加了复杂性,直到完成所有特定于shell 的内容之后才完成,并且在MC-Annotate 本身被执行之后。
    • 但这完全是另一回事,甚至不会增加复杂性——/dev/stdin 只是到 /proc/self/fd/0 的链接,所以 MC-Annotate 只是在做/proc/$$/fd/0 上的 open(),它已经由 shell 设置(不管重定向或流水线)。我上面的观点是,shell 在处理重定向之前为管道执行其 dup2()。
    【解决方案3】:

    通过查看有关 MC-Annotate http://bioinfo.cipf.es/ddufour/doku.php?id=mc-annotate 的信息,管道无法正常工作的原因是 MC-Annotate 无法将文件中的 cat 输出识别为 .pbd 类型之一

    管道链命令将第一个命令的输出用作下一个命令的输入。

    '

    http://tldp.org/LDP/abs/html/io-redirection.html#IOREDIRECTIONREF2

    【讨论】:

    • 为什么会这样呢? pdb 文件只是一个文本文件。如果执行以下cat out.pdb &gt; out1.pdb; diff out.pdb out1.pdb,则没有区别,导致我相信cat命令的输出与原始文件相同。
    • 因为 cat 的输出不是 pdb 文件。查看 MC-Annotate 的文档,有一个 -b 选项可以读取二进制文件而不是 pdb 文件。我猜想文件类型的逻辑是 MC-Annotate 内部的。
    • 我认为您可能是对的,因为我没有看到其他程序有类似的行为。但是,我仍然无法理解为什么文件和管道输入在它们包含的内容完全相同时应该被区别对待。
    • 如果命令在处理文件之前检查文件是否具有 pdb 扩展名,则管道输入将失败,因为它没有扩展名。
    • 事实并非如此。它在使用不同的文件扩展名时有效。尽管它可能正在尝试寻找来自终端的输入并且失败了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-25
    相关资源
    最近更新 更多