Pipes in Python

Pipe

Tunnel und Pipe Die Pipe (engl. für Rohr, Röhre) bezeichnet einen gepufferten uni- oder bidirektionalen Datenstrom zwischen zwei Prozessen nach dem "FIFO" (First In - First Out)- Prinzip. Das bedeutet, dass die Ausgabe eines Prozesses als Eingabe für einen weiteren verwendet wird. Pipes wurden 1973 in Unix eingeführt.

Generell unterscheidet man zwei Sorten von Pipes:

Anonyme Pipes existieren nur innerhalb von Prozessen und werden typischerweise in Verbindung mit forks benutzt.

Bier-Pipe in Python

99 Bottles of Beer "99 Bottles of Beer" ist ein bekanntes und beliebtes Lied in den USA und Kanada. Es wird häufig bei gemeinsam unternommenen Ausflügen und Feiern gesungen. Obwohl der Song aus 100 verschiedenen Strophen besteht, stellt er keine besonderen Anforderungen an die Gedächtnisleistung der Sängerinnen und Sänger:

Ninety-nine bottles of beer on the wall, Ninety-nine bottles of beer. Take one down, pass it around, Ninety-eight bottles of beer on the wall.

Dann geht es mit 98 Flaschen entsprechend weiter bis alle Flaschen "aufgebraucht" sind. Was tun, wenn kein Bier mehr da ist? Klar, neues kaufen gehen:

No more bottles of beer on the wall, no more bottles of beer. Go to the store and buy some more, Ninety-nine bottles of beer on the wall.

Dieses Lied hat auch in der Programmierung eine besondere Bedeutung erlangt. Dieser Song wurde mittlerweile in allen bekannten Programmiersprachen implementiert.
Wir programmieren die Aleph-Null-Variante, d.h. mit der "no more bottles"-Strophe, mit Forking und Pipe:
import os

def child(pipeout):
  bottles = 99
  while True:
    bob = "bottles of beer"
    otw = "on the wall"
    take1 = "Take one down and pass it around"
    store = "Go to the store and buy some more"

    if bottles > 0:
      values =  (bottles, bob, otw, bottles, bob, take1, bottles - 1,bob,otw)
      verse = "%2d %s %s,\n%2d %s.\n%s,\n%2d %s %s." % values
      os.write(pipeout, verse)
      bottles -= 1
    else:
      bottles = 99
      values =  (bob, otw, bob, store, bottles, bob,otw)
      verse = "No more %s %s,\nno more %s.\n%s,\n%2d %s %s." % values
      os.write(pipeout, verse)
      
def parent():
    pipein, pipeout = os.pipe()
    if os.fork() == 0:
        child(pipeout)
    else:
        counter = 1
        while True:
            if counter % 100:
                verse = os.read(pipein, 117)
            else:
                verse = os.read(pipein, 128)
            print 'verse %d\n%s\n' % (counter, verse)
            counter += 1

parent()
Das Problem im obigen Beispiel besteht darin, dass wir genau wissen müssen, wieviele Bytes wir jeweils vom Child-Prozess über die Pipe erwarten. Für die ersten 99 Strophen sind es jeweils 117 Bytes (verse = os.read(pipein, 117)) und für die Strophe ohne Flaschen 128 Bytes (verse = os.read(pipein, 128)).

In der folgenden Implementierung lesen wir jeweils ganze Zeilen aus der Pipe des Kind-Prozesses:
import os

def child(pipeout):
  bottles = 99
  while True:
    bob = "bottles of beer"
    otw = "on the wall"
    take1 = "Take one down and pass it around"
    store = "Go to the store and buy some more"

    if bottles > 0:
      values =  (bottles, bob, otw, bottles, bob, take1, bottles - 1,bob,otw)
      verse = "%2d %s %s,\n%2d %s.\n%s,\n%2d %s %s.\n" % values
      os.write(pipeout, verse)
      bottles -= 1
    else:
      bottles = 99
      values =  (bob, otw, bob, store, bottles, bob,otw)
      verse = "No more %s %s,\nno more %s.\n%s,\n%2d %s %s.\n" % values
      os.write(pipeout, verse)
def parent():
    pipein, pipeout = os.pipe()
    if os.fork() == 0:
        os.close(pipein)
        child(pipeout)
    else:
        os.close(pipeout)
        counter = 1
        pipein = os.fdopen(pipein)
        while True:
            print 'verse %d' % (counter)
            for i in range(4):
                verse = pipein.readline()[:-1]
                print '%s' % (verse)
            counter += 1
            print

parent()

Bidirektionale Pipes

Wir wollen die Implementierung von bidriektionalen Pipes anhand eines Spieles demonstrieren. Dabei handelt es sich um ein einfaches Zahlenratespiels, wie es von kleinen Kindern gerne gespielt wird. Dieses Spiel hatten wir bereits im Kapitel über Schleifen in unserem Python-Kurs verwendet.

Spiel mit bidirektionaler Pipe implementiert

Das folgende Diagram veranschaulicht sowohl die Regeln des Spiels, als auch die Implementierung in unserem Python-Skript. Ein Spieler, in unserem Fall der Eltern-Prozess mit der Funktion "deviser", denkt sich eine Zahl im Bereich 0 bis 100. Der andere Spieler, bei uns der Kindprozess mit der Funktion "guesser", versucht die Zahl zu erraten. Der Elternprozess gibt eine 0 zurück, wenn die Zahl erraten wurde, eine 1, falls die geratene Zahl größer als die zu erratende Zahl ist und ein -1, falls sie kleiner als die zu erratende Zahl ist. Sowohl der Elternprozess als auch der Kindprozess protokollieren die Zwischenergebnisse, d.h. die Zahlen, die geraten werden. deviser schreibt in deviser.log und guesser schreibt in guesser.log

Im folgenden haben wir eine Implementierung des gesamten Skriptes angegeben:
import os, sys, random


def deviser(max):
    fh = open("devisor.log","w")
    to_be_guessed = int(max * random.random()) + 1
    
    guess = 0
    while guess != to_be_guessed:
        guess = int(raw_input())
        fh.write(str(guess) + " ")
        if guess > 0:
            if guess > to_be_guessed:
                print 1
            elif guess < to_be_guessed:
                print -1
            else:
                print 0
            sys.stdout.flush()       
        else:
            break
    fh.close()

def guesser(max):
    fh = open("guesser.log","w")
    bottom = 0
    top = max
    fuzzy = 10
    res = 1
    while res != 0:
        guess = (bottom + top) / 2
        print guess
        sys.stdout.flush()       
        fh.write(str(guess) + " ")
        res = int(raw_input())
        if res == -1: # number is higher
            bottom = guess
        elif res == 1:
            top = guess
        elif res == 0:
            message = "Wanted number is %d" % guess
            fh.write(message)
        else: # this case shouldn't occur
            print "input not correct"
            fh.write("Something's wrong")
   

n = 100
stdin  = sys.stdin.fileno() # usually 0
stdout = sys.stdout.fileno() # usually 1

parentStdin, childStdout  = os.pipe() 
childStdin,  parentStdout = os.pipe() 
pid = os.fork()
if pid:
    # parent process
    os.close(childStdout)
    os.close(childStdin)
    os.dup2(parentStdin,  stdin)
    os.dup2(parentStdout, stdout)
    deviser(n)
else:
    # child process
    os.close(parentStdin)
    os.close(parentStdout)
    os.dup2(childStdin,  stdin)
    os.dup2(childStdout, stdout)
    guesser(n)

Benamte Pipes, Fifos

Beispiel: Benamte Pipe in die drei Prozesse schreiben
 und aus der einer liest. Unter Linux ebenso wie unter Unix ist es möglich Pipes anzulegen, die als Dateien eingerichtet sind.

Diese Pipes werden als benamte Pipes (englisch: named Pipes) oder manchmal auch als Fifos (First In First Out) bezeichnet.

Ein Prozess liest und schreibt von einer solchen Pipe wie bei einer normalen Datei. Oft schreiben mehrere Prozesse in eine Pipe und nur ein Prozess liest daraus.

Das folgende Beispiel illustriert den Fall, dass ein Prozess in die Pipe schreibt, der Kindprozess, und ein anderer Prozess, der Elternprozess, aus der Pipe liest.
import os, time, sys
pipe_name = 'pipe_test'

def child( ):
    pipeout = os.open(pipe_name, os.O_WRONLY)
    counter = 0
    while True:
        time.sleep(1)
        os.write(pipeout, 'Number %03d\n' % counter)
        counter = (counter+1) % 5

def parent( ):
    pipein = open(pipe_name, 'r')
    while True:
        line = pipein.readline()[:-1]
        print 'Parent %d got "%s" at %s' % (os.getpid(), line, time.time( ))

if not os.path.exists(pipe_name):
    os.mkfifo(pipe_name)  
pid = os.fork()    
if pid != 0:
    parent()
else:       
    child()