Programmaufrufe innerhalb von C/C++-Programmen

weiter zu Aufruf von Programmen in Windows

Aufruf von Programmen in Linux, Teil 3

Aufräumarbeiten: Fehlerbehandlung

zurück zu Aufruf von Programmen in Linux, Teil 2

Zur Erinnerung das Vorgehen in Kurzform:

  1. Einen Kind-Prozess erzeugen mit vfork() oder fork()
  2. Mit exec() den Kind-Prozess durch das neue Programm ersetzen
  3. Aufräumarbeiten:
    • Im Fehlerfall von exec() den Kind-Prozess beenden (Teil 1)
    • Das Auftreten von Zombies verhindern (Teil 2):
      • entweder über signal(SIGCHLD, SIG_IGN)
      • oder über waitpid().
    • Fehlerbehandlung (Teil 3)
    • Unerwünschte Dateideskriptoren (file descriptors) aufräumen (Teil 4)


Fehlerbehandlung

An vielen Stellen des Programms silidock können Fehler auftreten. Bisher wurden Fehler allenfalls durch eine Fehlermeldung per "cerr" ausgegeben. Solche Meldungen sind jedoch nur dazu geeignet, den Quellcode übersichtlicher zu machen. Den User*innen helfen sie in den seltensten Fällen weiter.
Nicht alle Fehler eignen sich für eine Meldung. User*innen sollten die Möglichkeit haben, etwas an dem Fehler zu ändern. Normalerweise beheben sie beispielsweise keine Programmierfehler. Es macht daher nicht viel Sinn, die Beschreibung solcher Fehler in einem Fenster anzuzeigen.
Hilfreiche Fehlermeldungen sind z.B. Zugriffsfehler, die durch sudo-Provilegien behoben werden können, nicht ausführbare Dateien, die durch chmod ausführbar gemacht werden können, falsche Pfadangaben...
Die genauere Eingrenzung des Fehlers ermöglicht im Idealfall seine Behebung. Dazu muss die Fehlermeldung irgendwie angezeigt werden. In Windows gib es dafür die Standard-"MessageBox"; in Linux gibt es nichts vergleichbares. Die silidock ist mit dem Toolkit FLTK programmiert, das praktischerweise auch eine Fehlermeldung beinhaltet (fl_alert() ). Davon wird im Folgenden ausgiebig Gebrauch gemacht.

Fehler eingrenzen durch errno - number of last error

Header: <errno.h>:
Viele System-Funktionen und -Bibliotheken setzten im Fehlerfall einen bestimmten Integerwert, der einem Fehlertyp entspricht, z.B. 13: EACCES Permission denied.
Das ist der "Wert des letzten Fehlers", errno. Taucht ein neuer Fehler auf, wird der Wert überschrieben.

Mit waitpid (siehe Teil 2) lässt sich auch prüfen, ob der Programmaufruf erfolgreich verlaufen ist bzw. der Grund ermitteln, warum er fehlgeschlagen ist. Im Fehlerfall gibt waitpid -1 zurück.
WUNTRACED sorgt dafür, dass auch ein Wert zurückgegeben werden soll, wenn der Kind-Prozess gestoppt hat.
Der durch waitpid verursachte Fehler ist für die meisten User*innen wenig hilfreich (ECHILD: nicht existierende PID oder EINTR: Funktion unterbrochen). Interessanter sind die Fehlerfälle der exec-Funktion, die vor waitpid im Eltern-Prozess unterschieden werden können, wie verweigerter Zugriff oder ungültige Pfadangabe. Daran können User*innen etwas ändern.

Im folgenden Code-Ausschnitt wird eine Fehlermeldung über einen FLTK-Dialog (fl_alert() ) ausgegeben. Es ist sinnvoll, nicht nur eine Fehlermeldung auszugeben, wenn waitpid fehlschlägt, obwohl sie das normalerweise auch tut, wenn beispielsweise der Pfad ungültig ist, sondern mit
else if ( WIFEXITED( child_status ) && WEXITSTATUS( child_status ) != 0)
abzufragen, ob der Kind-Prozess fehlschlägt.
Allerdings hat der Status, der mit WEXITSTATUS(child_status) abgefragt werden kann, keinen weiteren Sinn, deshalb wird darauf verzichtet ihn auszugeben.
Ein Fehlerwert aus waitpid überschreibt den Fehlerwert aus execv, deshalb wird der Fehlerwert von execv zunächst abgespeichert in der Variable child_error bevor waitpid aufgerufen wird.

waitpid gibt manchmal eine 0 zurück, obwohl der Prozess fehlschlägt und eigentlich eine -1 zurückgegeben werden müsste. Wir haben es hier mit zwei Prozessen zu tun, die nicht aufeinander warten. usleep(50) sorgt dafür, dass dem Kind-Prozess ein wenig Zeit gegeben wird, um den richtigen Rückgabewert zu ermitteln.

					
    else // this is executed by parent process
    {
      usleep(50); // give some time to get status of child: 50 microseconds

      // get und store useful errors of exec:
      string child_error = "";
      if ( errno == EACCES)
      {
         child_error = "\n Permission denied or process file not executable.";
      }
      else if ( errno == ENOENT)
      {
         child_error = "\n Invalid path or file.";
      }
      else if ( errno == EPERM)
      {
         child_error = "\n Superuser privileges required.";
      }
      else if ( errno == ENOEXEC)
      {
         child_error = "\n Unsupported format of file.";
      }
      else
      {
         child_error = "\n unexpected error:" + errno;
      }      
      int child_status;
      if ( waitpid(child, &child_status, WNOHANG | WUNTRACED) < 0) // waitpid failed
      {
         string waitpid_message = "Error - Execution failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      }
      else if ( WIFEXITED( child_status )  &&  WEXITSTATUS( child_status ) != 0) // child process failed although waitpid does not
      {
         string waitpid_message = "Error - Process failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      }    	
   }		
    			

Auch die Funktion fork() kann fehlschlagen. Das kann entweder daran liegen, dass bereits zu viele Prozesse laufen und die maximale Anzahl überschritten wird oder zu wenig Speicherplatz zur Verfügung steht.
Entsprechend der Fehlerausgabe im Eltern-Prozess, können auch diese Fehlerfälle über errno genauer bestimmt und ausgegeben werden:

		
    else if (child == -1) // fork error: child < 0
    {
    	string fork_note = "fork failed for program: \n" + string(program_call);
    	string fork_error = "";

        if (errno == EAGAIN)
        {
        	fork_error = "\n To much processes";
        }
        else if (errno == ENOMEM)
        {
        	fork_error = "\n Not enough space available.";
        }
        else 
        {
        	fork_error = "\n "\n Unexpected error: " + errno;
        }        
        string message = fork_note + fork_error;
        fl_alert( message.c_str() );
        Fl::run();

    }    				    			
    				


Der Code-Schnipsel zum Aufruf sieht dann folgendermaßen aus:
(dies ist ein verkürzter Ausschnitt aus dem Quellcode von silidock)


// C++ Header
#include <iostream.h>
#include <cerrno.h>
// C-Header
extern "C" {
   #include <unistd.h> // fork, execv, getpid  
   #include <sys/wait.h> // for signal, waitpid
   #include <errno.h> // fork, execv, getpid  
}

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();
			
        // 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) // fork error: child < 0
    {
    	string fork_note = "fork failed for program: \n" + string(program_call);
    	string fork_error = "";

        if (errno == EAGAIN)
        {
        	fork_error = "\n To much processes";
        }
        else if (errno == ENOMEM)
        {
        	fork_error = "\n Not enough space available.";
        }
        else 
        {
        	fork_error = "\n "\n Unexpected error: " + errno;
        }        
        string message = fork_note + fork_error;
        fl_alert( message.c_str() );
        Fl::run();
    }
    else // this is executed by parent process
    {
      usleep(50); // give some time to get status of child: 50 microseconds

      // get und store useful errors of exec:
      string child_error = "";
      if ( errno == EACCES)
      {
         child_error = "\n Permission denied or process file not executable.";
      }
      else if ( errno == ENOENT)
      {
         child_error = "\n Invalid path or file.";
      }
      else if ( errno == EPERM)
      {
         child_error = "\n Superuser privileges required.";
      }
      else if ( errno == ENOEXEC)
      {
         child_error = "\n Unsupported format of file.";
      }
      else
      {
         child_error = "\n unexpected error:" + errno;
      }      
      int child_status;
      if ( waitpid(child, &child_status, WNOHANG | WUNTRACED) < 0) // waitpid failed
      {
         string waitpid_message = "Error - Execution failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      }
      else if ( WIFEXITED( child_status )  &&  WEXITSTATUS( child_status ) != 0) // child process failed although waitpid does not
      {
         string waitpid_message = "Error - Process failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      } 
        // prevent zombies: 
        pid_t p;
        // Reap all pending child processes
        do {
            p = waitpid(-1, NULL, WNOHANG);
        } while (p != (pid_t)0 && p != (pid_t)-1);
    }
}    			
    			

weiter zu Teil 4: Dateideskriptoren