边缘情况 - 在 perl 中检测 STDIN 上的输入

边缘情况 - 在 perl 中检测 STDIN 上的输入

我不太知道如何问这个问题,我什至不确定这是问这个问题的地方。这看起来相当复杂,我对发生的事情没有完全理解。坦率地说,这就是我发帖的原因 - 以获得一些帮助来解决这个问题。我的最终目标是学习,而不是解决我的整体问题。我想了解我何时会遇到我将要描述的情况以及为什么会发生这种情况。

我有一个我一直在开发的 perl 模块。它所做的事情之一是检测标准输入是否有输入(无论是通过管道还是通过重定向(即<))。

为了捕获重定向,我针对不同的情况采用了一些不同的检查。其中之一是在输出中查找0r文件描述符。lsof它工作得相当好,我在很多脚本中使用我的模块没有问题,但我有 1 个用例,其中我的脚本认为它在 STDIN 上获取输入,但事实并非如此 - 这与我输入的内容有关lsof 输出。以下是我将案例范围缩小到的条件,但这并不是全部要求 - 我遗漏了一些东西。无论如何,这些条件似乎是必需的,但请对我的直觉持怀疑态度,因为我真的不知道如何在玩具示例中实现它 - 我已经尝试过 - 这就是为什么我知道我错过了某物:

  1. 当我通过反引号从 Perl 脚本内运行 Perl 脚本时,(内部脚本认为它已有意在 STDIN 上提供输入,而实际上它没有 - 尽管我应该指出,我不知道它是否是实际打开该句柄的父级或子级)
  2. 输入文件被提供给驻留在子目录中的内部脚本调用

0rlsof 报告的具有文件描述符的文件是:

/Library/Perl/5.18/AppendToPath

在其他条件下,此文件不会显示在 lsof 输出中。如果我在调用eof(STDIN)之前和之后执行lsof,结果每次都是 1。 -t STDIN未定义。 fileno(STDIN)0

我读到了这个文件这里如果我抓住它,它有:

>cat /Library/Perl/5.18/AppendToPath
/System/Library/Perl/Extras/5.18

看来这是一个 macOS-perl 特定的文件,旨在附加到@INCperl 路径,但我不知道其他操作系统是否提供类似的机制。

我想更多地了解该文件何时存在/打开以及何时关闭。我可以关闭它吗?看起来文件内容可能已经被解释器读入了 - 那么为什么它作为打开的文件句柄挂在我的脚本中?为什么它在 STDIN 上?当我自己实际重定向文件时,在这种情况下会发生什么?在我不知道的某些情况下,子进程是否以某种方式从父进程继承它?

更新:我发现了在子脚本执行期间使 AppendToPath 文件句柄在 STDIN 上打开所需的第三个(可能是最终的)要求。事实证明,我在父脚本的顶部有一行关闭 STDIN 的代码(可能是为了尝试解决类似的问题而添加的,当时我对检测 STDIN 上的输入的了解甚至比现在还少)。我注释掉了 close,一切都开始工作,无需排除那个奇怪的文件(即该文件:/Library/Perl/5.18/AppendToPath不再在 lsof 中的 STDIN 上显示为打开状态)。这是我注释掉的代码:

close(STDIN) if(defined(fileno(STDIN)) && fileno(STDIN) ne '' &&
                fileno(STDIN) > -1);

它上面有一条评论,内容如下:

#Prevent the passing of active standard in handles to the calls to the script
#being tested by closing STDIN.

所以我几年前写这篇文章时可能正在学习标准输入检测。我的模块可能最终使用了-t STDINand-f STDIN等,但我将它们切换出来以使用 lsof 解决像这样的问题,这样我就可以更好地看到发生了什么。因此-t/-f/-p,当我不关闭父级中的 STDIN 时,当前模块(使用 lsof 或我的新(/恢复?)简化版本 using工作得很好(按预期)。

但是,我仍然想了解为什么当父进程关闭 STDIN 时该文件位于子进程中的 STDIN 上......

答案1

如果某个用户从交互式 shell 调用您的脚本而不进行重定向,如下所示:

your-script with args

你的脚本将继承 shell 的标准输入,这将是一个 tty 设备,很可能以读+写模式打开。

如果用户将其调用为:

your-script with args < some-file

fd 0 将以只读模式打开some-file(任何类型;如果这样做< /dev/pts/0,那也将是一个 tty 设备;如果它是 fifo 文件,stdin 将显示为来自管道;如果是< /dev/null,那将是其他字符设备等)。

和:

your-script with args <> some-file

这将与上面相同,只是文件将以读+写模式打开,如果这样做<> /dev/pts/0,则与从终端中的交互式 shell 调用非重定向脚本时完全相同。

和:

your-script <&-

标准输入将被关闭。

和:

other-cmd | your-script

stdin 在大多数 shell 中将是一个管道(与执行< named-pipeor时相同< <(cmd)),但在 ksh93 中可能是套接字对。

you-script &非交互式 shell 中,stdin 将为/dev/null.

output=$(your-script)or output=`your-scrip`、 or中cmd <(your-script),stdin 将保持不变,但 stdout 将是一个管道。

your-script |&(ksh) 或coproc your-script(zsh, bash) 中,stdin 和 stdout 都将是管道。

如果您的脚本从以下位置启动:

ssh host your-script

也就是说,通过sshdon host,那么 stdin 和 stdout 也将是一个管道(通过rsh,这将是直接在读+写中的网络套接字)。

cron如果由or作业启动at,stdin 可能是/dev/null, stdout 一个管道(如果有输出,最终将在电子邮件中发送)。

ETC。

要从脚本中检测所有这些,不需要lsof.

检测:

  • stdin 是否打开:fcntl(STDIN, F_GETFL, 0)如果 stdin 未打开,执行 a 将失败。
  • 以哪种模式打开(r、w、rw):检查fcntl()上面的返回值中是否有 O_RDONLY、O_WRONLY、O_RDWR。
  • 在标准输入上打开的文件的类型(常规、管道、设备):执行fstat()系统调用(stat STDIN在 perl 中)并从其中的字段获取类型mode。或者你可以使用perl的-f// -d...-p来测试每种可能的文件类型。
  • 对于设备文件,无论是 tty 设备,都使用POSIX::isatty(STDIN)-t

但这些与回答这个问题没有什么关系:有什么可以从标准输入读取的吗或者会read()失败阻止或返回 EOF,为此您需要诸如poll().

我不确定你的最终目标是什么,但听起来你希望你的脚本有一个交互模式(用户与其交互)和一个自动化模式,并根据标准输入和/在两者之间切换或标准输出。

因此,这应该只是检查-t STDIN,也许还-t STDOUT检查 stdin 和/或 stdout 是否是 tty(是否有用户通过 tty 设备进行交互)。

答案2

当我通过反引号从 Perl 脚本内运行 Perl 脚本时,(内部脚本错误地认为 STDIN 上有输入)

内部脚本正确地认为 上有输入STDIN,只是另一个打开的文件获得了文件描述符 0(对于 perl 来说,它始终是文件句柄STDIN)。如您所知,通过perlqx{...}`...`在 perl 中运行的程序从外部脚本继承 stdin 文件描述符,就像任何其他子进程一样。

因为内部脚本继承了原始的文件描述符0,不是 perl STDIN文件句柄,这会产生缓冲问题,因为内部脚本或外部脚本最终可能会读取它需要的更多输入,直到没有为另一个脚本留下任何内容。考虑这个例子:

$ echo text | perl -e '$junk=`perl -e "eof(STDIN)"`; print while <>'
$ # nothing!

仅通过“测试 EOF”,内部脚本将不会为外部脚本留下任何输入。

然而,在内部脚本中进行无缓冲读取sysread将按预期工作:

$ cat inner.pl
sysread STDIN, $d, 2
$ echo text | perl -e '$junk = `perl inner.pl`; print while <>'
xt

[来自另一个答案]
With: your-script <&-stdin 将被关闭。

关闭像 stdin 这样的文件描述符从来都不是一个好主意(守护进程从 重定向它们/dev/null,它们永远不会关闭它们),但是当运行用 perl 或 python 等语言编写的脚本时尤其糟糕,因为这可能会导致 stdin 最终打开(并参考脚本)而不是关闭:

$ cat script.pl
seek STDIN, 0, 0;
print while <STDIN>;
$ perl script.pl <&-
seek STDIN, 0, 0;
print while <STDIN>;

发生这种情况是因为系统调用 likeopen(2)socket(2)返回第一个空闲文件描述符;如果 stdin 关闭,返回的 fd 将“成为”stdin。

答案3

到目前为止,答案回答了这个问题,但到目前为止,关于子 perl 进程何时以及为何打开一个名为 AppendToPath 的文件以在文件描述符 0 (STDIN) 上读取的具体问题尚未直接解决。 @zevzek 下面的评论阐明了可能发生的情况。我将保留选定的答案,因为它解释了事情是如何工作的,并提供了一种机制来解释 STDIN 最终如何成为标准输入以外的文件句柄,但我将把它放在文件的上下文AppendToPath中就我而言,有一个可重现的(在 macOS 上使用系统 perl)示例。

尽管无法知道文件描述符是如何“创建”的,但系统不会保留有关它的历史记录。如果您正在调试,它对跟踪您的程序有很大帮助,就像 strace -f ./your_script 一样。

根据上面的引用,我们不知道打开的是父进程还是子进程AppendToPath,但考虑到这AppendToPath是一个用于更新 perl 的文件@INC,这是早期需要的 - 它很可能是由子进程的 perl 解释器打开以准备的运行提供的脚本。

这是一个玩具示例,其中父级关闭 STDIN,而子级的 STDIN (fd 0) 结果是AppendToPath

bash-3.2$ perl -e 'close(STDIN); \
                   $c=q{perl -e } . \
                      chr(39) . \
                      print(fileno(STDIN),"\n"); \
                      q{print qx{lsof -w -b -p $$}} . \
                      chr(39); \
                   print `$c`'
0
COMMAND   PID     USER   FD   TYPE             DEVICE SIZE/OFF                NODE NAME
perl5.18 8901 robleach  cwd    DIR                1,8      832            35897246 /Users/robleach/GoogleDrive/WORK/RPST
perl5.18 8901 robleach  txt    REG                1,8    37552 1152921500311880916 /usr/bin/perl5.18
perl5.18 8901 robleach  txt    REG                1,8  1305808 1152921500312070866 /System/Library/Perl/5.18/darwin-thread-multi-2level/CORE/libperl.dylib
perl5.18 8901 robleach  txt    REG                1,8  1568368 1152921500312405021 /usr/lib/dyld
perl5.18 8901 robleach    0r   REG                1,8       33            90514450 /Library/Perl/5.18/AppendToPath
perl5.18 8901 robleach    1   PIPE 0xd289f4ba11f1bbb8    16384                     ->0x4d76dba4a1ac82fd
perl5.18 8901 robleach    2u   CHR               16,0  0t13390                 723 /dev/ttys000
perl5.18 8901 robleach    3   PIPE 0x9f2f7b3ec7eb66ba    16384                     ->0xc303b3e01efc707c

这个玩具示例没有关闭 STDIN,显示 fd 0 是一个 tty(即 STDIN)。

bash-3.2$ perl -e '$c=q{perl -e } . \
                      chr(39) . \
                      print(fileno(STDIN),"\n"); \
                      q{print qx{lsof -w -b -p $$}} . \
                      chr(39); \
                   print `$c`'
0
COMMAND   PID     USER   FD   TYPE             DEVICE SIZE/OFF                NODE NAME
perl5.18 8904 robleach  cwd    DIR                1,8      832            35897246 /Users/robleach/GoogleDrive/WORK/RPST
perl5.18 8904 robleach  txt    REG                1,8    37552 1152921500311880916 /usr/bin/perl5.18
perl5.18 8904 robleach  txt    REG                1,8  1305808 1152921500312070866 /System/Library/Perl/5.18/darwin-thread-multi-2level/CORE/libperl.dylib
perl5.18 8904 robleach  txt    REG                1,8  1568368 1152921500312405021 /usr/lib/dyld
perl5.18 8904 robleach    0u   CHR               16,0  0t14630                 723 /dev/ttys000
perl5.18 8904 robleach    1   PIPE 0xd289f4ba11f1bbb8    16384                     ->0x4d76dba4a1ac82fd
perl5.18 8904 robleach    2u   CHR               16,0  0t14630                 723 /dev/ttys000
perl5.18 8904 robleach    3   PIPE 0x9f2f7b3ec7eb66ba    16384                     ->0xc303b3e01efc707c

请注意,如果没有关闭 STDIN,该文件/Library/Perl/5.18/AppendToPath不会包含在 lsof 输出中。还值得注意的是,STDIN 文件描述符是fileno(STDIN)在子级中查询时定义的。

以下是我自己对@zenzek 的改写(经过编辑的引用):

引用 open(2) 联机帮助页:“成功调用返回的文件描述符将是当前未为进程打开的最小编号的文件描述符。”。关闭 STDIN (fd 0) 后,下一次调用 open 将获取该文件描述符 (0),并且-t STDIN子进程中的内容将是新打开的文件。这是一个例子:

perl -le 'close(STDIN); \
          open PW, "/etc/passwd"; \
          print fileno(PW); \
          print `perl -e "print fileno(STDIN),qq(\n)";lsof  -w -b -p $$`'
0
0
COMMAND    PID     USER   FD   TYPE             DEVICE SIZE/OFF                NODE NAME
perl5.18 13320 robleach  cwd    DIR                1,8      832            35897246 /Users/robleach/GoogleDrive/WORK/RPST
perl5.18 13320 robleach  txt    REG                1,8    37552 1152921500311880916 /usr/bin/perl5.18
perl5.18 13320 robleach  txt    REG                1,8  1305808 1152921500312070866 /System/Library/Perl/5.18/darwin-thread-multi-2level/CORE/libperl.dylib
perl5.18 13320 robleach  txt    REG                1,8  1568368 1152921500312405021 /usr/lib/dyld
perl5.18 13320 robleach    0r   REG                1,8     6946            90523670 /private/etc/passwd
perl5.18 13320 robleach    1u   CHR               16,0  0t23169                 723 /dev/ttys000
perl5.18 13320 robleach    2u   CHR               16,0  0t23169                 723 /dev/ttys000
perl5.18 13320 robleach    3   PIPE 0x5898fd9b21b123d7    16384                     ->0xc4c5f2aecc8eaaf7

更多相关引用:

文件描述符 0 是标准输入。它可能不是 stdio 的 stdin 流对象,也不是 perl 的 STDIN 文件句柄对象(两者都是更高级别的包装器,可能根本不引用任何实际的文件或文件描述符)。但它始终是通过 exec 继承的文件描述符,而不是任何更高级别的包装器。这就是当您通过反引号运行 lsof 或任何其他程序时会发生的情况(除非 fd 用 cloexec 标记,而标准 fd 不应该是这样)。任何文件描述符(包括 0)在打开时总是被继承,但在关闭时则不会被继承。如果 fd 0 被关闭,任何返回 fd 的函数(open()、accept()、socket()、epoll_create() 等)如果成功,都将返回 0,因为 0 是当前未使用的编号最小的 fd。

关闭 STDIN 后,我可以重现在给定 fd 0 的父级中打开的任意文件,但我无法重现在 fd 0 上的子级中打开的任意文件(因为我怀疑 AppendToPath 的打开甚至发生在子脚本)。我重现(上面)AppendToPath给出 fd 0 的文件。我只是无法明确确定是父级还是子级打开它。但我相信这是一个合理的猜测,它是由子进程中的 perl 解释器打开的。

相关内容