Mailtransfer mit Pipes und Childs

Martin Kompf

Anhand eines C-Programmes zum Mailversand wird erklärt, wie mittels der UNIX-Systemaufrufe pipe(), fork() und exec() ein externes Programm gestartet und an dieses Daten über eine Pipe geschickt werden kann.

Die Idee für diesen Artikel entstammt einem Diskussionsforum. Aufgabe ist es, möglichst einfach eine Mail aus einem C-Programm heraus zu versenden. Ehe man sich hier in den Tiefen der Socketprogrammierung und des Simple Mail Transfer Protocol (SMTP) verliert (wer dies trotzdem tun will: Links zum Thema stehen am Ende dieses Artikels), ist die Verwendung eines externen Programms, wie mail in Erwägung zu ziehen. Dieses einfache Mail-Frontend dürfte auf fast allen UNIX-Systemen zur Standardausrüstung gehören. Das folgende Beispiel ist daher so nur auf UNIX-Rechnern nachzuvollziehen (der Autor testete unter Linux); auch gibt es unter Windows den Systemaufruf fork() nicht, es sei denn, man verwendet hier die Cygwin Enwicklungsumgebung.

Die naheliegendste Idee, das externe Programm per system("mail") aufzurufen, scheitert daran, dass mail den Text der Nachricht auf seiner Standardeingabe verlangt. Die Funktion system() jedoch startet das als Argument angegebene Programm in einer neuen Shell. Eine Kommunikation mit dem aufrufenden Prozess ist dann so einfach nicht mehr möglich. Besser ist es daher, mail in einem vom aktuellen Prozess erzeugten Kindprozess (Childprocess) zu starten. Dann kann die Kommunikation der beiden Prozesse durch eine an das Kind übergebene Pipe erfolgen.

Eine Pipe kann man sich als eine Leitung vorstellen, in die an einem Ende Daten geschrieben werden können, welche dann unverändert in der selben Reihenfolge am anderen Ende herauslesbar sind. Die Pipe wird durch den Systemaufruf pipe(fd) angelegt. fd ist ein Array aus zwei Integerwerten, welches nach dem Aufruf die Filedeskriptoren der beiden Enden der Pipe enthält:
Schematische Darstelung einer Pipe

In C-Code gegossen sieht das ganze nun so aus:
Zunächst werden die notwendigen Headerfiles inkludiert und die main() Funktion begonnen:

#include <unistd.h>
#include <stdio.h>

int main( int argc, char** argv)
{

Drei Variablen werden in unserem Beispiel mit der Adresse des Mailempfängers, dem Betreff und dem Nachrichtentext initialisiert:

    char addr[] = "cplus@meome.de";
    char subject[] = "Murphy's Gesetz";
    char text[] = "Ausnahmen sind grundsätzlich zahlreicher als Regeln.\n";

Dann wird die Pipe angelegt:

    int fds[2];

    pipe(fds);

Nun wird der Kindprozesses erzeugt. Dazu dient die Systemfunktion fork(). Diese erzeugt eine identische Kopie des zur Zeit laufenden Prozesses. Dieser Kindprozess enthält also insbesondere auch eine Kopie von fds, in der die Deskiptoren der angelegten Pipe hinterlegt sind. Der einzige Unterschied zwischen Eltern- und Kindprozess ist, dass fork() im Kindprozess den Wert 0 zurückliefert, während der Elternprozess dort einen Wert größer 0 (genauer gesagt die Prozess-ID des Kindes) oder kleiner 0 bei Fehler zurückgeliefert bekommt. Nach der bedingten if-Anweisung

    if (0 == fork()) {

befinden wir uns also im Programmteil, der nur für das Kind ausgeführt wird. Bevor man jetzt mail startet, wird die Standardeingabe des Kindes mit der lesbaren Seite der Pipe (also fds[0]) verbunden. Dazu dient die Systemfunktion dup2(). Der Wert 0 steht dabei für den Filedeskriptor der Standardeingabe. Das andere Ende der Pipe wird im Kind nicht benötigt und daher geschlossen:

        /* child */
        dup2( fds[0], 0);
        close( fds[1]);

Jetzt kann das externe Programm mail gestartet werden. Dies erfolgt unter Verwendung des Systemaufrufs execlp(). Als Parameter werden die Option -s mit dem Betreff (Subject) und die Empfängeradresse übergeben. Da ein per execlp gestarteter Prozess alle offenen Filedeskriptoren des aufrufenden Prozesses bekommt, ist als Folge die Standardeingabe von mail mit der lesbaren Seite unserer Pipe verbunden. Die Funktion execlp() kehrt normalerweise nie zum Aufrufer zurück, es sei denn, bei ihrer Ausführung ist ein Fehler aufgetreten, beispielsweise weil kein ausführbares Programm mail gefunden wurde:

        execlp("mail", "mail", "-s", subject, addr, 0);
        fprintf( stderr, "execlp mail failed\n");
        exit(1);
    }

Damit ist der Programmteil des Kindes beendet, der verbleibende Code läuft im Elternprozess ab. Obwohl dieser rein optisch gesehen hinter dem Code des Kindes steht, werden in Wirklichkeit beide Prozesse parallel und asynchron nebeneinander ausgeführt!

Hier wird zunächst das nicht benötigte Ende der Pipe geschlossen. In das schreibbare Ende der Pipe (fds[1]) kann nun der komplette Text der Mail hineingeschrieben werden.

    /* parent */
    close( fds[0]);

    write( fds[1], text, strlen(text));

Der Text wird dabei durch die Pipe »hindurchgeschleust« und tritt am anderen Ende wieder ans Tageslicht. Dieses andere Ende (fds[0]) haben wir aber im Kindprozess gerade mit der Standardeingabe des externen Programms mail verbunden! Das heisst, unser Nachrichtentext gelangt direkt auf die Standardeingabe von mail - genau das von uns bezweckte Verhalten.

Zu guter Letzt schliessen wir die Pipe. Dies wird mail veranlassen, die Nachricht abzusenden und sich danach zu beenden. Um Prozessleichen (sogenannte »Zombies«) zu vermeiden, sollte der Elternprozess auf das Terminieren des Kindes warten. Dazu dient der Systemaufruf wait():

    close( fds[1]);
    wait(0);
}

Dieses kurze und auf den ersten Blick unspektakulär aussehende Programm führt auf weniger als dreißig Zeilen die mächtigsten Systemfunktionen eines UNIX-Betriebssystems vor: Das Erzeugen neuer Prozesse mit fork(), die Programmausführung mit exec(), die Prozesskontrolle mit wait() und exit() sowie die Verwendung von Pipes zur Kommunikation zwischen Prozessen. Das ist letzlich genau derselbe Vorgang, wie er bei der Eingabe des Namens eines ausführbaren Programms und anschließendem Betätigen der »Enter«-Taste am Prompt einer UNIX-Shell durch diese tagtäglich absolviert wird.