鼻恶魔

鼻恶魔

我有一个文件输入:

$ cat input
1echo 12345

我有以下程序

第一版

#include <stdio.h>
#include <stdlib.h>

int main() {
  system("/bin/bash -i");
  return 0;
}

现在如果我运行它,

$ gcc -o program program.c
$ ./program < input
bash: line 1: 1echo: command not found
$ exit

一切都按预期进行。

getchar()现在我想忽略文件输入的第一个字符,因此我在调用之前调用system().

第二版:

#include <stdio.h>
#include <stdlib.h>

int main() {
  getchar();
  system("/bin/bash -i");
  return 0;
}

令人惊讶的是,bash 立即退出,就像没有输入一样。

$ gcc -o program program.c
$ ./program < input
$ exit

问题为什么 bash 没有接收到输入?

笔记 我尝试了一些东西,发现为主进程分叉一个新子进程可以解决问题:

第三版

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

int main() {
  getchar();
  if (fork() > 0) {
    system("/bin/bash -i");
    wait(NULL);
  }
  return 0;
}

$ gcc -o program program.c
$ ./program < input
$ 12345
$ exit

操作系统Ubuntu 16.04 64位,gcc 5.4

答案1

文件流被定义为:

完全缓冲当且仅当可以确定不引用交互设备时

由于您要重定向到标准输入,因此 stdin 是非交互式的,因此它会被缓冲。

getchar是一个流函数,将导致缓冲区从流中填充,消耗这些字节,然后返回一个字节给您。system只运行 fork-exec,因此子进程按原样继承所有打开的文件描述符。当bash尝试从其标准输入读取时,它会发现它已经位于文件末尾,因为所有内容都已被您的父进程读取。


在您的情况下,您只想在移交给子进程之前消耗该单个字节,因此:

setvbuf()函数可以在流指向的流与打开的文件关联之后但在流上执行任何其他操作之前使用。

因此在之前添加一个合适的调用getchar()

#include <stdio.h>
#include <stdlib.h>

int main() {
  setvbuf(stdin, NULL, _IONBF, 0 );
  getchar();
  system("/bin/bash -i");
  return 0;
}

stdin通过设置为无缓冲 ( _IONBF)来执行您想要的操作。getchar将导致仅读取单个字节,其余输入将可供子进程使用。使用可能会更好read相反,在这种情况下,避免使用整个流接口。


当分叉后可以从两个进程访问句柄时,POSIX 强制执行某些行为,但明确指出

如果进程之一执行的唯一操作是其中之一exec函数[...],在该进程中永远不会访问句柄。

这意味着system()不需要(必须)对它做任何特别的事情,因为这只是 fork-exec

这可能就是您的fork解决方法所遇到的问题。如果手柄可从两侧接触到,然后对于第一个:

如果流以允许读取的模式打开,并且底层打开文件描述引用了能够查找的设备,则应用程序应执行fflush(),否则流将被关闭。

呼唤fflush()在读取流上意味着:

底层打开文件描述的文件偏移量应设置为流的文件位置

因此描述符位置应重置回 1 个字节,与流的位置相同,后续子进程将从该点开始获取其标准输入。

此外,对于第二个(子级)手柄:

如果任何先前的活动句柄已被显式更改文件偏移量的函数使用,除了上面第一个句柄的要求之外,应用程序应执行lseek()或者fseek()(根据手柄的类型)到适当的位置。

我想“适当的位置”可能是相同的(尽管没有进一步指定)。调用getchar()“显式更改了文件偏移量”,因此这种情况应该适用。这段话的目的是,在叉子的任一分支中工作应该具有相同的效果,因此fork() > 0和都fork() == 0应该起到相同的作用。然而,由于该分支中实际上没有发生任何事情,因此可以说这些规则根本不应该用于父级或子级。

确切的结果可能取决于平台 - 至少,没有直接指定什么算作“可以访问”,也没有直接指定哪个句柄是第一个和第二个。父进程还有一个更早的、最重要的情况:

如果对此打开的文件描述符的任何句柄执行的唯一进一步操作是关闭它,则无需执行任何操作。

这可以说适用于您的程序,因为它随后就会终止。如果确实如此,则应跳过所有剩余的情况,包括fflush(), ,并且您看到的行为将偏离规范。有争议的是,调用fork()构成了在句柄上执行操作,但并不明确或明显,所以我不相信这一点。要求中也有足够的“要么”和“或”,所以很多变化似乎是允许的。

由于多种原因我认为你看到的行为可能是一个错误,或者至少是对规范的慷慨解释。我的总体解读是,由于在每种情况下 的一个分支fork都不执行任何操作,因此不应应用这些规则,并且描述符位置应在其所在位置被忽略。我对此无法确定,但这似乎是最直接的阅读。


我不会依赖fork技术的运作。你的第三个版本在这里对我不起作用。使用setbuf/setvbuf反而。如果可能的话,我什至会使用popen或者类似于显式地设置具有必要过滤的过程,而不是依赖于变幻莫测的流和文件描述符交互。

答案2

我鼓掌迈克尔·霍默的回答,以及查找 POSIX 参考。但我相信我至少理解了这些东西的 70%,而我并不完全理解他的回答——所以我准备了这个长话短说我相信他所说的版本。

  • getchar(又名getc),当从文件读取时,实际上会从文件中读取一个块(或整个文件,以较小者为准)。它将数据读入缓冲区。然后它会给你缓冲区中的第一个字符。

    • 后续调用将从缓冲区返回后续字符,直到缓冲区耗尽。然后它将(尝试)从文件中读取另一个块。这种“缓冲 I/O”使得程序读取文件并一次处理一点点的效率更高。


    即使缓冲文件流的逻辑 I/O 指针是文件中的一个字符,这也会导致文件描述的 I/O 指针被移动到文件中的一个块(或者,在小文件的情况下,文件末尾)。当systemfork时,子进程继承文件描述,但不继承文件流,因此bash继承位于文件末尾的文件I/O指针。因此,当它读取文件时,它只得到一个 EOF。

  • 在程序的第三个版本中, mainfork直接调用(而不是仅仅调用system,后者调用fork)。当然,就像在程序的第二个版本中一样, main调用getcharMichael 找到的 POSIX 文档明确表示它正在描述 ISO C 标准的扩展。据我从它神秘的语言中可以看出,它是说main(或者,我想是 C 编译器)负责注意到上述问题 Whenfork由调用函数的例程直接调用stdio 并解决它。因此,显然,fork(或其他一些机制?)与stdio族交互以使文件描述的物理 I/O 指针移回以与文件指针的逻辑 I/O 指针同步;即,将一个字符放入文件中。第二个fork——隐藏的,被调用system——偶然地受益于这一点,因为它导致 shell 能够从第二个字节开始读取文件。

我粗略地搜索了相关的手册页,但找不到描述这种行为的信息。我也无法重现你的结果。因此,这可能是 POSIX 指定的行为,但显然尚未普遍实现。

答案3

鼻恶魔

man 3 getchar说:

The getchar() function shall be equivalent to getc(stdin).

man 3 getc说:

It is not advisable to mix calls to  input  functions  from  the  stdio
library  with  low-level  calls  to  read(2)  for  the  file descriptor
associated with the input stream; the results  will  be  undefined  and
very probably not what you want.

交互模式下的 Bash 可能使用read等(通过 readline)来直接访问输入。所以我们有鼻恶魔

相关内容