Sehr Umfangreiche Webseite zum Programmieren in C Perl CGI, Skripting, Linux, SystemprogrammierungC C++ C/C++ ANSI C Linux Linuxsystemprogrammierung Unix fork system exec Prozess Prozesse getpid getppid getlogin Linuxsystemprogrammieren fork system exec Prozess Prozesse getpid getppid getlogin 2. Kreiern und Starten von Prozessen

2.1. Grundlagen zu Prozessen           zurück Ein Kapitel tiefer zum Inhaltsverzeichnis

Jeder der sich ernsthaft mit der Programmierung und Linux auseinandersetzen will, sollte wissen, worum es sich bei Prozessen handelt. Unter Linux ist die Prozessverwaltung sozusagen die Schlüsselfigur bei vielen Programmen die Sie Schreiben werden. Sie werden in diesem Kapitel erst mal einiges zu den Prozessen lernen, bevor Sie selbst welche Erzeugen.

Definition eines Prozesses
Ein Prozess ist ein sich in der Ausführung befindliches Programm mit seiner Ausführungsumgebung (Environment).

Die Prozessverwaltung
Da Linux ein echtes Multitaskingsystem ist, werden mehrer Programme (Prozesse, Tasks) gleichzeitig ausgeführt. Wobei Gleichzeitig nicht ganz stimmt. Ein Prozessor (CPU) kann logischerweise immer nur mit einem Prozess arbeiten. Wollen Sie wirklich gleichzeitig das mehrere Programme parallel ablaufen, benötigen Sie entweder mehrere Rechner oder mehrer Prozessoren. Dies dürfte aber für die meisten Anwender uninteressant sein (wegen den Anschaffungskosten).

Jeder Prozess hat einen extra virtuellen Adressraum. Damit kann keiner der Prozesse den anderen Stören oder Beeinflussen.

Zugegriffen wird auf die einzelnen Prozesses nacheinander von der CPU. Wie lange und in welcher Reihenfolge, entscheidet der Prozess-Scheduler. Von der Geschwindigkeit werden Sie nichts mitkriegen. Sie werden den Eindruck haben, dass die Prozesse wirklich parallel ablaufen.

Linux ist als ein preemptive Multitaskingsystem. Dies bedeutet das Linux selbst entscheidet wie lange ein Prozess die CPU benutzen darf und wann der nächste Prozess an der Reihe ist. Wollen Sie in den Prozess-Scheduler eingreifen, können Sie dies als root mit dem Kommando nice machen. Mehr dazu erfahren Sie unter man nice oder nice --help

Welche Prozesse im Augenblick laufen, können Sie mit dem Kommando ps in Erfahrung bringen ...

ps -x

Nun wird ein Liste der momentan laufenden Prozesse ausgegeben. Hierzu kurz ein Erläuterung was die einzelnen Sektionen bedeuten ...

PID  TTY              STAT    TIME   COMMAND
1234 pts/0            R       0:00   ps -x

PID = Prozess-ID. Jeder Prozess bekommt eine eigenen eindeutige Prozess-ID. Über diese Prozess-ID können Sie auf diesen Prozess zugreifen. Wenn Sie zum Beispiel Informationen zum Prozess mit der ID 1501 haben wollen, geben Sie das Kommando ...

ps 1501

... ein. Kennen Sie die Prozess-ID nicht, aber das laufende Kommando, geben Sie in der Konsole folgendes ein ...

pidof /bin/bash

... und Sie erhalten die Prozess-ID dafür. pidof lässt sich allerdings nur als root ausführen.

TTY = Zeigt an in welchem (Kontroll)Terminal der Prozess ausgeführt wird. Steht dort kein Wert handelt es sich meist um einen Dämon-Prozess (kommt noch).

STAT = Zeigt den aktuellen Status des Prozesses an. In unserem Fall steht hier ein R für RUN. Dieser Prozess ist also gerade Aktiv. Folgende Bedeutung haben folgende Zeichen:

TIME = Laufzeit des Prozesses

COMMAND = Kommandoname des Prozesses mit dem Sie diesen gestartet haben.

Die Anordnung der Prozesse unter Linux sind Hierarchisch. Dies läuft wie bei einem Familienstammbaum ab. Jeder Prozess besitzt Informationen von welchem Prozess er erzeugt wurde. Also dem Vaterprozess oder auch Elternprozess genannt.

Sind Sie nun Interessiert welche Prozess wie viel CPU-Zeit benötigt können Sie das Kommando top verwenden. Diese zeigt Ihnen eine Überblick wie viel Rechenzeit die CPU gerade für ein bestimmtes Programm verbrät. Mit 'q' können Sie das Programm wieder beenden. Achten Sie auf die '%CPU' Rechenzeit.

Ein bißchen verwirren wird Sie dabei der Eintrag kapm-idled oder nur idled. Dieser Prozess benötigt ja fast die gesamte Rechenzeit. Da auf unserem System ja momentan nicht allzu viel läuft, müssen die Prozesse ja ein wenig aufgeteilt werden. Idled kann man sich praktisch als eine Endlosschleife vorstellen, die immer wieder ausgeführt wird, wenn nichts anderes zu tun ist. Sie können ja mal eine Kernel compilieren, ein Diskette formatieren und dann das Kommando top eingeben.

Die Prozeßtabelle
Mancher Wissensdurstiger wird sich jetzt fragen, wo sich dieser Scheduler für den Prozess befindet. Sie finden diese Prozesstabelle in der Headerdatei ...

/usr/include/linux/sched.h

Nach der Struktur struct task_struct sollten Sie nun Ausschau halten. Hier befinden sich alle Zustandsinformationen zu einem Prozess. Diese Informationen sind recht Umfangreich und alle hier zu Erklären würde nicht viel bringen. Aber ich finde es ist ganz gut Kommentiert. Grob zusammengefasst finden Sie darin folgende Informationen ...

Prozessidenfikation - Hier steht fest welche Rechte der Prozess hat. Diese wiederum ergeben sich aus den effektiven bzw. realen Benutzer- und Gruppennummern. Natürlich finden Sie hier die oben besprochene Prozessnummer (PID) wieder.

Prozesspriorität - Sie wissen ja das Linux einen Prozess nach dem anderen durchläuft. Jeder Prozeß hat nun eine bestimmte Zeit für sich zur Verfügung. Ist diese Zeit um, muss er seine Arbeit hinlegen, sofern er nicht fertig ist und warten bis er das nächste mal dran kommt. Und dieser Teil ist dafür Zuständig. So kann der Kernel feststellen welchen Prozess er als nächstes ansteuert.

Accounting Informationen - Hier stehen die Informationen zu den Speicherzugriffen im virtuellen Speicher. Diese Informationen werden benötigt wenn ein Prozess auf eine bestimmte Speicherseite zugreifen will, diese aber noch nicht geladen wurde. Nun meldet die Hardware eine Page Fault worauf der Kernel mit dem Nachladen dieser Seite antwortet.

Kontrollterminal - Jeder Prozess, abgesehen von den Dämonprozessen benötigt ein Kontrollterminal wo Sie die Einträge für Standard-Ein/Aus und Fehlerausgabe finden (stdin, stdout, stderr). Lokale Deskriptortabelle - Diese Tabelle wird von Prozessen benötigt die auf die Speicherverwaltung von M$-Systemen zugreifen wollen. Wird von Windows-Emulationen verwendet.

Dies war ein theoretischer Überblick zu den Prozessen unter Linux. In den nächsten Kapiteln werden Sie sehen, wie Sie Prozesse kreieren und diese miteinander kommunizieren können. Natürlich werden Sie auch etwas zu den Dämonprozessen erfahren. Wie und Wozu man Sie verwendet. Sie werden auch erfahrren wie man eigene Dämonprozesse schreibt.

2.2. Der Systemaufruf fork - Kreiern eines Prozesses           zurück Ein Kapitel tiefer Ein Kapitel höher zum Inhaltsverzeichnis

Eben haben Sie jetzt viel über ein Programm und seiner Arbeitsumgebung gehört (Programm+Arbeitsumgebung=Prozess). Nun wird es Zeit selbst Prozesse zu erzeugen. Dazu haben Sie zwei Möglichkeiten. Zum einen kann ein Prozess vollkommend durch einen anderen Ausgetauscht werden, ohne das die Arbeitsumgebung gewechselt wird. Und zum anderen können Sie einen neuen Prozess mit dem Funktionsaufruf fork() erzeugen ...

#include <sys/types>
#include <unistd.h>

pid_t fork(void);

pid_t ist in Primitiver Datentyp, der für Prozess-ID oder auch Gruppen-Prozess-ID steht. Mit einem Aufruf von fork erzeugen Sie einen neuen Prozess (Kindprozess), der fast identisch ist mit seinem Erzeuger, dem Elternprozess. Folgende Merkmale erbt der Kindprozess von seinen Eltern ...

Folgende Merkmale bekommt das Kind nicht von seinen Eltern ...

Durch den Aufruf von fork entstehen somit zwei völlig Identische Prozesse die vom Programm verarbeitet werden. Alle Anweisungen nach fork werden zweimal ausgeführt. Einmal im Kindprozess und einmal im Elternprozess.

Der Kindprozess und der Elternprozess erhalten nach dem Aufruf von fork unterschiedlich Rückgabewerte ... Der Elternprozess bekommt die Prozessidenfikation (PID) des Kindes als Rückgabewert. Sollte dieser Wert negativ sein liegt ein Fehler vor. Der Kindprozess erhält den Wert 0 als Rückgabewert wenn der fork-Aufruf geglückt ist.

Somit können Sie das ganze folgendermaßen überprüfen, ob ein neuer Prozess erzeugt werden konnte ...

switch(ret=fork())
   {
     case -1 :  /*fork() war erfolglos - Fehlermeldung _exit*/
     case 0 :   /*Code für den Kindprozeß*/
     default :  /*Code für den Elternprozeß*/
   }

Nun folgt ein Beispiel zu fork ...

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
 pid_t kind1;

 while(1)
  {
    switch(kind1=fork())
      {
        case -1 : exit(0);  break;
        case  0 : system("konsole -caption 'kind' -vt_sz 20x10");  break;
        default : printf("Elternprozess: Warte auf das Ende vom Kindprozess!\n");
                  printf("Um den Prozess %s zu killen gib bitte ein : ",*argv);
                  printf("kill -9 %d (%d=PID)\n",kind1,kind1);exit(1);
      }
  }
 return 0;
}

Ich gehe mal davon aus, dass die meisten Linux unter einer grafischen Oberfläche (KDE, GNOME...) benutzen. Mit ...

switch(kind1=fork())

...führen Sie fork aus. Der Rückgabewert steht nun in der Variablen kind1. Nun wird getestet ob ein Kindprozess gestartet werden konnte. Bei -1 wird das Programm gleich wieder abgebrochen. Falls ein Kindprozess erzeugt werden konnte, wird in diesem Beispiel mittels....

case 0 : system("konsole -caption 'kind' -vt_sz 20x10");  break;

...eine neue Konsole 'kind' mit der Größe 20x10 geöffnet. Diese Kind besitzt nun sämtliche oben genannten Eigenschaften wie der Elternprozess, kann aber nur Code ausführen die für Ihn bestimmt sind (kind1==0). Würden Sie das break entfernen, würde unser Kindprozess ein neuen Kindprozess starten und wäre somit der "Vater" von einem neuen Kindprozess und der "Sohn" von dem Elternprozess. Damit würden natürlich unendlich viele "Kinder" erzeugt, bis vor Überbevölkerung des Arbeitspeichers der PC streikt. Also lassen Sie break wo es ist! while(1) läuft erneut ab und nun gibt unser Elternprozess (kind1 > 0) aus, wie Sie den Kindprozess wieder killen können. Sie können nun versuchen das Fenster zu schließen oder die Konsole mit exit schließen oder gar mit ...

kill -SIGKILL PID von konsole

...den Kindprozess schließen. Geben Sie doch in einer der beiden Konsolen mal ps oder ps x ein (oder pstree). Diese Befehle zeigen Ihnen, welche Prozesse mit welchem Status gerade am laufen sind. Bei ps x sehen Sie z.B.

PID    TTY      STAT  TIME      COMMAND
214    ?         S    0:00      xterm -ls -T Failsave -geometry 80x24-0-0
223    pts/0     SW   0:00      [bash]
..........................................
..........................................
294    pts/1     SW   0:00      [cat]
295    pts/0     S    0:00      knotify
298    pts/0     S    0:00      ksmserver --restore
299    ?         S    0:02      kdeinit: kwin
300    ?         S    0:04      kdeinit: kwrite
678    ?         S    0:01      kdeinit: konsole
679    pts/4     S    0:00      /bin/bash
1019   ?         S    0:07      kdeinit: kwrite
1111   pts/4     S    0:00      fork1
1112   pts/4     S    0:00      konsole -caption kind -vt_sz 20x10
1113   pts/2     S    0:00      /bin/bash
1120   pts/4     R    0:00      ps x

In diesem Beispiel ist es der Prozess mit der PID 1111 und dem Terminal pts/4 und dem Status sleep (S). Das Programm hat den Namen fork1. fork1 ist nun eine Prozess in unserem System. Das Fenster das Sie mit fork geöffnet haben ist in dem Fall ...

1113 pts/2 S 0:00 /bin/bash

Prozesse erzeugen mit fork, Elternprozess - Kindprozess

Anhand dieser Zeichnung können Sie erkennen, dass der Elternprozess solange wartet, bis sich der Kindprozess beendet. Vorher kann sich der Elternprozess nicht beenden. Dies läßt sich aber auch vermeiden. Dazu kommen wir noch. Daher blockiert sich in unserem Programm die Eltern-Konsole so lange, bis sich der Kind-Prozess beendet. Um dies zu vermeiden könnten Sie aber auch das Programm fork1 mit ...

./fork1 &

...im Hintergrund laufen lassen.

Das Linux ein Multitasking-System ist, wissen Sie ja bereits. Ein Beispiel unter MS-DOS. Sie wollen eine Diskette mit dem Befehl ...

format a:\

...formatieren. Was machen Sie nun während die Diskette (was ja einige Zeit dauert) formatiert? Nichts, Sie müssen warten bis der Vorgang fertig ist. Unter Linux können Sie dabei ganz einfach das Terminal wechseln oder unter KDE oder Gnome eine neues Fensterterminal öffnen. Dies Beispiel wollen wir jetzt in das Programm miteinbauen. Der Kindprozess formatiert eine Diskette mit ...

fdformat /dev/fd0 (Low-Level-Formatierung)

...und derElternprozess öffnet eine neues Fenster mit dem Sie weiterarbeiten können ...

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
 pid_t kind1;

 while(1)
  {
   switch(kind1=fork())
    {
      case -1 :   exit(0);  break;
      case  0 :   system("fdformat /dev/fd0");  exit(1);
      default :   system("konsole -caption 'Neue Konsole'");
                  exit(2);
    }
  }
 return 0;
}

Ähnliches machen Sie, wenn Sie z.B. fdformat folgendermaßen anwenden...

fdformat /dev/fd0 > /dev/null &

Mit dem Zeichen '&' sorgen Sie dafür das der Prozess im Hintergrund ausgeführt wird und mit > /dev/null leiten Sie die nervige Ausgabe 'Formatiert n Prozent' ins nichts.

Dies ermöglicht es dem Befehlsinterpreter, sofort zurückzukehren, so dass Sie weitere Befehle eingeben können. Ohne dem &-Symbol wartet der Befehlsinterpreter darauf, dass ein Programm beendet wird, bevor er die Eingabe weiterer Befehle erlaubt. Manch einer von Ihnen wird versucht haben, in dem vom Elternprozess neu geöffneten Fenster, mittels ps x zu schauen welche PID unser Prozess 'fdformat ....' hat. Dabei wird Ihnen aufgefallen sein, das der Status von 'fdformat ....' auf S für Sleeping steht. Und das ist auch richtig so, den in dem Moment in dem Sie ps x ausführen wird die Formatierung der Diskette für Bruchteile von Sekunden unterbrochen um den Befehl von ps x auszuführen, der ja auch ein Prozess in der Ausführung ist.

Daran können Sie auch erkennen, dass echtes Multitasking nicht möglich ist, da ein Prozessor und mag er 10.000 MHZ Taktung haben nicht zwei Dinge auf einmal erledigen kann. Dazu würden Sie mehr (mindestens zwei) Prozessoren oder Rechner benötigen. Also denken Sie daran wenn Sie das Wort Multitasking hören. Dies ist nur Möglich mit Multiprozessor-Systemen oder mehreren Rechnern (kommt auf das selbe raus). Ändern wir das Programm nun um, damit keine Konsole mehr geöffnet werden muss ...

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
 pid_t kind1;

 while(1)
  {
   switch(kind1=fork())
    {

     case -1 :   exit(0);  break;
     case  0 :   system("fdformat /dev/fd0 > /dev/null &");  exit(1);
     default :   printf("Formatieren unterbrechen mit kill -9 %d\n",kind1);
                 exit(2);
    }
  }
 return 0;
}

2.3. fork - Probleme mit der Pufferung des Kindprozesses           zurück Ein Kapitel höher zum Inhaltsverzeichnis

Ein Problem bei fork() kann z.B. die Pufferung darstellen, wenn man nicht weiß wie man damit umzugehen hat. Testen Sie.dazu folgendes Programm ...

#include <sys/types.h>
#include <stdio.h>

int main()
{
 pid_t pid;

 printf("Hallo fork()\n");

 switch(pid=fork())
  {
    case -1: printf("Fehler bei fork().....\n"); exit(0);
    case  0: printf("Ich bin das Kind\n"); break;
    default: printf("Ich bin der Elternprozess\n"); break;
  }
 exit(0);
}

Das Programm dürfte etwa folgendes Ausgeben ...

Hallo fork()
Ich bin der Elternprozess
Ich bin das Kind

Auf dem ersten Blick scheint das OK zu sein. Aber müsste vor dem Kindprozess nicht auch die Ausgabe ...

Hallo fork()

...stehen?

Normalerweise schon aber da printf mit Puffern arbeitet gilt folgendes ...

Und Vollpufferung heißt, dass erst geschrieben wird, wenn der Puffer voll ist (in unserem Fall eine Ausgabe auf dem Bildschirm). Sie können ja mal als Beweis dazu die Ausgabe in eine Datei Umleiten ...

forkig1 > temp

In diesem Fall bietet sich an, die Ausgabe mit der Funktion write zum machen und ganz auf die Pufferung zu verzichten. Dies würde dann so aussehen ...

write(stdout, text, strlen(text));

Umgeschrieben auf unser Programmbeispiel von oben ...

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
 pid_t pid;
 char text[] = "Hallo fork()\n";

 write(STDOUT_FILENO, text, strlen(text));

 switch(pid=fork())
  {
    case -1: printf("Fehler bei fork().....\n"); exit(0);
    case  0: printf("Ich bin das Kind\n"); break;
    default: printf("Ich bin der Elternprozess\n"); break;
  }
 exit(0);
}

Die Reihenfolge bei der Ausgabe die gemacht wird, haben Sie jetzt noch keinen Einfluss, also ob zuerst KIND und dann Eltern oder umgekehrt. Im Kern gibt es dafür einen Algorithmus, einen Scheduler, der dafür verantwortlich ist. Wollen Sie dies Beeinflussen müssen Sie die beiden Prozesse (Eltern und Kind) synchronisieren. Sie werden noch sehen wie Sie das Bewerkstelligen können.

Weiter mit 2.4. Mehrere fork()-Aufrufe hintereinander