Upline: Infos & Dokus Programmierung C/C++

C/C++ unter Linux - Prozess-Management


FHTW Berlin, Fachrichtung IT/Vernetzte Systeme (B/M), Betriebssysteme SS2005 bei Prof. Metzler
Laborthema Systemprogrammierung Laborkomplex L1: Prozess-Management und Parallele Prozesse unter UNIX
Die Sourcen wurden mit vim geschrieben und per GNU enscript 1.6.3 bunt gemacht. Der Rest entstand im Notepad - wo auch sonst ;)

Aufgabenstellung (Prof. Dr. J. Schmidek • Laboranleitung Systemprogrammierung)

1 Ziel

Ziel dieses Laborkomplexes ist es, sich mit dem Prozesskonzept von UNIX aus Programmierersicht vertraut zu machen. Hierzu soll die Funktion der Systemaufrufe zum Prozess-Management anhand selbständig erstellter Programme demonstriert werden. Dabei wird der Vorlesungsstoff konkretisiert und praktisch umgesetzt.

2 Vorbereitung

2.1 Informieren Sie sich anhand Ihrer Vorlesungsunterlagen sowie der Linux-Manual-Pages über die exec-Systemaufruffamilie.

Beachten Sie dabei besonders die verschiedenen Aufrufvarianten.
  • Wovon hängt die Entscheidung über den Einsatz der jeweils richtigen Variante ab?

2.2 Machen Sie sich vertraut mit den Aufrufen fork(), wait(), waitpid(), getpid(), getppid() und exit() sowie mit der Bibliotheksfunktion sleep().

Benutzen Sie dabei auch die Manual-Pages von Linux.
  • Welchen Wert liefert der Aufruf fork() zurück? Welche Bedeutung hat dieser für Parent u. Child?
  • Was realisieren die Aufrufe wait() und waitpid()? Welche Aufrufvarianten gibt es? Suchen Sie mit Hilfe des Manuals nach verwandten Aufrufen. Wann erscheint der Einsatz welches Aufrufes bzw. welcher Aufrufvariante sinnvoll?
  • Welche Möglichkeiten hat der Parent, verschiedene beendete Child-Prozesse zu unterscheiden?
  • In welchem Zustand befindet sich ein Child-Prozess, der zeitlich vor dem Erreichen des wait-Aufrufes seines Parent-Prozesses terminiert, warum und für wie lange?
  • Wann und warum wird der exit-Aufruf verwendet? Ist dessen Gebrauch zwingend? Welche Aufrufvarianten kennen Sie und was bewirken diese exakt? Liefert der Aufruf einen Wert zurück?
  • Welches Format hat die Exit-Status-Variable, die beim Terminieren über den wait-Aufruf an den Parent übergeben wird, und wie muss dieses bei deren Auswertung berücksichtigt werden?



3 Durchführung

3.1 Entwerfen Sie zwei Programme (flip und flop), ...

die sich über einfache Bildschirmausgaben selbst identifizieren (flip – links, flop – rechts im Bild) und sich anschließend im Sekundentakt wechselseitig mit dem Code des jeweils anderen überlagern. Die Anzahl der Überlagerungen soll beim Start als Argument übergeben werden. Implementieren Sie ebenfalls eine Überprüfung der Gültigkeit des Argumentes. Ausgegeben werden sollen zur Laufzeit der jeweilige Programmname, die Prozess-ID und die Nummer des jeweiligen Durchlaufes. Probieren Sie unterschiedliche exec-Varianten aus. Die Programme sollen in der Programmiersprache C unter Linux / UNIX / Irix implementiert werden.

3.2 Entwerfen Sie ein zweites Programm, dem zwei Argumente (Blockadezeiten in Sek.) übergeben werden können.

Nach dem Prozessstart soll die Gültigkeit der Argumente überprüft und anschließend ein Child-Prozess erzeugt werden. Parent und Child-Prozess sollen jeder für sich (!) die eigenen PID und PPID sowie die jeweils vorgegebene Blockadezeit in folgendem Format ausgeben:
Parent: PID=... PPID=... Blocked for ... s Child: PID=... PPID=... Blocked for ... s
Danach sollen Parent und Child entsprechend den in den Argumenten übergebenen Zeiten blockieren und terminieren. Der Parent soll mit wait auf die Terminierung des Child-Prozesses warten und den vom Child übergebenen Exit-Status anzeigen. Lassen Sie durch Variation der Blockadezeiten den Child-Prozess einmal vor und einmal nach dem wait-Aufruf des Parent-Prozesses terminieren. Kontrollieren Sie Parent- und Child-Prozess-Status zur Laufzeit, z.B. über das Shell-Kommando "ps". Was stellen Sie in beiden Fällen bezüglich des laufenden Child-Prozessstatus fest?

3.3 Ermitteln Sie mit Hilfe eines einfachen C-Programms die tatsächliche Anzahl der auf Ihrem System pro User bzw. pro User-Prozess maximal möglichen Child-Prozesse.

Ist diese konstant? Begründung?

Protokoll

2 Vorbereitung

Einen Teil der deutschen Manpages findet man u.a. auf http://www.linuxinfor.com/german/ und http://www.planetpenguin.de/manpages.html, von wo ich so weit zur Fragestellung passend teilweise direkt kopiert habe. Eine weitere sehr schöne Quelle zum Thema ist http://www.netzmafia.de/skripten/server/syscalls.html bzw. http://wwwuser.gwdg.de/~kboehm/ebook/25_kap19_w6.html, eines von beidem ist aber wohl nur eine leicht geänderte Kopie des anderen...

2.1 Die exec-Funktionen

Die im Rahmen des Labors betrachteten exec-Systemaufrufe (Bibliotheksfunktionen (3)) dienen als Schnittstelle zur Systemfunktion execve(2). Es gibt die Aufruffamilie der execl-Funktionen (execl, execlp, execle) und der execv-Funktionen (execv, execvp), wobei erstere die Parameter (auch: Argumente) in einer normalen Liste übernehmen und die zweite Variante mit den für C/C++ typischen Pointern auf die Parameter herumhantiert. In beiden Fällen werden als 1. Parameter der Dateiname und als letzter ein 0-String übergeben. Weiterhin müssen alle Strings, wie in C/C++ üblich, 0-terminiert sein. Die Funktion execle erhält als zusätzliche Parameter die Umgebungsvariablen des aufrufenden Programms, wärend alle anderen Funktionen die Umgebungsvariablen aus environ übernehmen. Die Funktionen execlp und execvp verfügen über zusätzliche Funktionalität in der Pfadauswertung.

2.2 fork(), wait(), waitpid(), getpid(), getppid(), exit() und sleep()

  • Rückgabewert von fork(): Bei Erfolg wird dem Vaterprozess die PID des Kindes zurückgegeben und 0 dem Kindprozess. Bei Fehlern wird dem Vaterprozess -1 zurückgegeben und errno entsprechend gesetzt. Ein Kindprozess wird nicht generiert.
    Bedeutung für Parent und Child: fork erzeugt einen Kindprozeß, der sich vom Vaterprozeß nur durch die PID und PPID unterscheidet und darin, daß die Verwendung von Resourcen auf 0 gesetzt ist. File locks und noch ausstehende Signale werden nicht vererbt.
  • wait(), waitpid(): Die wait-Funktion sperrt die Ausführung des aktuellen Prozesses, bis ein Child beendet wurde, der aktuelle Prozess ein terminate-Sig bekommen hat oder um eine Sig-Verarbeitung aufzurufen. Wurde der Child bereits beendet (Zombie), kehrt der Aufruf sofort zurück und alle Ressourcen des Childs werden freigegeben. Mit waitpid ist über die Funktionalität von wait hinausgehend das warten auf bestimmte über die PID wählbare Childs möglich, wobei pid=-1 das gleiche Verhalten erzeugt, wie wait(). Des weiteren kann hiermit auch darauf geprüft werden, ob ein Child Zombie ist ohne damit auf dessen Ende warten zu müssen - der Parent kann also weiterarbeiten, falls kein Child beendet wurde. Beide Funktionen geben einen Status zurück, aus dem der Todesgrund für das Child ausgelesen werden kann. Unter BSD gibt es zusätzlich die verwandten Funktionen wait3 und wait4. Das Verhalten von waitpid() kann über eine Bitmaske angepaßt werden. Wird ein Child beendet und der Parent ruft keine der wait-Funktionen auf, so verbleibt bis zum Ende des Parents bereits erwähnter Zombie im System. Theoretisch ist dies nicht weiter schlimm, da kein Speicher mehr belegt wird. Andererseits kann dadurch allerdings auch der Prozesstable gefüllt werden, welcher oft auf 32k begrenzt ist. Gerade Programme mit vielen Forks müssen zwingend regelmäßig nach ihren Childs sehen.
  • Zustand eines terminierten Childs vor wait durch Parent: Der Speicher wird geräumt und es werden entsprechende Einträge in der Prozesstabelle vorgenommen (siehe auch exit()).
  • Wann und warum exit: exit() ist eine Bibliotheksfunktion, die den System Call _exit() um Aufräumfunktionalitäten ergänzt. Es werden alle Puffer geleert, der Adressraum wird freigegeben und in der Prozesstabelle werden der Exit-Status (Werte von 0 bis 255) und die Markierung als Zombie hinterlassen. Diese Einträge werden entweder vom Parent oder falls dieser nicht mehr existiert vom init-Prozess ausgewertet und entfernt. Falls die PID des zum exec-Aufruf gehörenden Prozesses der Prozessgruppen-ID (bzw. Terminalgruppen-ID) entspricht, so schickt der Kernel ein SIGHUP-Signal an alle Prozesse der Gruppe. exit() wird aufgerufen, wenn ein Programm seine Codeausführung beendet hat oder auch von Fehlerroutinen.

3 Durchführung

Die Quelltexte sind auf Grund ihrer Kürze und Übersichtlichkeit unter der Voraussetzung, daß diese Dokumentation gelesen wurde, auch ohne Kommentare weitestgehend selbsterklärend, weshalb ich nur wenige Kommentare eingefügt habe.
Hinweis: Um den an verschiedenen Stellen gewünschten Effekt der Ausgabe in nur einer Zeile zu erreichen, benötigt man für meine Programme ein Terminalfenster mit mehr als 80 Zeichen Breite. Auf der Konsole ist dazu ein entsprechender Textmodus nötig, unter X genügt es, das Terminalfenster zu verbreitern.

3.1 flip und flop

// flip.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv){
 char befehl[]="./flop";
 int z;
 char b2[10];
 if(argc!=2){
  printf("Aufruf: flip x mit x=Anzahl der Aufrufe\n");
  return 1;
 }
 z=atoi(argv[1]);
 printf("flip | argc=%i | argv[1]=%i | ",argc,z);
 printf("PID %i | PPID %i \r",getpid(),getppid());
 fflush(stdout);
 sleep(1);
 if(z>0){
  sprintf(b2,"%i",--z);
  execl(befehl,befehl,b2,0);
 }
 else printf("\nausgeflipt\n");
 return 0;
}
// flop.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv){
 char befehl[]="./flip";
 int z;
 char b2[10];
 if(argc!=2){
  printf("Aufruf: flip x mit x=Anzahl der Aufrufe\n");
  return 1;
 }
 z=atoi(argv[1]);
 printf("\t\t\t\t\t\t\tflop | argc=%i | argv[1]=%i | ",argc,z);
 printf("PID %i | PPID %i \r",getpid(),getppid());
 fflush(stdout);
 sleep(1);
 if(z>0){
  sprintf(b2,"%i",--z);
  execl(befehl,befehl,b2,0);
 }
 else printf("\nausgeflopt\n");
 return 0;
}
Es wird überprüft, ob die Anzahl der Parameter genau 1 ist und falls nicht, erfolgt eine Ausgabe des Programmaufrufes. Da unter Linux als 1. Parameter jedoch der Dateiname selbst mit übergeben wird, muß auf die Paramterlänge 2 getestet werden. Kann der Parameter nicht in einen Ordinaltypen umgewandelt werden, dann wird auch nicht gezählt, die Programme enden in dem Fall ohne Fehlermeldung.

3.2 Parent und Child - Prozeßstatus bei Terminierung

// block.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char **argv){
 int p; //Blockadezeit für Parent
 int c; //Blockadezeit für Child
 pid_t  pid;
 int status;

 if(argc!=3){
  printf("Aufruf: block x y mit x=Parent-Block-Zeit und y=Child-Block-Zeit\n");
  return 1;
 }
 pid=fork();
 if(pid<0){
  printf("Fehler: fork()-Rueckgabe %d.\n", pid);
  exit(2);
 }
 if(pid==0){ //Child
  c=atoi(argv[2]);
  printf("\t\t\t\t\t\tChild: PID=%i PPID=%i Blocked for %is",getpid(),getppid(),c);
  fflush(stdout);
  sleep(c);
  exit(3);
 }else{      // Parent, pid=PID des Childs
  p=atoi(argv[1]);
  printf("Parent: PID=%i PPID=%i Blocked for %is\r",getpid(),getppid(),p);
  fflush(stdout);
  sleep(p);
  pid=wait(&status);
  printf("\nExit-Status des Child: ");
  if(WIFEXITED(status)!=0)printf("%d\n",WEXITSTATUS(status));
  else printf("Fehler\n");
 }
 return 0;
}
Zum Testen verwendete ich die Parameterreihenfolge 5 15, womit der Parent bereits vor dem Ende des Child per wait wartete und ein ps huax demzufolge nichts ungewöhnliches anzeigte. Nach dem Vertauschen der Parameter zu 15 5 konnte man für 10s einen Zombie im System finden. Hierbei wird in der Statusspalte von ps ein "Z" angezeigt und als Prozess ein "[block <defunct>]".

3.3 Child-Flood

// childflood.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
 int z;
 pid_t  pid;
 int status;

 printf("Bisher erzeugte Childs: \n");
 for(z=0;z<10000;z++){
  pid=fork();
  if(pid<0){
   printf("Fehler: fork()-Rueckgabe %d.\n", pid);
   exit(2);
  }
  if(pid==0){ //Child
   sleep(c);
   exit(3);
  }else{      // Parent, pid=PID des Childs
   printf("%i\n",z);
   fflush(stdout);
  }
 }
 return 0;
}
Getestet habe ich dieses Programm als root auf einem Debian 3.0 Woody testing mit Kernelversion 2.4, wobei als Hardware ein Dual-PII-266 mit 192MB RAM Verwendung fand. Etwa ab dem 1500. Child begann das System bei der Erzeugung weiterer Childs, dezent zu stottern. Bei Child #2484 konnte das Programm keine weiteren forks mehr durchführen und entsprechend erhielt ich die im Programm eingebaute Fehlermeldung mit dem Rückgabewert -1. Weitere Programme konnten bis zur Beendigung des Programms childflood auch nicht mehr gestartet werden, womit bei mir nicht nur die Obergrenze der Childs pro Userprozess sondern auch die Obergrenze für den gesamten User erreicht waren. Auch Programme des einfachen Users zeigten Merkwürdigkeiten. So wurde z.B. ein xpenguins gestartet, welches zwar normal lief, jedoch nach Beendigung die Pinguine nicht vom Bildschirm entfernte. Da ein einfaches ps huax bei den vielen Childs etwas unübersichtlich ist, mußte eine andere Möglichkeit gefunden werden, um den Parent wieder zu beenden. Hierfür verwendete ich folgende Zeile:
kill -9 $(ps huax|grep childflood|cut --fields=7 --delimiter=" ")
Dummer Weise funktionierte diese allerdings auch nicht immer auf Anhieb - abhängig davon, ob evtl. doch noch ein weiterer fork mgl. war oder nicht, was man mit dem Beenden eines (anderen) Prozesses erreichen kann. Hierbei kann es zu der Fehlermeldung "su: fork: Die Ressource ist zur Zeit nicht verfügbar" kommen. Sollte dies passieren, dann kann man die Named-Pipes nicht verwenden und muß erstmal ein paar Childs einzeln per Hand abschießen. Ist der Befehl dann ausführbar, entsteht eine (unwichtige) Fehlermeldung durch kill. Diese kann bei Bedarf durch ein tac vermieden werden, welches die letzte grep-Zeile abschneidet. Alternativ hätte man beim Erreichen der programmseitigen Fehlermeldung die Parent-PID ausgeben können, damit hätte dann ein einfaches kill des Parents gereicht ;)