C-Programmierung

Ihr erstes C-Programm mit Fork System Call

Ihr erstes C-Programm mit Fork System Call
Standardmäßig haben C-Programme keine Nebenläufigkeit oder Parallelität, es wird nur eine Aufgabe gleichzeitig ausgeführt, jede Codezeile wird sequentiell gelesen. Aber manchmal muss man eine Datei lesen oder - noch schlimmer - eine Steckdose, die mit einem entfernten Computer verbunden ist, und das dauert bei einem Computer wirklich lange. Es dauert im Allgemeinen weniger als eine Sekunde, aber denken Sie daran, dass ein einzelner CPU-Kern 1 oder 2 Milliarden ausführen der Anweisungen während dieser Zeit.

So, als guter Entwickler, Sie werden versucht sein, Ihr C-Programm anzuweisen, etwas Nützlicheres zu tun, während Sie warten. Hier ist die Parallelitätsprogrammierung zu Ihrer Rettung da here - und macht Ihren Computer unglücklich, weil er mehr funktionieren muss.

Hier zeige ich Ihnen den Linux-Fork-Systemaufruf, eine der sichersten Möglichkeiten, gleichzeitig zu programmieren.

Gleichzeitiges Programmieren kann unsicher sein?

Ja, kann es. Es gibt zum Beispiel auch eine andere Möglichkeit, anzurufen Multithreading. Es hat den Vorteil, leichter zu sein, aber es kann Ja wirklich schief gehen, wenn Sie es falsch verwenden. Wenn Ihr Programm aus Versehen eine Variable liest und in die gleiche Variable Gleichzeitig wird Ihr Programm inkohärent und es ist fast nicht nachweisbar - einer der schlimmsten Albträume der Entwickler.

Wie Sie unten sehen werden, kopiert fork den Speicher, sodass solche Probleme mit Variablen nicht möglich sind. Außerdem erstellt fork einen unabhängigen Prozess für jede gleichzeitige Aufgabe. Aufgrund dieser Sicherheitsmaßnahmen ist es ungefähr 5x langsamer, eine neue gleichzeitige Aufgabe mit fork zu starten als mit Multithreading. Wie Sie sehen, ist das nicht viel für die Vorteile, die es mit sich bringt.

Nun, genug der Erklärungen, ist es an der Zeit, Ihr erstes C-Programm mit einem fork-Aufruf zu testen.

Das Linux-Fork-Beispiel

Hier ist der Code:

#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
int main()
pid_t forkStatus;
forkStatus = fork();
/* Kind… */
if        (forkStatus == 0)
printf("Kind läuft, Verarbeitung.\n");
schlafen(5);
printf("Kind ist fertig, wird beendet.\n");
/* Elternteil… */
else if (forkStatus != -1)
printf("Eltern wartet…\n");
warten (NULL);
printf("Elternteil wird beendet… \n");
sonst
perror("Fehler beim Aufrufen der Gabelfunktion");

0 zurückgeben;

Ich lade Sie ein, den obigen Code zu testen, zu kompilieren und auszuführen, aber wenn Sie sehen möchten, wie die Ausgabe aussehen würde und Sie zu „faul“ sind, um ihn zu kompilieren - schließlich bist du vielleicht ein müder Entwickler, der den ganzen Tag C-Programme kompiliert hat - Sie finden die Ausgabe des C-Programms unten zusammen mit dem Befehl, den ich zum Kompilieren verwendet habe:

$ gcc -std=c89 -Wpedantic -Wand gabelSleep.c -o GabelSchlaf -O2
$ ./gabelSchlaf
Eltern warten…
Kind läuft, Verarbeitung.
Kind ist fertig, Ausstieg.
Elternteil geht aus…

Bitte haben Sie keine Angst, wenn die Ausgabe nicht zu 100% mit meiner obigen Ausgabe übereinstimmt. Denken Sie daran, dass die gleichzeitige Ausführung von Aufgaben bedeutet, dass Aufgaben nicht in der richtigen Reihenfolge ausgeführt werden, es gibt keine vordefinierte Reihenfolge order. In diesem Beispiel sehen Sie möglicherweise, dass das Kind läuft Vor Eltern warten, und daran ist nichts auszusetzen. Im Allgemeinen hängt die Reihenfolge von der Kernel-Version, der Anzahl der CPU-Kerne, den Programmen, die derzeit auf Ihrem Computer ausgeführt werden, usw.

OK, jetzt zurück zum Code. Vor der Zeile mit fork() ist dieses C-Programm völlig normal: 1 Zeile wird gleichzeitig ausgeführt, es gibt nur einen Prozess für dieses Programm (wenn es eine kleine Verzögerung vor fork gab, können Sie dies in Ihrem Task-Manager bestätigen).

Nach fork() gibt es jetzt 2 Prozesse, die parallel laufen können. Zuerst gibt es einen untergeordneten Prozess. Dieser Prozess wurde bei fork() erstellt. Dieser untergeordnete Prozess ist etwas Besonderes: Er hat keine der Codezeilen über der Zeile mit fork() ausgeführt. Anstatt nach der Hauptfunktion zu suchen, wird eher die Zeile fork() ausgeführt.

Was ist mit den Variablen, die vor fork deklariert wurden??

Nun, Linux fork() ist interessant, weil es diese Frage intelligent beantwortet. Variablen und tatsächlich der gesamte Speicher in C-Programmen wird in den Kindprozess kopiert.

Lassen Sie mich in wenigen Worten definieren, was fork macht: Es erzeugt a Klon des Prozesses, der es aufruft. Die beiden Prozesse sind fast identisch: Alle Variablen enthalten die gleichen Werte und beide Prozesse führen die Zeile direkt nach fork() aus. Allerdings nach dem Klonen, Sie sind getrennt. Wenn Sie eine Variable in einem Prozess aktualisieren, wird der andere Prozess Gewohnheit seine Variable aktualisieren lassen. Es ist wirklich ein Klon, eine Kopie, die Prozesse teilen fast nichts. Es ist wirklich nützlich: Sie können viele Daten vorbereiten und dann fork() und diese Daten in allen Klonen verwenden.

Die Trennung beginnt, wenn fork() einen Wert zurückgibt. Der ursprüngliche Prozess (er heißt der übergeordnete Prozess) erhält die Prozess-ID des geklonten Prozesses. Auf der anderen Seite der geklonte Prozess (dieser heißt is der Kindprozess) erhält die 0-Zahl. Jetzt sollten Sie verstehen, warum ich if/else if-Anweisungen nach der fork()-Zeile gesetzt habe. Mit dem Rückgabewert können Sie das Kind anweisen, etwas anderes zu tun als das Elternteil - und glaub mir, es ist nützlich.

Auf der einen Seite führt das Kind im obigen Beispielcode eine Aufgabe aus, die 5 Sekunden dauert, und druckt eine Nachricht. Um einen Vorgang zu imitieren, der lange dauert, nutze ich die Schlaffunktion. Dann beendet das Kind erfolgreich.

Auf der anderen Seite druckt das Elternteil eine Nachricht, wartet, bis das Kind das Programm verlässt und druckt schließlich eine weitere Nachricht. Die Tatsache, dass Eltern auf ihr Kind warten, ist wichtig. Als Beispiel wartet das Elternteil die meiste Zeit damit, auf sein Kind zu warten. Aber ich hätte die Eltern anweisen können, jede Art von lang andauernden Aufgaben zu erledigen, bevor ich ihr sagte, sie solle warten. Auf diese Weise hätte es nützliche Aufgaben erledigt, anstatt zu warten - Schließlich verwenden wir dafür Gabel(), nein?

Aber wie ich oben schon sagte, es ist wirklich wichtig, dass Eltern warten auf ihre Kinder. Und es ist wichtig, weil Zombie-Prozesse.

Wie wichtig Warten ist

Eltern möchten im Allgemeinen wissen, ob die Kinder ihre Verarbeitung abgeschlossen haben. Sie möchten beispielsweise Aufgaben parallel ausführen, aber willst du bestimmt nicht die Eltern beenden, bevor die Kinder fertig sind, denn wenn dies passiert, würde die Shell eine Eingabeaufforderung zurückgeben, während die Kinder noch nicht fertig sind - was ist seltsam.

Mit der Wait-Funktion kann gewartet werden, bis einer der Kindprozesse beendet wird. Wenn ein Elternteil 10 mal fork() aufruft, muss es auch 10 mal wait() aufrufen, einmal für jedes Kind erstellt.

Aber was passiert, wenn Eltern die Wartefunktion aufrufen, während alle Kinder haben? bereits verlassen? Hier sind Zombie-Prozesse gefragt.

Wenn ein Kind beendet wird, bevor parent wait() aufruft, lässt der Linux-Kernel das Kind beenden aber es wird ein Ticket behalten dem Kind sagen, dass es gegangen ist. Wenn das Elternteil dann wait() aufruft, findet es das Ticket, löscht dieses Ticket und die Funktion wait() kehrt zurück sofort weil es weiß, dass die Eltern wissen müssen, wann das Kind fertig ist. Dieses Ticket heißt a Zombie-Prozess.

Deshalb ist es wichtig, dass parent wait() aufruft: Wenn dies nicht der Fall ist, bleiben Zombie-Prozesse im Speicher und im Linux-Kernel kippen halten viele Zombie-Prozesse im Gedächtnis. Sobald das Limit erreicht ist, ist Ihr Computers kann keinen neuen Prozess erstellen und so wirst du in a sehr schlechter zustand: sogar Um einen Prozess zu beenden, müssen Sie möglicherweise einen neuen Prozess dafür erstellen. Wenn Sie beispielsweise Ihren Task-Manager öffnen möchten, um einen Prozess zu beenden, können Sie dies nicht, da Ihr Task-Manager einen neuen Prozess benötigt. Noch schlimmer, Sie können nicht Töte einen Zombie-Prozess.

Deshalb ist der Aufruf von wait wichtig: Er erlaubt dem Kernel Aufräumen den untergeordneten Prozess, anstatt sich ständig mit einer Liste beendeter Prozesse anzuhäufen. Und was ist, wenn die Eltern gehen, ohne jemals anzurufen? warten()?

Glücklicherweise kann niemand sonst wait() für diese Kinder aufrufen, da das Elternteil beendet ist, also gibt es kein Grund um diese Zombie-Prozesse zu halten. Daher, wenn ein Elternteil ausscheidet, alles übrige Zombie-Prozesse mit diesem Elternteil verbunden werden entfernt. Zombie-Prozesse sind Ja wirklich nur nützlich, um Elternprozessen zu erlauben, festzustellen, dass ein Kind beendet wurde, bevor Eltern wait() aufgerufen haben.

Jetzt möchten Sie vielleicht einige Sicherheitsmaßnahmen kennen, um die beste Verwendung der Gabel ohne Probleme zu ermöglichen.

Einfache Regeln, damit die Gabel wie vorgesehen funktioniert

Erstens, wenn Sie Multithreading kennen, teilen Sie bitte kein Programm mit Threads. Vermeiden Sie es im Allgemeinen, mehrere Parallelitätstechnologien zu mischen. fork geht davon aus, dass es in normalen C-Programmen funktioniert, es beabsichtigt nur eine parallele Task zu klonen, nicht mehr.

Zweitens, vermeiden Sie es, Dateien vor fork() zu öffnen oder zu öffnen. Dateien ist eines der einzigen Dinge geteilt und nicht geklont zwischen Eltern und Kind. Wenn Sie 16 Byte in Parent lesen, wird der Lese-Cursor um 16 Byte vorwärts bewegt beide im Elternteil und im kind. Am schlimmsten, wenn Kind und Eltern Bytes in die . schreiben gleiche Datei gleichzeitig können die Bytes von Parent sein gemischt mit Bytes des Kindes!

Um es klarzustellen: Außerhalb von STDIN, STDOUT und STDERR möchten Sie wirklich keine offenen Dateien mit Klonen teilen.

Drittens, seien Sie vorsichtig mit Steckdosen. Steckdosen sind auch geteilt zwischen Eltern und Kind. Dies ist nützlich, um einen Port abzuhören und dann mehrere untergeordnete Worker bereitzuhalten, die eine neue Client-Verbindung verarbeiten können. jedoch, Wenn Sie es falsch verwenden, werden Sie in Schwierigkeiten geraten.

Viertens, wenn Sie fork() innerhalb einer Schleife aufrufen möchten, tun Sie dies mit extreme Sorgfalt. Nehmen wir diesen Code:

/* DIES NICHT KOMPILIEREN */
const int targetFork = 4;
pid_t forkErgebnis
 
für (int i = 0; i < targetFork; i++)
forkResult = fork();
/*… */
 

Wenn Sie den Code lesen, könnten Sie erwarten, dass er 4 Kinder erstellt child. Aber es wird eher schaffen 16 Kinder. Es ist, weil Kinder es werden ebenfalls führe die Schleife aus und die Kinder werden wiederum fork() aufrufen. Wenn die Schleife unendlich ist, heißt sie a Gabelbombe und ist eine der Möglichkeiten, ein Linux-System zu verlangsamen so sehr, dass es nicht mehr funktioniert und brauche einen Neustart. Kurz gesagt, denken Sie daran, dass Clone Wars nicht nur in Star Wars gefährlich ist!

Jetzt haben Sie gesehen, wie eine einfache Schleife schief gehen kann, wie Sie Schleifen mit fork() verwenden? Wenn Sie eine Schleife benötigen, überprüfen Sie immer den Rückgabewert von fork:

const int targetFork = 4;
pid_t forkResult;
int i = 0;
tun
forkResult = fork();
/*… */
i++;
while ((forkResult != 0 && forkResult != -1) && (i < targetFork));

Fazit

Jetzt ist es Zeit für Sie, Ihre eigenen Experimente mit fork() durchzuführen! Probieren Sie neue Möglichkeiten aus, um die Zeit zu optimieren, indem Sie Aufgaben über mehrere CPU-Kerne hinweg ausführen oder einige Hintergrundverarbeitungen durchführen, während Sie auf das Lesen einer Datei warten!

Zögern Sie nicht, die Handbuchseiten über den Befehl man zu lesen. Sie erfahren, wie fork() genau funktioniert, welche Fehler Sie erhalten können usw. Und genieße die Parallelität!

Battle for Wesnoth-Tutorial
The Battle for Wesnoth ist eines der beliebtesten Open-Source-Strategiespiele, die Sie derzeit spielen können. Dieses Spiel befindet sich nicht nur se...
0 A.D. Lernprogramm
Von den vielen Strategiespielen da draußen, 0 A.D. schafft es, sich trotz Open Source als umfassender Titel und sehr tiefgehendes, taktisches Spiel ab...
Unity3D-Tutorial
Einführung in Unity 3D Unity 3D ist eine leistungsstarke Engine für die Spieleentwicklung. Es ist plattformübergreifend, das heißt, Sie können Spiele ...