web-dev-qa-db-ja.com

C system( "bash")はstdinを無視します

ファイル入力があります:

_$ cat input
1echo 12345
_

そして私は次のプログラムを持っています

第1バージョン

_#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()を呼び出します。

第2バージョン:

_#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が入力を受け取っていないのですか?

[〜#〜] note [〜#〜]私はいくつかのことを試しましたが、メインプロセスの新しい子をフォークすることで問題が解決することがわかりました。

第3バージョン

_#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
_

[〜#〜] os [〜#〜] Ubuntu 16.04 64ビット、gcc 5.4

7
Hedi Ghediri

ファイルストリーム は次のように定義されます

インタラクティブデバイスを参照しないと判断できる場合に限り、完全にバッファリングされます

標準入力にリダイレクトしているので、stdinは非対話型なのでバッファリングされます。

getchar はストリーム関数であり、ストリームからバッファーが満たされ、それらのバイトを消費して、1バイトを返します。 systemはfork-exec を実行するだけなので、サブプロセスは開いているすべてのファイル記述子をそのまま継承します。 bashが標準入力から読み取ろうとすると、すべてのコンテンツが親プロセスによって既に読み取られているため、すでにファイルの最後にあることがわかります。


あなたのケースでは、子プロセスに渡す前にその単一バイトのみを消費したいので、

setvbuf()関数は、streamが指すストリームが開いているファイルに関連付けられた後、他の操作[...]がストリームに対して実行される前に使用できます。

したがって、getchar()の前に適切な呼び出しを追加します。

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

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

stdinをバッファリングしないように設定することにより、必要なことを実行します(__IONBF_)。 getcharを指定すると、1バイトだけが読み取られ、残りの入力はサブプロセスで使用できます。代わりに read を使用する方がよい場合があります。この場合、ストリームインターフェース全体を回避します。


POSIXは、fork の後に両方のプロセスからハンドルにアクセスできる場合の特定の動作を義務付けていますが、

プロセスの1つによって実行される唯一のアクションが exec 関数[...]の1つである場合、ハンドルはそのプロセスでアクセスされません。

つまり、 は単にfork-exec なので、system()は特別なことを何もしません(しなければなりません)。

おそらくこれがforkの回避策によるものです。ハンドルが両側でアクセスされる場合は、 、最初のハンドル の場合:

読み取りを許可するモードでストリームが開いていて、基礎となる開いているファイルの説明がシーク可能なデバイスを参照している場合、アプリケーションは fflush() 、またはストリームが閉じられます。

読み取りストリームで fflush() を呼び出すと、次のようになります。

基本となるオープンファイルの説明のファイルオフセットは、ストリームのファイル位置に設定されます。

したがって、記述子の位置は、ストリームと同じ1バイトにリセットする必要があります。その後のサブプロセスは、そのポイントから標準入力を取得します。

さらに、2番目の(子の)ハンドルについて

上記の最初のハンドルに必要な場合を除いて、ファイルオフセットを明示的に変更した関数によって以前のアクティブハンドルが使用されている場合、アプリケーションは lseek() または fseek() (ハンドルのタイプに応じて)を適切な場所に。

「適切な場所」は同じかもしれないと思います(詳細は指定されていません)。 getchar()呼び出しは「ファイルオフセットを明示的に変更しました」ので、このケースが適用されます。このパッセージの意図は、フォークのどちらのブランチでも同じ効果を持つはずなので、fork() > 0fork() == 0の両方が同じように動作することです。ただし、このブランチでは実際には何も起こらないため、これらのルールを親と子のどちらにもまったく使用しないでください。

正確な結果はおそらくプラットフォームに依存します-少なくとも、「これまでにアクセス可能」と見なされるものは直接指定されておらず、どのハンドルが最初と2番目であるかも指定されていません。親プロセスには、以前のオーバーライドケースもあります。

この開いているファイル記述子のハンドルに対して実行する追加のアクションがそれを閉じることだけである場合、アクションを実行する必要はありません。

あとで終了するだけなので、プログラムに間違いなく当てはまります。その場合、fflush()を含む残りのケースはすべてスキップする必要があり、表示される動作は仕様からの逸脱になります。 fork()を呼び出すことは、ハンドルに対してアクションを実行することを構成しますが、明示的または明白ではないため、私はそれを信用しません。また、多くのバリエーションが許容されるように見える要件には、「どちらか」または「または」が十分にあります。

複数の理由が発生している動作はバグ、または少なくとも仕様の寛大な解釈の可能性があります。私の全体的な読みは、すべての場合でforkの1つのブランチは何もしないので、これらのルールはいずれも適用されるべきではなく、記述子の位置は無視されているはずです。私はそれについて明確にすることはできませんが、それは最も簡単な読みのようです。


私はforkテクニックが機能することに依存しません。ここでは、3番目のバージョンは機能しません。 代わりにsetbuf/setvbufを使用してください。可能であれば、 popen などを使用して、ストリームとファイル記述子の変化に依存するのではなく、必要なフィルタリングを使用してプロセスを明示的に設定します相互作用。

14
Michael Homer

Michael Homer氏の回答 、そしてそのPOSIXリファレンスを見つけたことを称賛します。しかし、私はこのことの少なくとも70%を理解していると私は信じており、私は彼の答えを完全には理解していません。そのため、このTL; DRバージョンを準備しました私が彼が言っていると思うことの。

  • getchar(a.k.a. getc)は、ファイルから読み取る場合、実際にはファイル(またはファイル全体のいずれか小さい方)からブロックを読み取ります。データをバッファに読み込みます。次に、バッファから最初の文字を取得します。

    • 後続の呼び出しは、バッファが使い果たされるまで、バッファから後続の文字を返します。次に、ファイルから別のブロックを(試みて)読み取ります。この「バッファI/O」により、プログラムがファイルを読み取って、一度に少しずつ処理することがはるかに効率的になります。


    バッファリングされたファイルストリームにファイルへの1文字の論理I/Oポインターがある場合でも、これによりファイル記述のI/Oポインターがファイルに1ブロック移動します(または、小さなファイル、ファイルの終わりまで)。 systemがフォークすると、子プロセスはファイルの説明を継承しますが、ファイルストリームは継承しません。そのため、bashはファイルの最後にあるファイルI/Oポインターを継承します。したがって、ファイルから読み取る場合、EOFのみを取得します。

  • プログラムの3番目のバージョンでは、mainforkを直接呼び出しています(systemを呼び出すだけでなく、forkを呼び出しています)。そしてもちろん、プログラムの2番目のバージョンと同様に、maingetcharを呼び出します。 Michaelが見つけたPOSIXドキュメント は、ISO C標準の拡張機能を説明していることを明示的に述べています。その暗号言語からわかる限り、main(または、おそらくCコンパイラ)が上記の問題に気づく責任がありますwhen fork is stdio関数を呼び出すルーチンによって直接呼び出され、およびそれを解決するために。したがって、明らかに、fork(またはその他のメカニズム?)はstdioファミリーと相互作用して、ファイル記述の物理I/Oポインターを元に戻し、ファイルポインターの論理I/Oポインターと同期させます。 ;つまり、ファイルに1文字入ります。 2番目のforksystemによって呼び出される隠しファイル—シェルは2番目のバイトからファイルを読み取ることができるので、これから偶然にもメリットがあります。

関連するmanページをざっと検索しましたが、説明されているこの動作は見つかりませんでした。また、結果を再現することもできませんでした。したがって、これはPOSIX仕様の動作である可能性がありますが、まだ一般的に実装されていないことは明らかです。

鼻の悪魔

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を介して)入力に直接アクセスします。 鼻の悪魔 があります。

1
muru