谁切断了进程替换文件的第一个符号以及出于什么原因?

谁切断了进程替换文件的第一个符号以及出于什么原因?
$ type 1.sh
#!/bin/bash -eu
php <(echo 12)
$ ./1.sh
2

$ type 2.sh
#!/bin/bash -eu
cat <(echo 12)
$ ./2.sh
12

$ type 3.sh
#!/bin/bash -eu
echo 12 | php
$ ./3.sh
12

$ type 4.sh
#!/bin/bash -eu
rm -f named_pipe
mknod named_pipe p
echo 12 > named_pipe
$ ./4.sh
$ php named_pipe   # from another window
2

我在Debian( php-5.4.14, bash-4.1.5) 和Arch Linux( php-5.4.12, bash-4.2.42) 上进行了测试。

答案1

当然是 PHP。管道不会吃掉文件的第一个字符。 PHP 正在读取所有字符,但不输出第一个字符。到目前为止,您无法判断问题是在输入中还是在输出中:可能是 PHP 由于某种原因没有输出第一个字符。

一个小实验表明问题确实出在输入上。

$ php <(echo '<?php echo "hello" ?>') 
?php echo "hello" ?>
$ php <(echo ' <?php echo "hello" ?>')
hello$ 

仅当脚本由文件名给出时(不是当没有命令行参数并且脚本从标准输入读取时),仅当脚本文件是管道或其他时,PHP 才会吃掉脚本的第一个字符不可查找的文件(当脚本文件可查找时,例如,当它是常规文件时)。

发生的事情是,在一开始,在正常的 PHP 解析器启动之前,命令行处理器会检查脚本是否以舍邦线。如果脚本以两个字符 开头#!,PHP 会跳过第一行。这样你就可以写一个像这样的PHP脚本

#!/usr/bin/php
first line
<?php echo "second line"?>

该脚本将输出

first line
second line

#!/usr/bin/php并且一开始就没有虚假。

shebang 探测器的工作原理如下:

  • 读出第一个字符。
  • 如果第一个字符是#,则读取另一个字符。
  • 如果前两个字符是#!,则继续阅读直到第一个换行符。
  • 如果前两个字符不是#!,则倒回到文件的开头。
  • 开始正常的PHP解析。

如果脚本文件是不可查找的,则倒带步骤会失败,但 PHP 不会尝试检测到这一点,因此文件的第一个字符会丢失(如果第一个字符是 a,则还会丢失第二个字符#)。这是 PHP 命令行解释器中的一个错误。

您可以看到代码为你自己(在函数中cli_seek_file_begin)。

答案2

我可以重现你在 Ubuntu 上看到的内容:

#!/bin/bash -eu
rm -rf named_pipe
mkfifo named_pipe
echo 12 >  named_pipe


$ ./namedpipe.sh &  # background it
$ php named_pipe    # same terminal; don't need another window
2

这是与php.我无法重现它cat(这表明内核中的 fifo 代码已严重损坏)。

日志strace显示php它已从管道读取数据:两个数字和一个换行符:

open("named_pipe", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFIFO|0664, st_size=0, ...}) = 0
mmap2(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb6fdc000
read(3, "12\n", 65536)                  = 3

它尝试的下一个文件描述符3是 lseek:

_llseek(3, 0, 0xbf93c640, SEEK_SET)     = -1 ESPIPE (Illegal seek)

这就是事情可能出错的地方。假设这是通过一些缓冲 I/O 库调用的,该库会被管道混淆并弄乱其缓冲区。

此时,还会发生许多其他操作。

随后它又尝试了一些操作,包括尝试获取 tty 设置和再次读取,这清楚地表明了 EOF:

ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0xbf93a2e8) = -1 EINVAL (Invalid argument)
fstat64(3, {st_mode=S_IFIFO|0664, st_size=0, ...}) = 0
read(3, "", 65536)                      = 0

PHP 不认为 Unix 管道的阻塞读取的零返回意味着 EOF,因此它会再次尝试:

read(3, "", 65536)                      = 0
close(3)              

跟踪中接下来的两行是这些。在读取之前分配的缓冲区被释放(可能是流缓冲区?),然后是切碎的输出:

munmap(0xb6fdc000, 65536)               = 0
write(1, "2\n", 2)                      = 2

这样就基本回答了哪个软件负责的问题。如果您想深入研究它,一种前进的方法是获得 的调试版本php并使用gdb.


附录:

从标准输入读取时,行为完全不同php,就像本echo 12 | php例一样。例如,llseek永远不会在文件描述符 0 上尝试该操作。此外,流永远不会关闭(当然),并且读取和写入之间没有中间操作。以下显示为连续块:

read(0, "12\n", 4096)                   = 3
read(0, "", 4096)                       = 0
read(0, "", 4096)                       = 0
write(1, "12\n", 3)                     = 3

人们会期望打开在命令行上命名的文件与从标准输入(可能由某些专门设置的全局标准输入流对象表示)读取相比会经历不同的流程。

相关内容