2016-01-05 4 views
9

Ich mache eine kleine Ncurses-Anwendung in Rust, die mit einem Kindprozess kommunizieren muss. Ich habe bereits einen Prototyp in Common Lisp geschrieben; Das gif here wird hoffentlich zeigen, was ich machen möchte. Ich versuche es neu zu schreiben, weil CL eine große Menge an Speicher für ein so kleines Werkzeug benötigt.Wie lese ich die Ausgabe eines Child-Prozesses ohne Blockierung in Rust?

Ich habe Rust nicht vorher (oder andere Low-Level-Sprachen) verwendet, und ich habe einige Schwierigkeiten herauszufinden, wie man mit dem Unterprozess interagieren.

Was ich derzeit tun, ist in etwa so:

  1. den Prozess erstellen:

    let mut program = match Command::new(command) 
        .args(arguments) 
        .stdin(Stdio::piped()) 
        .stdout(Stdio::piped()) 
        .stderr(Stdio::piped()) 
        .spawn() { 
         Ok(child) => child, 
         Err(_) => { 
          println!("Cannot run program '{}'.", command); 
          return; 
         }, 
        }; 
    
  2. Pass es zu einer unendlichen (bis User-Exits) Schleife, die Eingabe liest und Griffe und wartet auf eine Ausgabe wie diese (und schreibt sie auf den Bildschirm):

    fn listen_for_output(program: &mut Child, 
            output_viewer: &TextViewer) { 
        match program.stdout { 
         Some(ref mut out) => { 
          let mut buf_string = String::new(); 
          match out.read_to_string(&mut buf_string) { 
           Ok(_) => output_viewer.append_string(buf_string), 
           Err(_) => return, 
          }; 
         }, 
         None => return, 
        }; 
    } 
    

Der Aufruf an read_to_string blockiert jedoch das Programm, bis der Prozess beendet wird. Von dem, was ich sehen kann read_to_end und read scheinen auch zu blockieren. Wenn ich versuche, etwas wie ls auszuführen, das sofort beendet wird, funktioniert es, aber mit etwas, das nicht wie python oder sbcl beendet wird, fährt es nur fort, sobald ich den Unterprozess manuell beende.

Edit:

Basierend auf this answer, änderte ich den Code BufReader zu verwenden:

fn listen_for_output(program: &mut Child, 
        output_viewer: &TextViewer) { 
    match program.stdout.as_mut() { 
     Some(out) => { 
      let buf_reader = BufReader::new(out); 
      for line in buf_reader.lines() { 
       match line { 
        Ok(l) => { 
         output_viewer.append_string(l); 
        }, 
        Err(_) => return, 
       }; 
      } 
     }, 
     None => return, 
    } 
} 

jedoch das Problem immer noch gleich bleibt. Es liest alle verfügbaren Zeilen und blockiert dann. Da das Werkzeug mit jedem Programm arbeiten soll, gibt es keine Möglichkeit zu erraten, wann die Ausgabe enden wird, bevor versucht wird zu lesen. Es scheint auch keine Möglichkeit zu geben, ein Timeout für BufReader zu setzen.

Antwort

11

Streams sind blockiert standardmäßig. TCP/IP-Streams, Dateisystemströme und Pipe-Streams blockieren alle. Wenn Sie einem Datenstrom mitteilen, dass er einen Teil der Bytes erhalten soll, wird er angehalten und gewartet, bis er die angegebene Menge an Bytes erreicht oder bis etwas anderes passiert (ein interrupt, ein Ende des Streams, ein Fehler).

Die Betriebssysteme sind bestrebt, die Daten an den Leseprozess zurückzugeben. Wenn Sie also nur auf die nächste Zeile warten und sie verarbeiten möchten, funktioniert die von Shepmaster in Unable to pipe to or from spawned child process more than once vorgeschlagene Methode. (In der Theorie muss es nicht, denn ein Betriebssystem darf die BufReader warten auf mehr Daten in read, aber in der Praxis bevorzugen die Betriebssysteme die frühen "kurzen Lesevorgänge" zu warten).

Dieser einfache BufReader -basierte Ansatz funktioniert nicht mehr, wenn mehrere Streams (wie stdout und stderr eines untergeordneten Prozesses) oder mehrere Prozesse verarbeitet werden müssen. Zum Beispiel kann der BufReader -basierte Ansatz einen Deadlock verursachen, wenn ein untergeordneter Prozess darauf wartet, dass Sie die Pipeline stderr leeren, während Ihr Prozess blockiert ist, und darauf wartet, dass er leer ist stdout.

In ähnlicher Weise können Sie nicht BufReader verwenden, wenn Sie nicht möchten, dass Ihr Programm unbegrenzt auf den untergeordneten Prozess wartet. Vielleicht möchten Sie eine Fortschrittsanzeige oder einen Timer anzeigen, während das Kind noch arbeitet und Ihnen keine Ausgabe gibt.

Sie können BufReader-basierten Ansatz nicht verwenden, wenn Ihr Betriebssystem nicht eifrig die Daten an den Prozess zurückgibt (bevorzugt "vollständige liest" zu "kurze Lesevorgänge"), weil in diesem Fall ein paar letzte Zeilen gedruckt durch den Kindprozess kann in einer grauen Zone enden: das Betriebssystem hat sie, aber sie sind nicht groß genug, um den BufReader Puffer zu füllen.

BufReader ist auf was die Read Schnittstelle ermöglicht es mit dem Stream zu tun, es ist nicht weniger blockiert als der zugrunde liegende Stream ist. Um effizient zu sein, wird es read die Eingabe in Chunks, das Betriebssystem zu sagen, so viel von seinem Puffer zu füllen, wie es zur Verfügung hat.

Sie wundern sich vielleicht, warum das Lesen von Daten in Chunks hier so wichtig ist, warum die BufReader die Daten nicht Byte für Byte lesen kann. Das Problem ist, dass wir zum Lesen der Daten aus einem Stream die Hilfe des Betriebssystems benötigen. Auf der anderen Seite sind wir nicht das Betriebssystem, wir arbeiten isoliert davon, um uns nicht damit herumzuschlagen, wenn etwas mit unserem Prozess schief geht. Um auf das Betriebssystem zugreifen zu können, muss daher in den "Kernel-Modus" gewechselt werden, was ebenfalls zu einem "Kontextwechsel" führen kann. Deshalb ist der Aufruf des Betriebssystems, jedes einzelne Byte zu lesen, teuer. Wir wollen so wenige OS-Aufrufe wie möglich und so erhalten wir die Stream-Daten in Stapeln.

Um auf einen Stream ohne Blockierung zu warten, benötigen Sie einen nicht blockierenden Stream. MIO promises to have the required non-blocking stream support for pipes, höchstwahrscheinlich mit PipeReader, aber ich habe es bisher nicht überprüft.

Die nicht blockierende Natur eines Streams sollte das Lesen von Daten in Chunks ermöglichen, unabhängig davon, ob das Betriebssystem die "kurzen Lesevorgänge" bevorzugt oder nicht. Da der nicht blockierende Stream niemals blockiert. Wenn es keine Daten im Stream gibt, sagt es Ihnen das einfach.

In der Abwesenheit eines nicht blockierenden Streams müssen Sie auf die Erstellung von Threads zurückgreifen, damit die blockierenden Lesevorgänge in einem separaten Thread ausgeführt werden und somit Ihren primären Thread nicht blockieren. Sie können den Stream auch byteweise lesen, um sofort auf das Zeilentrennzeichen zu reagieren, falls das Betriebssystem die "kurzen Lesevorgänge" nicht bevorzugt. Hier ist ein funktionierendes Beispiel: https://gist.github.com/ArtemGr/db40ae04b431a95f2b78.

+0

Danke für die hilfreiche Erklärung. Ich werde in MIO schauen, und wenn das nicht klappt, werde ich separate Threads verwenden. – jkiiski

Verwandte Themen