Programmaufrufe innerhalb von C/C++-Programmen
weiter zu Aufruf von Programmen in Windows1. Aufruf von Programmen in Linux
Die hier zugrunde liegende Problemstellung ist die einer Button-Leiste, mit der Programme gestartet werden. Die Code-Beispiele sind Ausschnitte aus dem Programm silidock.
Das Vorgehen in Kurzform:
- Einen Kind-Prozess erzeugen mit
vfork()oderfork() - Mit
exec()den Kind-Prozess durch das neue Programm ersetzen - Aufräumarbeiten:
- Im Fehlerfall von exec() den Kind-Prozess beenden:
- im Falle von
fork()mitexit(), - im Falle von
vfork()mitkill()oder_exit()(= exit() ohne cleanup).
- im Falle von
- Das Auftreten von Zombies verhindern (Teil 2)
- Fehlerbehandlung (Teil 3)
- Unerwünschte Dateideskriptoren aufräumen (Teil 4)
- Im Fehlerfall von exec() den Kind-Prozess beenden:
fork() und vfork():
Header:<unistd.h>fork() erzeugt einen Kind-Prozess der mit dem Eltern-Prozess identisch ist bis auf die neue PID (process identifier).
vfork() arbeitet wie fork(), dupliziert aber nicht den Prozess im Arbeitsspeicher, ist also performanter. Allerdings
ist der Performance-Unterschied in neueren Unix-Versionen gering. vfork() ist seit 2008
nicht mehr POSIX-kompatibel. Probleme beim gleichzeitigen Zugriff auf geöffneten
Dateien, die im Arbeitsspeicher im Eltern- und Kind-Prozess
doppelt vorliegen, spielen hier keine Rolle, da vfork() hier nur zur Erzeugung eines neuen Prozesses benutzt wird. Der mit
fork() erzeugte Prozess kann im Fehlerfall mit exit() beendet werden, im Falle von vfork()
würde sich dann das ganze Programm beenden, da der Kind-Prozess kein Duplikat des Eltern-Prozesses ist. Der Prozess muss
stattdessen mit _exit() (Unterstrich!) beendet werden. Der Kind-Prozess erbt die Dateideskriptoren des Eltern-Prozesses (siehe Teil 3).
vfork() und fork() hat im Erfolgsfall zwei Rückgabewerte, mit denen Eltern- und
Kind-Prozess unterschieden werden können: An den Eltern-Prozess wird die PID des neuen Kind-Prozesses
zurückgegeben, an das Kind eine 0. Im Fehlerfall wird eine -1 zurückgegeben und der Wert für
errno (number of last error) gesetzt.
Die exec*-Familie: execl, execlp, execle, execv, execvp, execvpe
Header:<unistd.h>: Der laufende Prozess wird mit exec*() vollständig durch einen neuen ersetzt.
execv() übernimmt als erstes Argument den Dateinamen der auszuführenden Datei.
Die Datei muss ausführbar bzw. eine Script-Datei sein, die eine ausführbare Datei aufruft. Weitere Argumente
repräsentieren die optionale Liste von Argumenten für den Programmaufruf. Alle Argumente von execv()
sind C-Strings, die mit '\0' enden müssen. Bei
execl, execlp, und execle werden die weiteren Argumente als
Zeiger auf C-Strings angegeben. Bei
execve und execle können außerdem die Umgebungsvariablen gesetzt werden. Die exec-Funktionen haben im Erfolgsfall keinen Rückgabewert, nur im Fehlerfall wird -1 zurückgegeben und der Wert für
errno (number of last error) gesetzt.
Auruf von Programmen mit execv und fork().
Eltern- und Kind-Prozess können über den Rückgabewert von fork() unterschieden werden:
Kind-Prozess: 0, Eltern-Prozess: PID (process identifier), Fehlerfall: -1.
Mit getpid() (aktueller Prozess) und getppid() (Eltern-Prozess) können
die Prozesskennungen (PIDs) ermittelt werden.
Hier ein vorläufiger Beispiel-Code mit vfork(), ein verkürzter Ausschnitt aus dem Quellcode von
silidock.
silidock ist eine einfache Menüleiste mit Buttons zum Programmaufruf. Der Programmaufruf wird in der Funktion
execute_program vorgenommen. Das Argument "program_call" enthält
Name und Pfad der auszuführenden Datei, "params" oder weitere Argumente bleiben ungenutzt.
// C++ Header
#include <iostreamh>
#include <cerrnoh>
// C-Header
extern "C" {
#include <unistd.h> // fork, execv, exit
}
void execute_program(const char* program_call, const char* param )
{
pid_t child = vfork();
if(child == 0) // executed by child process:
{
int child_pid = getpid();
cout<< program_call << ": child PID: " << child_pid << ", parent PID: " << getppid() << endl;
// replace the child process with exec*-function
char *args[2]; // arguments for exec
args[0] = (char*)program_call; // first argument is program_call
args[1] = (char*)param;
args[2] = NULL; // no arguments...
execv(program_call,args);
// executes if execv failed
_exit(2);
}
else if (child == -1) // error
{
cerr<< "fork to call program failed: " << program_call << endl;
}
else // executed by parent process:
{
cout<< "pid of parent: "<< getpid() << endl;
}
}
Diese etwas gewöhnungsbedürftige if-else-Konstruktion, die gewöhnlich im Sinne von "entweder-oder" funktioniert,
scheint hier auf den erste Blick im Sinne von "sowohl-als-auch" abzulaufen, allerdings liege zwei Prozesse vor.
Deutlich wird das, wenn jeweils die PID ermittelt wird.
Der Code nach dem execv-Aufruf wird nur dann abgearbeitet, wenn execv() fehlschlägt und
das Programm nicht aufgerufen
werden kann. In diesem Fall ist es notwendig, den Kind-Prozess mit _exit() zu beenden.
Im Falle von fork() sähe das Beispiel ganz ähnlich aus. Nur die Fehlerbehandlung
würde über exit() statt über _exit() erfolgen, was ein cleanup
(u.a. Standard-Input/Output schließen) zur Folge hätte. Im Falle von vfork()
würde dieser cleanup das Hauptprogramm beenden, denn hier wurde der Eltern-Prozess nicht dupliziert.
Eine Alternative zu _exit() wäre auch
kill(child_pid, SIGKILL);
da die PID des Kind-Prozesses über getpid() ermittelt werden kann.
Das obige Code-Beispiel würde gut funktionieren und im normalen Gebrauch auf PCs auch keine nennenswerten
Probleme aufwerfen.
Sollte es aber auf einem Rechner laufen, der nicht nach mehr oder weniger kurzer Zeit herunter fährt und
damit alle Programme beendet und wieder neu startet, würden dabei zwei Probleme auftauchen:
Das Code-Beispiel produziert sogenannte Zombies am laufenden Band und die aufgerufenen Programme
besitzen Dateideskriptoren (file descriptors), die sie jeweils vom Elternprozess übernommen haben, aber im hiesigen
Anwendungsfall nicht benötigen.
weiter zu Teil 2: Erste Aufräumarbeiten