Themen
Process und fork fuer Einsteiger
Dieses Kapitel ist absichtlich fuer Leute geschrieben, die mit Child-Processes, Pipes oder IPC bisher kaum etwas gemacht haben.
Wenn dir Woerter wie stdin, stdout oder fork erstmal gross vorkommen,
dann nimm fuer den Anfang nur diese zwei Bilder mit:
Antra::Processstartet ein anderes Programm.System.fork(...)startet einen zweiten Antra-Kindprozess aus dem laufenden Interpreter heraus.
Das ist fast schon die halbe Wahrheit.
Die Kurzversion#
Wenn du nur schnell wissen willst, was du nehmen sollst:
- Nimm
Antra::Process, wenn du ein externes Programm wieperl,curloder ein Shell-Tool starten willst. - Nimm
System.fork(...), wenn Eltern- und Kindprozess beide Antra sprechen und Werte hin- und herschicken sollen.
Das Grundgefuehl#
Antra::Process#
Process.spawn(...) bedeutet:
"Starte bitte ein anderes Programm und gib mir einen Handle darauf zurueck."
Dieser Handle ist ein opaker Process-Wert. Ueber ihn kannst du dann:
- die PID lesen
- Text an
stdinschreiben stdoutundstderrlesen- Signale wie
TERModerKILLschicken - auf das Ende des Prozesses warten
System.fork(...)#
System.fork(...) bedeutet:
"Erzeuge eine Kopie des aktuellen Prozesses und fuehre im Kind eine Antra-Funktion aus."
Der Elternprozess bekommt ebenfalls einen Process-aehnlichen Handle zurueck. Der Unterschied ist nicht die Form des Rueckgabewerts, sondern die Herkunft des Kindes:
- bei
Process.spawn(...)kommt das Kind von einem externen Kommando - bei
System.fork(...)kommt das Kind aus deinem laufenden Antra-Prozess selbst
Bildlich gedacht#
Du kannst es dir so merken:
Process.spawn(...)ist wie "oeffne ein Walkie-Talkie zu einem fremden Programm".System.fork(...)ist wie "klone mich selbst und gib meinem Klon ebenfalls ein Walkie-Talkie".
Erster Schritt mit Antra::Process#
Nimm zuerst das einfachste Modell:
- du startest ein Programm
- du schreibst Text hinein
- du liest Text wieder heraus
import Antra::Process as Process
import Antra::Result as Result
my process = Process.spawn("perl", ["-e", 'print uc(do { local $/; <STDIN> })']).!
Result.unwrap!(Process.write(process, "hello"))
Result.unwrap!(Process.close_input(process))
println(Process.read_all(process).!)
println(Process.wait(process).!)Was passiert hier?
Process.spawn!startetperl- das kleine Perl-Snippet liest alles von
STDINund macht es gross Result.unwrap!(Process.write(...))schickthellohineinResult.unwrap!(Process.close_input(...))signalisiert: es kommt nichts mehrProcess.read_all!liest die komplette AntwortProcess.wait!liefert den Exit-Code
Die Ausgabe ist:
HELLO
0Warum close_input(...) oft wichtig ist#
Viele Programme warten, bis auf stdin wirklich Schluss ist.
Wenn du nur schreibst, aber nie Result.unwrap!(Process.close_input(process)) aufrufst, denkt das Kind oft:
"Vielleicht kommt gleich noch mehr Input."
Dann blockiert dein Programm scheinbar grundlos.
Merksatz:
- Wenn das Kind den kompletten Input am Stueck lesen soll, schliesse danach fast immer
stdin.
Die naechste Komfortstufe: with_spawn#
Oft willst du gar nicht dauernd selbst daran denken, den Prozess spaeter sauber zu schliessen.
Dafuer gibt es with_spawn(...) und with_spawn!(...).
import Antra::Process as Process
my result = Process.with_spawn!("perl", ["-e", 'print qq[ok]']) |process| -> {
Process.read_all(process).!
}
println(result)Das ist praktisch, wenn du sagen willst:
- starte kurz einen Prozess
- arbeite mit ihm
- raeum ihn danach automatisch auf
Wenn ein Prozess haengen bleibt#
Nicht jeder Kindprozess beendet sich von selbst.
Dann gibt es drei Stufen:
Process.signal(process, :int)fuer ein explizites SignalProcess.terminate(process)fuer das uebliche freundliche Stoppen perTERMProcess.kill(process)fuer den harten Abbruch perKILL
Die gaengigen Signal-Keywords sind in Process.ant direkt sichtbar:
:term:kill:int:hup:quit:usr1:usr2:stop:cont
Ein kleines Beispiel:
import Antra::Process as Process
import Antra::Result as Result
my process = Process.spawn("perl", ["-e", 'select undef, undef, undef, 30;']).!
println(Result.unwrap!(Process.terminate(process)))
println(Process.close(process).!)Wichtig ist der Unterschied:
Process.close(...)schickt selbst kein SignalProcess.close(...)schliesst nur die Streams und wartet dann auf das Ende- wenn der Prozess aktiv beendet werden soll, brauchst du vorher
signal,terminateoderkill
Wann Process.stdin/stdout/stderr sinnvoll sind#
Die High-Level-Funktionen reichen oft schon:
writeread_allread_stderr_all
Manchmal willst du aber direkt mit den einzelnen Streams arbeiten. Dann holst du dir explizit die FileHandles:
import Antra::IO::File as File
import Antra::Process as Process
import Antra::Result as Result
Process.with_spawn!("perl", ["-e", 'print uc(do { local $/; <STDIN> })']) |process| -> {
my stdin = Process.stdin(process).!
my stdout = Process.stdout(process).!
Result.unwrap!(File.write(stdin, "hello"))
Result.unwrap!(File.close(stdin))
println(File.read_all(stdout).!)
}Das brauchst du vor allem dann, wenn du absichtlich mit den Streams als Handles arbeiten willst.
Das erste Gefuehl fuer System.fork(...)#
fork ist nicht dafuer da, irgendein externes Programm zu starten.
Stattdessen sagst du:
- hier ist eine Funktion fuer das Kind
- gib dem Kind einen Eingangs- und Ausgangskanal
- lass Eltern- und Kindprozess miteinander reden
Das kleinste Beispiel sieht so aus:
import Antra::AND as AND
import Antra::Process as Process
import Antra::Result as Result
import Antra::System as System
my child = System.fork(|stdin, stdout| -> {
my msg = System.receive(stdin).!
Result.unwrap!(System.send(stdout, %Reply{ok: true, payload: msg}))
}).!
Result.unwrap!(System.send(child, %User{name: "Ada", tags: [:admin, :active]}))
Result.unwrap!(Process.close_input(child))
println(AND.encode(System.receive(child).!).!)
println(Process.wait(child).!)Wenn du lieber ein vollstaendiges Beispiel aus dem Repo starten willst, sieh dir ../examples/fork_worker.ant an.
Wenn du statt Text oder normaler Antra-Werte rohe Binärdaten transportieren willst, lies danach direkt auch 29-binaere-prozesskanale-und-bytestrings.md.
Wichtig ist hier die Rollenverteilung:
- im Kind ist
stdindas, was vom Elternprozess kommt - im Kind ist
stdoutdas, was zum Elternprozess zurueckgeht - im Elternprozess ist
childder Handle auf dieses Gegenueber
Warum System.send/receive so bequem sind#
System.send(...) und System.receive(...) transportieren nicht nur nackten Text.
Sie benutzen intern AND. Das heisst:
- du kannst Strings schicken
- du kannst Zahlen schicken
- du kannst Arrays und Maps schicken
- du kannst Tagged Values wie
%User{...}oder%Ok[...]schicken
Du musst also fuer normale Antra-Daten nicht selbst AND.encode(...) und AND.decode(...) schreiben.
Das einfachste Denkmodell fuer fork#
Denk nicht zuerst an Betriebssystem-Theorie.
Denk erstmal nur so:
- Der Elternprozess schickt einen Wert.
- Das Kind liest diesen Wert.
- Das Kind baut eine Antwort.
- Der Elternprozess liest diese Antwort.
Wenn du das verstanden hast, ist der Rest nur noch sauberes Schliessen und Warten.
Was Process.close_input(...) bei fork bedeutet#
Auch beim geforkten Kind gilt:
- solange die Eingabeseite offen ist, kann das Kind auf weitere Nachrichten warten
- wenn du sicher bist, dass nichts mehr kommt, kannst du die Eingabe schliessen
Das ist besonders dann wichtig, wenn der Kindcode mehr als nur eine einzelne Nachricht lesen will oder bewusst auf EOF wartet.
Roher Text statt AND#
Manchmal spricht das Gegenueber kein Antra-Datenformat, sondern nur einfache Textzeilen.
Dafuer gibt es:
System.send_text(...)System.receive_text(...)
Beispiel:
import Antra::Process as Process
import Antra::Result as Result
import Antra::System as System
my child = System.fork(|stdin, stdout| -> {
my msg = System.receive_text(stdin).!
Result.unwrap!(System.send_text(stdout, "raw: " <> msg))
}).!
Result.unwrap!(System.send_text(child, "hello raw"))
Result.unwrap!(Process.close_input(child))
println(System.receive_text(child).!)
println(Process.wait(child).!)Das ist der richtige Weg, wenn du absichtlich ein schlichtes Zeilenprotokoll willst.
Wann Process und wann fork?#
Hier ist die wichtigste Entscheidungshilfe:
- Nimm
Process, wenn du ein externes Tool starten willst. - Nimm
fork, wenn dein Kindprozess direkt als Antra-Funktion beschrieben werden soll. - Nimm
System.send/receive, wenn beide Seiten normale Antra-Daten austauschen. - Nimm
System.send_text/receive_text, wenn beide Seiten nur rohe Textzeilen austauschen.
Typische Fehler am Anfang#
1. Process und fork verwechseln#
Falsche Erwartung:
- "Mit
forkstarte ich einfach mal schnellperlodercurl."
Richtig ist:
forkstartet keinen fremden BefehlProcess.spawn(...)startet fremde Befehle
2. Input nicht schliessen#
Wenn der Kindprozess auf das Ende des Inputs wartet, aber der Elternprozess close_input! vergisst, bleibt oft alles haengen.
3. Text und Daten durcheinanderwerfen#
Wenn du System.send(...) benutzt, dann wird ueber AND serialisiert.
Wenn du absichtlich nur rohe Zeilen willst, nimm System.send_text(...).
4. Zu frueh an Handles denken#
Viele Anfaenger greifen sofort zu stdin, stdout und stderr als FileHandle.
Oft ist das gar nicht noetig. Starte lieber zuerst mit:
Process.write(...)Process.read_all(...)System.send(...)System.receive(...)
Die direkte Handle-Ebene kannst du spaeter immer noch dazu nehmen.
Ein kleines Entscheidungsrezept#
Wenn du vor einer neuen Aufgabe sitzt, geh diese Fragen in der Reihenfolge durch:
- Starte ich ein externes Programm oder nur eine zweite Antra-Logik?
- Reicht einfacher Text oder will ich echte Antra-Werte austauschen?
- Muss ich die Streams selbst als Handles anfassen oder reicht die bequemere API?
Fast immer kommst du dann automatisch bei der passenden Funktion an.
Zum Mitnehmen#
Wenn du nur drei Dinge behalten willst, dann diese:
Process.spawn(...)startet externe Programme.System.fork(...)startet einen Antra-Kindprozess.System.send/receivesind fuer Daten,System.send_text/receive_textfuer rohe Textzeilen.
