/* Programm vom 30May22 (T03) */ #include #include #include #include #include #include /******************************************************/ /* BONUS: Eine Beispielimplementierung von execvp: */ /******************************************************/ int _execvp(const char *file, char *const argv[]) { /* Der unterschied zwischen execv/execl und execvp/execlp, ist * dass man letzteren beim Aufruf lediglich den Namen des * Programms geben muss, und keinen Pfad zum Programm. Diese * Funktionalität wird von der Shell benutzt um gcc anstatt * /usr/bin/gcc schreiben zu müssen. * * Für Prozesse sind "Umgebungsvariablen" definieren, die weiter * geerbt werden an Kindprozesse. Man kann in einer Shell * Session alle Variablen mit dem `EDV' Befehl ausgeben. In * diesem Kontext ist eine Variable besonderen wichtig, PATH. * Diese enthält eine "Liste" von Verzeichnissen, die * nacheinander abgesucht werden sollen um die tatsächlichen * Programme zu finden. Der Inhalt dieser Variable könnte die * Form * * /bin:/usr/lokal/bin:/usr/bin * * haben. Dieses bedeutet, "Schaue nach einer Datei namens * gcc. Als erstes wird das Verzeichnis /bin betrachtet, dann * /usr/local/bin, dann /usr/bin. Wir schlagen fehl, falls es * die Datei in keinem dieser Verzeichnisse gibt." * * Um in C den Inhalt einer Umgebungsvariable herauszulesen, * können wir die Funktion `getenv' benutzen: */ char *path = getenv("PATH"), *dir; /* Sollte PATH nicht gesetzt sein, haben wir ein Problem, und * geben einen Fehler zurück. Hier ist es ggf. interessant * anzumerken, dass errno nicht in der Fehlerbehandlung sondern * zuvor gesetzt wird. Dieses ist nicht falsch, und demonstriert * wieso man vorsichtig sein muss beim handhaben von `errno'. Im * allgemeinen ist der Wert nur dann sinnvoll, wenn die letzte * Funktion indiziert, dass es Fehler aufgetreten ist UND errno * gesetzt wurde. Führt man in der Zwischenzeit eine andere * Funktion aus, kann diese wie hier gesehen `errno' * überschreiben, auch wenn kein Fehler aufgetreten ist. Daher * ist es auch allgemein eine schlechte Idee Fehlerbehandlung auf * der Grundlage von `errno' durchzuführen, da man es sich damit * erschwert robuste Programme zu schreiben. */ errno = ENOENT; if (path == NULL) { return -1; } /* Wir schauen uns die Komponenten von PATH an, indem wir mit * `strtok' über die Zeichenkette laufen. Wir initialisieren * `strtok' indem wir dem ersten Argument die Daten übergeben, * und rufen die nächsten Komponenten ab indem wir NULL * benutzen. */ dir = strtok(path, ":"); do { /* Für jedes Verzeichnisse bauen wir ein Pfad zusammen, * indem das jetzige Verzeichnis mit der gesuchten Datei * zusammengefügt wird. Hierzu legen wir und genug Speicher * auf dem Stack an und benutzen `sprintf' (wie `printf' nur * wird die Ausgabe in den Speicher, anstatt an die Standard * Ausgabe beschreiben). */ char pathname[strlen(dir) + 1 + strlen(file) + 1]; snprintf(pathname, sizeof(pathname), "%s/%s", dir, file); /* Dateien haben je nach Nutzer verschiedene Berechtigungen * (lesen, schreiben und ausführen), und da wir `exec' * implementieren, wollen wir sichergehen, dass die Datei * ausgeführt werden kann. Dazu wird `access' benutzt, * worauf ich hier nicht in größerem Detail eingehen werde. * Wer verstehen will was hier passiert sollte die access(2) * manpage lesen. * * Kann die Datei ausgeführt werden, übergeben wir den * absoluten Pfad an `execv'. */ if (0 == access(pathname, X_OK)) { return execv(pathname, argv); } } while ((dir = strtok(NULL, ":")) != NULL); return -1; } int main(int argc, char *argv[]) { char *cmd; int i; /* Wie immer wenn man mit `argv' arbeitet, muss man sichergehen * dass genügend Argumente übergeben wurden -> argc muss * abgefragt werden, bevor man auf argv zugreift, sonst * umdefiniertest Verhalten. */ if (argc < 2) { fprintf(stderr, "usage: %s [arg...]\n", argv[0]); return EXIT_SUCCESS; } cmd = argv[1]; /* Für jedes restliche Argument führen wir `cmd' in einem * Kindprozess aus... */ for (i = 2; i < argc; i++) { pid_t pid = fork(); /* Der Rückgabewert von `fork' kann drei Sachen andeuten: */ switch (pid) { /* (1) Das Betriebssystem hat es nicht geschafft (oder * dem Benutzer verweigert) ein neunten Prozess zu * starten. In diesem Fall sagen wir, dass das * fatal ist (was aber nicht allgemein der Fall * sein muss), und geben auf nachdem eine * Fehlermeldung getätigt wurde. */ case -1: perror("fork"); exit(EXIT_FAILURE); /* (2) Das Programm wird im Kindprozess weiter * ausgeführt. In diesem Kontext bedeutet das wir * versuchen das Programm im neuem Prozess zu * ersetzen. Gewöhnlicherweise wäre an dieser * stelle * * execlp(cmd, cmd, argv[i], NULL) * * intuitiver, aber damit `_execvp' zu lang wird * indem varargs (variable argument) Behandlung * implementiert wird, erstellen wir ein Array * Literal und übergeben das direkt an `_execvp'. * Es wäre auch möglich gewesen ein Array auf dem * Stack anzulegen, aber wieso der Aufwand? */ case 0: { if (-1 == _execvp(cmd, (char *[3]) { cmd, argv[i] /*, NULL */ })) { perror("exec"); } /* Nicht vergessen, immer `exit' oder return nach * exec-Funktionen, damit wir nicht versuchen im * Kindprozess die Schleife weiter abzulaufen. */ exit(EXIT_FAILURE); } /* (3) Wir sind im Elternprozess, und müssen auf das * Kind warten. Dazu wird `waitpid' wie in der * Übung besprochen benutzt. Würden wir hier kein * `waitpid' benutzen, würden effektiv alle Prozess * gleichzeitig laufen. Zusätzlich betrachten wir * den Wert der in `status' zurückgeschrieben wird, * und stellen sicher dass 1. das Kind normal * terminiert ist, und nicht vom Betriebssystem * oder von jemand anderem getötet wurde, 2. der * exit status was erfolgreich (testbar mit dem * Programm "false", das immer EXIT_FAILURE setzt). * Ist dieses nicht der Fall, wird ein * Fehlernachricht generiert und wir brechen * insgesamt ab. */ default: { int status, code; if (-1 == waitpid(pid, &status, 0)) { perror("waitpid"); exit(EXIT_FAILURE); } if (!WIFEXITED(status)) { fprintf(stderr, "Command \"%s %s\" did not terminate normally\n", cmd, argv[i]); exit(EXIT_FAILURE); } code = WEXITSTATUS(status); if (EXIT_SUCCESS != code) { fprintf(stderr, "Failed \"%s %s\" with status code %d\n", cmd, argv[i], code); exit(EXIT_FAILURE); } } } } return EXIT_SUCCESS; }