Verständnisfrage os.fork() und os.pipe()

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

Hallo,

kann mir bitte jemand erklären, warum im else_Zweig von parent() pipe_write geschlossen werden muss.

Code: Alles auswählen

import os

def child(pipe_write):
    for i in range(2):
        names = "Baum, Haus, Kuchen\n"
        os.write(pipe_write, bytes(names, "utf-8"))

def parent():
    pipe_read, pipe_write = os.pipe()
    if os.fork() == 0:
        os.close(pipe_read)
        child(pipe_write)
    else:
        os.close(pipe_write)
        counter = 1
        pipe_read = os.fdopen(pipe_read)
        for i in range(3):
            value = pipe_read.readline()
            print(f'{counter}: {value if value else None}', end="")
            counter += 1
        print()
        pipe_read.close()

parent()
Kommentiere ich die Zeile

Code: Alles auswählen

        os.close(pipe_write)
aus, komme ich in eine Endlosschleife (Weiß nicht ob das wirklich eine Endlosschleife ist?) und muss das Programm mit strg+c abbrechen. Kann mir bitte jemand erklären, warum das so ist, weil im else-Zweig wird doch pipe_write garnicht verwendet?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hier https://unix.stackexchange.com/question ... -in-a-pipe findet sich die Antwort, in der zweiten Antwort:

"""
Having the child close its copy of the write end (which it's not going to use), makes it possible for the child to detect when the parent does so. If the child kept the write end open, it'd never see an EOF on the pipe, since it would essentially be waiting for itself. (2)

Similarly, having the parent close its copy of the read end also lets the parent detect if the child goes away. (1)
"""

Das ist auch keine Schleife. Das Programm steht einfach in dem readline. Weil es ja noch ein drittes mal lesen will, du aber nur zweimal schreibst. Wenn du in child den Wert 2 auf 5 erhoehst, geht das auch.

counter statt i ist ein bisschen komisch, warum zweimal zaehlen?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@_Mala_Fide_: Was die Namen angeht ist es unschön bis falsch in der gleichen Funktion `pipe_read` mal für einen Dateideskriptor, also eine Zahl, und mal für ein komplettes Dateiobjekt zu verwenden.

Der Kindprozess schliesst das schreibende Ende der Pipe nicht explizit.

Du kodierst im Kindprozess die Daten als UTF-8 aber beim `os.fdopen()` wird keine Kodierung angegeben. Das kann funktionieren, muss es aber nicht. Wenn man Textdateien öffnet sollte man immer die Kodierung angeben.

Die `bytes()`-Funktion zum Kodieren zu verwenden ist eher ungewöhnlich. Die `encode()`-Methode auf der Zeichenkette wird da normalerweise für genommen. Man muss diese Kodierung der immer gleichen Daten auch nicht bei jedem Schleifendurchlauf machen.

Wo es möglich ist, sollte man Dateien mit ``with`` verwenden, damit die auch geschlossen werden wenn beispielsweise eine Ausnahme auftritt.

Statt `range()` oder manuell mit ``+= 1`` einen Zähler zu führen könnte man auch einfach über die Datei iterieren und den Zähler mit `enumerate()` generieren.

Code: Alles auswählen

#!/usr/bin/env python3
import os


def child(output_fd):
    names = "Baum, Haus, Kuchen\n".encode("utf-8")
    for _ in range(2):
        os.write(output_fd, names)


def parent():
    input_fd, output_fd = os.pipe()
    if os.fork() == 0:
        try:
            os.close(input_fd)
            child(output_fd)
        finally:
            os.close(output_fd)
    else:
        os.close(output_fd)
        with os.fdopen(input_fd, encoding="utf-8") as read_file:
            for i, line in enumerate(read_file, 1):
                print(f"{i}: {line}", end="")
        print()


if __name__ == "__main__":
    parent()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

@__deets__
Also heißt das, dass im child-Prozess der else-Zweig von parent() ausgeführt wird?
Verstehe den ersten englischen Satz nicht ganz. Warum steht da, dass das Kind den Schreibenden Zugriff niemals benutzen wird? Es ist doch das Kind, welches schreibt und das Elternteil, welches ließt.

Das mit Counter ist Blödsinn, sehe ich gerade selber. Wollte eigentlich damit zählen, wieviele Forks erstellt wurden. Aber es wird ja bloß einer erstellt... Ich hatte vorher das Thema Threads in meinem Tutorial, wo so eine Zählervariable verwendet wurde um die Threads zu zählen. Habe ich komplett falsch gedacht, ändere ich gleich ab.

@__blackjack__
Danke, die Tips werde ich mir merken.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, das heisst nicht, dass das child im else ausgefuehrt wird. Das ist schon das if. Aber deine Kommunikation baut darauf, dass jemand die pipe schliesst. Und wenn das parent sein Ende zum schreiben (dass es nicht benutzt) nicht schliesst, dann haben zwei Prozesse die Pipe offen. Und damit ist es egal, dass das Kind seinen Teil schliesst, der Parent haengt weiter im read(line) fest. Weil das sigpipe nicht kommt.
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

@__deets__
Also heißt das, dass der Prozess, der einen pipe-Teil (lesen bzw. schreiben) nicht benötigt, ihn auch schließen sollte, so dass am Ende beide pipe-Teile in jedem Prozess geschlossen sind?

@__blackjack__
Ich habe jetzt an deinen Code im else-Zweig mal noch os.close(input_fd) gehangen, weil ich input_fd noch explizit schließen wollte, damit in jedem Prozess beide pipe-Teile geschlossen sind.

Code: Alles auswählen

import os

def child(output_fd):
    names = "Baum, Haus, Kuchen\n".encode("utf-8")
    for _ in range(2):
        os.write(output_fd, names)

def parent():
    input_fd, output_fd = os.pipe()
    if os.fork() == 0:
        try:
            os.close(input_fd)
            child(output_fd)
        finally:
            os.close(output_fd)
    else:
        os.close(output_fd)
        with os.fdopen(input_fd, encoding="utf-8") as read_file:
            for i, line in enumerate(read_file, 1):
                print(f"{i}: {line}", end="")
        os.close(input_fd)
        
if __name__ == "__main__":
    parent()
So bekomme ich aber beim Ausführen folgenden Fehler:

Code: Alles auswählen

OSError: [Errno 9] Bad file descriptor
Ich interpretiere den Fehler so, dass der Datei-Deskriptor bereits geschlossen ist und er nicht geschlossen werden kann. Wann wird der Datei-Deskriptor geschlossen? Wird er mit dem Datei-Objekt gemeinsam geschlossen?
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

Oder sollten in jedem Prozess immer alle pipe-Teile geschlossen werden, sprich in child in und out und in parent in und out?
So würde ja jeder Teil doppelt geschlossen werden... Obwohl ja jeder Prozess beide pipe-Teile geöffnet haben sollte, also ist das schließen beider Teile in jedem Prozess doch sinnvoll?
Ein Wenig stehe ich noch auf dem Schlauch...
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@_Mala_Fide_: Genau, der wird mit dem Schliessen des Dateiobjekts bereits geschlossen. Das könnte man verhindern wenn man beim `os.fdopen()` das Argument ``closefd=False`` übergeben würde. Aber für gewöhnlich will man das ja, dass die Datei bei `close()` auch tatsächlich geschlossen wird.

Und man sollte nach Möglichkeit immer explizit aufräumen, also alle Dateien schliessen, entweder die Objekte oder wenn man auf einer tieferen Ebene operiert `os.close()` mit den Dateideskriptoren aufrufen.

Edit: Und eigentlich ist das *sehr* selten das man so etwas überhaupt machen will oder muss, also `os.fork()`, `os.pipe()`, `os.close()`, und Dateideskriptoren. Man würde da eher mit höheren Abstraktionsschichten arbeiten wie `multiprocessing` oder `concurrent.futures`.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

_Mala_Fide_ hat geschrieben: Dienstag 10. Mai 2022, 11:58 @__deets__
Also heißt das, dass der Prozess, der einen pipe-Teil (lesen bzw. schreiben) nicht benötigt, ihn auch schließen sollte, so dass am Ende beide pipe-Teile in jedem Prozess geschlossen sind?
Wenn man sich darauf verlaesst, dass das schliessen von der anderen Seite als Signal wahrgenommen werden soll, dann ja klar. Du kannst das natuerlich auch anders loesen, zB ueber ein Protokoll. Aber tendentiell ist es besser, denn wenn die Gegenseite abschmiert, dann bleibt man sonst uU einfach haengen. Darum sollte man das immer machen, ausser man hat einen speziell guten Grund. Mir faellt gerade keiner ein.
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

@__blackjack__
Ich denke zum Verständnis ist es schon nicht verkehrt, sich dort auch einzuarbeiten. Das Kapitel vom Tutorial war auch schon sehr alt (Der Großteil des Tutorials ist in python3, bis jetzt war nur in 3 Kapiteln python2 Code.), musste den kompletten Code von python2 in python3 umschreiben. Warscheinlich lag es daran, dass er dort mit os gearbeitet hat.

@__deets__
Kannst Du mir bitte noch erklären inwiefern bei meinem Code child erwartet, dass pipe_write in parent geschlossen wird?

Ist es denn so, dass wenn pipe_write in parent nicht geschlossen werden würde, readline ins Leere läuft, weil es versucht, nachdem von child nichts mehr in pipe_write geschrieben wird, von pipe_write von parent zu lesen?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das habe ich schon mehrfach versucht zu sagen, ja. Dein readline im parent steht da, und versucht, von einer geoffneten Pipe was zu lesen. Wenn die zweimal geoeffnet ist, dann reicht einmal schliessen nicht aus. Darum muss der parent selbst immer die Pipe schliessen, damit danach das schliessen von Seiten des Kindes bemerkt werden kann. Du siehst doch auch, wenn du das Programm mit Ctrl-C abbrichst, dass er im readline steht.
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

Ich denke jetzt habe ich es verstanden...
Ich danke euch, für die schnelle Hilfe!
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

So was ähnliches mal mit `multiprocessing`. Da braucht man ein Datum welches das Ende der Nachrichten kennzeichnet, ich habe hier `None` verwendet. Es ist etwas weniger Code und es funktioniert im Gegensatz zu `os.fork()` auch unter Windows.

Code: Alles auswählen

#!/usr/bin/env python3
from multiprocessing import Process, SimpleQueue


def child(queue):
    for _ in range(2):
        queue.put("Baum, Haus, Kuchen\n")
    queue.put(None)


def parent():
    queue = SimpleQueue()
    process = Process(target=child, args=[queue], daemon=True)
    process.start()
    for line in iter(queue.get, None):
        print(line, end="")
    process.join()


if __name__ == "__main__":
    parent()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
_Mala_Fide_
User
Beiträge: 53
Registriert: Dienstag 22. Dezember 2015, 19:17

Das ist auch auf Anhieb leichter zu verstehen, als mit os.fork(), obwohl ich multiprocessing noch nie verwendet habe.
Danke für das Beispiel.
Antworten