Das C-System ("bash") ignoriert stdin

7

Ich habe eine Dateieingabe:

$ cat input
1echo 12345

und ich habe folgendes programm

1. Version

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

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

Wenn ich es jetzt laufen lasse,

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

Alles funktioniert wie erwartet.

Jetzt möchte ich das erste Zeichen der Dateieingabe ignorieren, also rufe ich getchar()vor dem Aufruf an system().

2. Version:

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

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

Überraschenderweise wird die Bash sofort beendet, als gäbe es keine Eingabe.

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

Frage, warum bash die Eingabe nicht empfängt?

HINWEIS Ich habe einige Dinge ausprobiert und festgestellt, dass das Problem dadurch behoben wird, dass ein neues Kind für den Hauptprozess gegabelt wird:

3. Version

#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 64bit, gcc 5.4

Hedi Ghediri
quelle
Was für eine Bash ist das, die keinen Fehler zurückgibt, wenn sie -cohne Befehl ausgegeben wird ?
Muru
Ja, es war ein Tippfehler. -i ist die Option verwendet
Hedi Ghediri
Auf jeden Fall ist dies eine C-Programmierfrage und sollte bei Stack Overflow gestellt werden .
Muru
3
Es ist eine Frage zur Unix C-API von getchar und ihren Interaktionen mit fork-exec.
Michael Homer
Dies ist eine relevante Beschreibung: repository.root-me.org/Administration/Unix/Linux/…
wulfgarpro

Antworten:

14

Ein Dateistream ist definiert als :

genau dann vollständig gepuffert, wenn festgestellt werden kann, dass kein interaktives Gerät verwendet wird

Da Sie in die Standardeingabe umleiten, ist stdin nicht interaktiv und daher gepuffert.

getcharist eine Stream-Funktion und bewirkt, dass der Puffer aus dem Stream gefüllt wird, diese Bytes verbraucht und dann ein einzelnes Byte an Sie zurückgibt. systemführt nur fork-exec aus , sodass der Unterprozess alle geöffneten Dateideskriptoren unverändert erbt. Beim bashVersuch, von der Standardeingabe zu lesen, wird festgestellt, dass sie sich bereits am Ende der Datei befindet, da der gesamte Inhalt bereits von Ihrem übergeordneten Prozess gelesen wurde.


In Ihrem Fall möchten Sie nur dieses einzelne Byte verbrauchen, bevor Sie es dem untergeordneten Prozess übergeben.

Die setvbuf()Funktion kann verwendet werden, nachdem der Stream, auf den der Stream zeigt, einer geöffneten Datei zugeordnet ist, bevor jedoch eine andere [...] Operation für den Stream ausgeführt wird.

So fügen Sie einen geeigneten Anruf vor dem getchar():

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

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

wird tun, was Sie wollen, indem Sie stdinauf ungepuffert setzen ( _IONBF). getcharbewirkt, dass nur ein einzelnes Byte gelesen wird und der Rest der Eingabe für den Unterprozess verfügbar ist. readIn diesem Fall ist es möglicherweise besser, stattdessen die gesamte Streams-Schnittstelle zu verwenden.


POSIX schreibt bestimmte Verhaltensweisen vor, wenn nach einem Fork von beiden Prozessen aus auf das Handle zugegriffen werden kann, merkt dies jedoch ausdrücklich an

Wenn die einzige Aktion, die von einem der Prozesse ausgeführt wird, eine der execFunktionen [...] ist, wird in diesem Prozess niemals auf das Handle zugegriffen.

was bedeutet, dass system()das nichts Besonderes damit zu tun hat, da es nur Fork-Exec ist .

Dies ist wahrscheinlich das, was Ihre forkProblemumgehung trifft. Wenn der Griff auf beiden Seiten zugänglich ist, dann für den ersten :

Wenn der Stream mit einem Modus geöffnet ist, der das Lesen ermöglicht, und sich die zugrunde liegende Beschreibung der geöffneten Datei auf ein Gerät bezieht, das suchen kann, muss die Anwendung entweder einen ausführen fflush()oder der Stream wird geschlossen.

Das Aufrufen fflush()eines Lesestreams bedeutet Folgendes:

Der Dateiversatz der zugrunde liegenden Beschreibung der geöffneten Datei wird auf die Dateiposition des Streams gesetzt

Daher sollte die Deskriptorposition auf 1 Byte zurückgesetzt werden, genau wie die des Streams, und der nachfolgende Unterprozess erhält ab diesem Punkt seine Standardeingabe.

Zusätzlich für den zweiten (Kinder-) Griff :

Wenn ein zuvor aktives Handle von einer Funktion verwendet wurde, die den Dateiversatz explizit geändert hat, außer wie oben für das erste Handle erforderlich, muss die Anwendung ein lseek()oder fseek()(je nach Handle-Typ) an einer geeigneten Stelle ausführen .

und ich nehme an, "ein geeigneter Ort" könnte der gleiche sein (obwohl es nicht weiter spezifiziert ist). Der getchar()Aufruf "hat den Dateiversatz explizit geändert", daher sollte dieser Fall zutreffen. Die Absicht der Passage ist, dass das Arbeiten in beiden Zweigen der Gabel den gleichen Effekt haben sollte, also beide fork() > 0und fork() == 0das gleiche funktionieren sollten. Da in diesem Zweig jedoch tatsächlich nichts passiert, ist es fraglich, ob keine dieser Regeln für Eltern oder Kinder verwendet werden sollte.

Das genaue Ergebnis ist wahrscheinlich plattformabhängig - zumindest wird nicht direkt angegeben, was als "jemals zugänglich" gilt, und welches Handle das erste und zweite ist. Es gibt auch einen früheren, übergeordneten Fall für den übergeordneten Prozess:

Wenn die einzige weitere Aktion, die an einem Handle für diesen geöffneten Dateideskriptor ausgeführt werden muss, darin besteht, ihn zu schließen, muss keine Aktion ausgeführt werden.

Dies gilt wohl für Ihr Programm, da es erst danach beendet wird. Wenn dies der Fall ist fflush(), sollten alle verbleibenden Fälle, einschließlich der , übersprungen werden, und das angezeigte Verhalten wäre eine Abweichung von der Spezifikation. Es ist fraglich, ob das Aufrufen fork()eine Aktion für das Handle darstellt, aber nicht explizit oder offensichtlich, also würde ich dem nicht vertrauen. Es gibt auch genug "entweder" und "oder" in den Anforderungen, so dass viele Variationen zulässig erscheinen.

Aus mehreren Gründen denke ich, dass das Verhalten, das Sie sehen, ein Fehler oder zumindest eine großzügige Interpretation der Spezifikation sein kann. Meine allgemeine Lesart ist, dass, da in jedem Fall ein Zweig der forknichts tut, keine dieser Regeln angewendet werden sollte und die Deskriptorposition ignoriert werden sollte, wo sie war. Ich kann das nicht definitiv sagen, aber es scheint die einfachste Lektüre zu sein.


Ich würde mich nicht darauf verlassen, dass die forkTechnik funktioniert. Ihre dritte Version funktioniert hier bei mir nicht. Verwenden Siesetbufsetvbuf stattdessen / . Wenn möglich, würde ich sogar popenoder ähnlich verwenden, um den Prozess mit der erforderlichen Filterung explizit einzurichten, anstatt mich auf die Unklarheiten der Stream- und Dateideskriptor-Interaktionen zu verlassen.

Michael Homer
quelle
Bitte lesen Sie meine Antwort und geben Sie jede Antwort, die Sie geben möchten.
G-Man sagt 'Reinstate Monica'
2

Ich begrüße Michael Homer für seine Antwort und dafür, dass er diese POSIX-Referenz gefunden hat. Aber ich glaube, dass ich mindestens 70% dieses Materials verstehe, und ich verstehe seine Antwort nicht vollständig - also habe ich diese TL; DR- Version dessen vorbereitet, was er meiner Meinung nach sagt.

  • getchar(aka getc) liest beim Lesen aus einer Datei tatsächlich einen Block aus der Datei (oder die gesamte Datei, je nachdem, welcher Wert geringer ist). Es liest Daten in einen Puffer. Sie erhalten dann das erste Zeichen aus dem Puffer.

    • Nachfolgende Aufrufe geben nachfolgende Zeichen aus dem Puffer zurück, bis der Puffer erschöpft ist. Es wird dann (versuchen) einen anderen Block aus der Datei lesen. Diese „gepufferte E / A“ macht es für ein Programm viel effizienter, eine Datei zu lesen und jeweils ein wenig damit umzugehen.


    Obwohl der gepufferte Dateistream einen logischen E / A-Zeiger hat, der ein Zeichen in der Datei enthält, wird der E / A-Zeiger der Dateibeschreibung um einen Block in die Datei verschoben (oder im Fall Ihrer kleinen Datei nach) das Ende der Datei). Beim systemGabeln erbt der untergeordnete Prozess die Dateibeschreibung, nicht jedoch den Dateistream. Daher erbt bash einen Datei-E / A-Zeiger, der sich am Ende der Datei befindet. Daher erhält es beim Lesen aus der Datei nur einen EOF.

  • In der dritten Version Ihres Programms mainwird forkdirekt aufgerufen (anstatt nur aufzurufen system, was aufruft fork). Und natürlich, wie in der zweiten Version Ihres Programms, mainAufrufe getcharDas von Michael gefundene POSIX-Dokument besagt ausdrücklich, dass es eine Erweiterung des ISO C-Standards beschreibt. Soweit ich aus der kryptischen Sprache ersehen kann, heißt es, dass main(oder vermutlich der C-Compiler) dafür verantwortlich ist, das obige Problem zu bemerken, wenn forkes direkt von der Routine aufgerufen wird, die die stdioFunktion (en) aufruft , und es zu lösen es. Daher forkinteragiert anscheinend (oder ein anderer Mechanismus?) Mit demstdioFamilie, um den physischen E / A-Zeiger der Dateibeschreibung zurückzuschieben, um ihn mit dem logischen E / A-Zeiger des Dateizeigers zu synchronisieren; dh ein Zeichen in die Datei. Die zweite fork- die versteckte, die von aufgerufen wird system- profitiert zufällig davon, da die Shell die Datei ab dem zweiten Byte lesen kann.

Ich habe die relevanten Manpages flüchtig durchsucht und konnte dieses beschriebene Verhalten nicht finden. Und ich konnte Ihr Ergebnis auch nicht reproduzieren. Dies mag also das von POSIX spezifizierte Verhalten sein, aber es ist eindeutig noch nicht universell implementiert.

G-Man sagt "Reinstate Monica"
quelle
Ich denke nicht, dass es erforderlich ist, dass dieselbe Routine sowohl die Funktionen stdio als auch fork verwendet, nur dass die Verwendung von stdio sowohl vor als auch nach der Fork erfolgt. Es ist nicht unbedingt der Fall, dass das gepufferte Lesen einen ganzen Block dauert, obwohl dies eine plausible Puffergröße ist.
Michael Homer
Ich habe meine Antwort bearbeitet, um auch die Situation für den übergeordneten Prozess anzuzeigen, der eigentlich deutlicher sein sollte. Meine Lektüre der Spezifikation ist, dass, da im übergeordneten Element tatsächlich nichts mehr auf den Stream zugreift, tatsächlich nichts dagegen unternommen werden sollte und das beobachtete Verhalten des Abfragenden daher möglicherweise ein Fehler ist. Es ist fraglich, ob ein erneuter Aufruf forkmöglicherweise das Handle verwendet.
Michael Homer
Wenn ich weiter denke, sieht es für mich so aus, als ob - da in beiden Gabeln nie etwas mit dem Griff in einem Zweig passiert - keine dieser Regeln angewendet werden sollte und die Position so belassen werden sollte, wie sie war.
Michael Homer
1

Nasendämonen

man 3 getchar sagt:

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

Und man 3 getcsagt:

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 im interaktiven Modus verwendet wahrscheinlich readusw. (über Readline), um direkt auf die Eingabe zuzugreifen. Wir haben also Nasendämonen .

muru
quelle
2
Dies ist nicht ganz richtig - selbst wenn bashdie stdioBibliothek verwendet wird, greift sie auf andere Puffer als den übergeordneten Prozess zu, sodass das Ergebnis immer noch undefiniert ist.
Nneonneo