Fork und Prozesse

Fork

Baum als Beispiel für Forking Schon lange vor der Biologie hat sich die Informatik mit dem Klonen beschäftigt. Aber man nannte es nicht klonen sondern forken.
fork bedeutet in Englisch Gabel, Gabelung, Verzweigung und als Verb gabeln, aufspalten und verzweigen. Aber es bedeutet auch eine Aufspaltung oder als Verb aufspalten. Im letzeren Sinn wird es bei Betriebssystemen, vor allem bei Unix und Linux verwendet. Prozesse werden mittels des Kommandos "fork" aufgespalten - geklont würde man heute wohl eher sagen - und führen dann ein eigenständiges "Leben".

In der Informatik steht der Begriff Fork zur Bezeichnung verschiedener Sachverhalte:

Fork in Python

Beim Systemaufruf fork() erzeugt der aktuelle Prozess eine Kopie von sich selbst, welche dann als Kindprozess des erzeugenden Programmes läuft. Der Kindprozess übernimmt die Daten und den Code vom Elternprozess und erhält vom Betriebssystem eine eigene Prozessnummer, die PID (engl. "Process IDentifier"). Der Kindprozess läuft als eigenständige Instanz des Programms, unabhängig vom Elternprozess. Am Rückgabewert von fork() erkennt man, in welchem Prozess man sich befindet. 0 kennzeichnet den Kindprozess und ein positiver Rückgabewert steht für den Elternprozess. Im Fehlerfall liefert fork() einen Wert kleiner 0 und kein Kindprozess wird erzeugt.

Um Prozesse forken zu können, müssen wir das Modul os in Python importieren.

Das folgende Beispiel-Skript zeigt einen Eltern-Prozess (Parent), der sich beliebig oft forken kann, solange man als Benutzer des Skriptes kein q bei der Eingabeaufforderung eingibt. Sowohl der Kindprozess als auch der Elternprozess machen nach dem fork mit der if-Anweisung weiter. Im Elternprozess hat newpid einen von 0 verschiedenen Wert, während newpid im Kindprozess den Wert 0 hat, so dass im Kindprozess die Funktion child() aufgerufen wird. Die exit-Anweisung os.exit(0) in der child-Funktion ist notwendig, da sonst der Kindprozess in den Elternprozess zurückkehren würde und zwar zum raw_input().
import os

def child():
   print('\nA new child ',  os.getpid())
   os._exit(0)  

def parent():
   while True:
      newpid = os.fork()
      if newpid == 0:
         child()
      else:
         pids = (os.getpid(), newpid)
         print("parent: %d, child: %d\n" % pids)
      reply = input("q for quit / c for new fork")
      if reply == 'c': 
          continue
      else:
          break

parent()

Unabhängige Programme durch fork() starten

Bisher haben die Kindprozesse in den Beispielen eine Funktion innerhalb des Skriptes selbst aufgerufen und haben sich dann beendet.

Forks werden aber häufig genutzt, um unabhängig laufende Programme zu starten. Dazu gibt es im Modul os die exec*()-Funktionen.

Sie führen ein neues Programm aus, indem sie den aktuellen Prozess damit ersetzen. Sie kehren nicht in das aufrufende Programm zurück. Sie erhalten unter Unix/Linux sogar die gleich Prozess-ID, als das aufrufende Programm.

Die exec*()-Funktionen

Die exec*()-Funktionen gibt es in verschiedenen Variationen: Wie werden die Funktionen im folgenden mit vielen Beispielen erklären, da sie bisher nur sehr spärlich in Büchern und im Internet beschrieben sind.

Für unsere Beispiele wollen wir das folgende Bash-Shell-Skript nutzen, dass wir unter test.sh abspeichern. Wir haben es im Verzeichnis /home/bernd/bin2 abgespeichert. Zum Verständnis der folgenden Beispiele ist es lediglich wichtig, dass test.sh nicht in einem Verzeichnis liegt, das sich in $PATH befindet. Außerdem sollte test.sh ausführbar sein. Also folgenden Shell-Befehl ausführen:
chmod 755 test.sh
#!/bin/bash

script_name=$0
arg1=$1
current=`pwd`
echo $script_name, $arg1
echo "XYZ: "$XYZ
echo "PATH: "$PATH
echo "current directory: $current"
In einem anderen Verzeichnis, z.B. /home/bernd/python, haben wir ein Python-Skript execvp.py, dass dieses Bash-Skript aufruft:
#!/usr/bin/python
import os
args = ("test","abc")
os.execvp("test.sh", args)
Da test.sh nicht in $PATH ist, gibt es eine Fehlermeldung, wenn man execvp in der Kommandozeile aufruft:
$ ./execvp.py
Traceback (most recent call last):
  File "./execvp.py", line 6, in <module>
    os.execvp("test.sh", args)
  File "/usr/lib/python2.6/os.py", line 344, in execvp
    _execvpe(file, args)
  File "/usr/lib/python2.6/os.py", line 380, in _execvpe
    func(fullname, *argrest)
OSError: [Errno 2] No such file or directory
Damit diese Fehlermeldung nicht erfolgt und unser Shell-Skript gefunden werden kann, erweitern wir die PATH-Umgebungsvariable um das Verzeichnis, in dem test.sh steht, also in unserem Fall /home/bernd/bin2:
$ PATH=$PATH:/home/bernd/bin2
$ ./execvp.py
/home/bernd/bin2/test.sh, abc
XYZ: 
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/bin:/home/bernd/bin:/home/bernd/bin2
current directory: /home/bernd/python
Eine andere elegante Möglichkeit den Pfad zu erweitern bietet die execvpe()-Funktion. Ihr kann man als drittes Argument ein Dictionary mit Umgebungsvariablen mitgeben. Die Umgebungsvariablen werden durch die Werte in diesem Dictionary ersetzt:
import os
env =  {"PATH":"/home/bernd/bin2", "XYZ":"BlaBla"}
args = ("test","abc")
os.execvpe("test.sh", args, env)
Speichern wir obiges Skript in execvpe.py, erhalten wir auf der Shell beim Start folgende Ergebnisse:
$ ./execvpe.py
/home/bernd/bin2/test.sh, abc
XYZ: BlaBla
PATH: /home/bernd/bin2/
current directory: /home/bernd/python/
$
Im vorigen Beispiel wird der Wert der Shell-Umgebungsvariablen $PATH durch den neuen Wert im Dictionary überschrieben. Will man das vermeiden, d.h. das neue Verzeichnis nur anhängen, muss man den Code wie folgt ändern:
import os

path = os.environ["PATH"] + ":/home/bernd/bin2/" 
env =  {"PATH":path, "XYZ":"BlaBla"}
os.execlpe("test.sh", "test","abc", env)
Will man execlpe() statt execvpe() benutzen, so muss man das Python-Skript wie folgt ändern:
import os
env =  {"PATH":"/home/bernd/bin2/", "XYZ":"BlaBla"}
os.execlpe("test.sh", "test","abc", env)

Übersichtsbild der exec-Funktionen

Systematik der exec-Funktionen