C Programmeren

Uw eerste C-programma met Fork System Call

Uw eerste C-programma met Fork System Call
Standaard hebben C-programma's geen gelijktijdigheid of parallellisme, er gebeurt slechts één taak tegelijk, elke regel code wordt opeenvolgend gelezen. Maar soms moet je een bestand lezen of - nog erger - een socket aangesloten op een externe computer en dit duurt echt lang voor een computer. Het duurt over het algemeen minder dan een seconde, maar onthoud dat een enkele CPU-kern kan uitvoeren van 1 of 2 miljarden instructies gedurende die tijd.

Zo, als een goede ontwikkelaar, je zult in de verleiding komen om je C-programma te instrueren om iets nuttigs te doen terwijl je wacht. Dat is waar gelijktijdigheidsprogrammering is hier voor uw redding - en maakt je computer ongelukkig omdat hij meer moet werken.

Hier laat ik je de Linux-fork-systeemaanroep zien, een van de veiligste manieren om gelijktijdig te programmeren.

Gelijktijdig programmeren kan onveilig zijn?

Ja het kan. Er is bijvoorbeeld ook een andere manier om te bellen multithreading. Het heeft het voordeel dat het lichter is, maar het kan werkelijk fout gaan als je het verkeerd gebruikt. Als uw programma per ongeluk een variabele leest en schrijft naar de dezelfde variabele tegelijkertijd wordt je programma onsamenhangend en is het bijna niet op te sporen - een van de ergste nachtmerries voor ontwikkelaars.

Zoals je hieronder zult zien, kopieert fork het geheugen, dus het is niet mogelijk om dergelijke problemen met variabelen te hebben. Fork maakt ook een onafhankelijk proces voor elke gelijktijdige taak. Vanwege deze beveiligingsmaatregelen is het ongeveer 5x langzamer om een ​​nieuwe gelijktijdige taak te starten met fork dan met multithreading. Zoals je kunt zien, is dat niet veel voor de voordelen die het met zich meebrengt.

Nu genoeg uitleg, het is tijd om je eerste C-programma te testen met behulp van fork call.

Het voorbeeld van de Linux-vork

Hier is de code:

#include
#include
#include
#include
#include
int hoofd()
pid_t forkStatus;
forkStatus = fork();
/* Kind... */
if        (forkStatus == 0)
printf("Kind loopt, bezig met verwerken.\n");
slaap(5);
printf("Kind is klaar, verlaten.\n");
/* Ouder… */
else if (forkStatus != -1)
printf("Ouder wacht... \n");
wacht (NULL);
printf("Ouder gaat weg... \n");
anders
perror("Fout bij het aanroepen van de fork-functie");

retourneer 0;

Ik nodig je uit om de bovenstaande code te testen, compileren en uit te voeren, maar als je wilt zien hoe de uitvoer eruit zou zien en je te "lui" bent om het te compileren - je bent tenslotte misschien een vermoeide ontwikkelaar die de hele dag C-programma's heeft gecompileerd - je kunt de uitvoer van het C-programma hieronder vinden, samen met de opdracht die ik heb gebruikt om het te compileren:

$ gcc -std=c89 -Wpedantic -WandvorkSlaap.c -o vorkSlaap -O2
$ ./vorkSlaap
Ouder wacht..
Kind is aan het rennen, bezig met verwerken.
Kind is klaar, verlaten.
Ouder gaat weg..

Wees alsjeblieft niet bang als de output niet 100% identiek is aan mijn output hierboven. Onthoud dat het tegelijkertijd uitvoeren van taken betekent dat taken niet meer in orde zijn, er is geen vooraf gedefinieerde volgorde. In dit voorbeeld ziet u misschien dat kind rent voordat ouder wacht, en daar is niks mis mee. Over het algemeen hangt de volgorde af van de kernelversie, het aantal CPU-cores, de programma's die momenteel op uw computer draaien, enz.

OK, ga nu terug naar de code. Vóór de regel met fork(), is dit C-programma volkomen normaal: er wordt 1 regel tegelijk uitgevoerd, er is maar één proces voor dit programma (als er een kleine vertraging was voor de fork, kunt u dat bevestigen in uw taakbeheer).

Na de fork() zijn er nu 2 processen die parallel kunnen lopen. Ten eerste is er een kindproces. Dit proces is het proces dat is gemaakt op fork(). Dit onderliggende proces is speciaal: het heeft geen enkele coderegel boven de regel met fork() uitgevoerd. In plaats van te zoeken naar de hoofdfunctie, zal het eerder de fork()-regel uitvoeren.

Hoe zit het met de variabelen die vóór fork zijn gedeclareerd??

Welnu, Linux fork() is interessant omdat het deze vraag slim beantwoordt. Variabelen en in feite al het geheugen in C-programma's worden gekopieerd naar het onderliggende proces.

Laat me in een paar woorden definiëren wat vork doet: het creëert een kloon van het proces dat het noemt. De 2 processen zijn bijna identiek: alle variabelen zullen dezelfde waarden bevatten en beide processen zullen de regel uitvoeren net na fork(). Echter, na het kloonproces, ze zijn gescheiden. Als u een variabele in het ene proces bijwerkt, zal het andere proces zal niet zijn variabele laten bijwerken. Het is echt een kloon, een kopie, de processen delen bijna niets. Het is echt handig: je kunt veel gegevens voorbereiden en vervolgens fork() gebruiken en die gegevens in alle klonen gebruiken.

De scheiding begint wanneer fork() een waarde retourneert. Het oorspronkelijke proces (het heet het ouderproces) krijgt de proces-ID van het gekloonde proces. Aan de andere kant, het gekloonde proces (deze heet het kindproces) krijgt het 0-nummer. Nu zou je moeten beginnen te begrijpen waarom ik if/else if-statements na de fork()-regel heb geplaatst. Met behulp van retourwaarde kunt u het kind instrueren iets anders te doen dan de ouder doet - en geloof me, het is handig.

Aan de ene kant, in de voorbeeldcode hierboven, voert het kind een taak uit die 5 seconden duurt en drukt een bericht af. Om een ​​proces na te bootsen dat lang duurt, gebruik ik de slaapfunctie. Dan verlaat het kind met succes.

Aan de andere kant drukt de ouder een bericht af, wacht tot het kind afsluit en drukt tenslotte nog een bericht af. Het feit dat de ouder op zijn kind wacht, is belangrijk. Het is een voorbeeld: de ouder wacht het grootste deel van deze tijd op zijn kind. Maar ik had de ouder kunnen instrueren om langlopende taken uit te voeren voordat ik hem vertelde te wachten. Op deze manier zou het nuttige taken hebben gedaan in plaats van te wachten - dit is tenslotte de reden waarom we vork(), nee?

Zoals ik hierboven al zei, is het echter heel belangrijk dat ouder wacht op zijn kind. En het is belangrijk vanwege zombieprocessen.

Hoe wachten belangrijk is

Ouders willen over het algemeen weten of kinderen klaar zijn met de verwerking. U wilt bijvoorbeeld taken parallel uitvoeren, maar dat wil je zeker niet de ouder om af te sluiten voordat het kind klaar is, want als het zou gebeuren, zou shell een prompt teruggeven terwijl het kind nog niet klaar is - wat raar is.

Met de wachtfunctie kan worden gewacht tot een van de onderliggende processen is beëindigd. Als een ouder 10 keer fork() aanroept, moet hij ook 10 keer wait() aanroepen, een keer voor elk kind gemaakt.

Maar wat gebeurt er als de ouder de wachtfunctie aanroept terwijl alle kinderen dat hebben? nu al verlaten? Dat is waar zombieprocessen nodig zijn.

Wanneer een kind afsluit voordat de ouder wacht() aanroept, laat de Linux-kernel het kind afsluiten maar het zal een kaartje houden vertellen dat het kind is weggegaan. Dan, wanneer de ouder wait() aanroept, zal het het ticket vinden, dat ticket verwijderen en de wait() functie zal terugkeren direct omdat het weet dat de ouder moet weten wanneer het kind klaar is. Dit kaartje heet a zombie proces.

Daarom is het belangrijk dat de ouder wait() aanroept: als dit niet het geval is, blijven de zombieprocessen in het geheugen en de Linux-kernel kan niet bewaar veel zombieprocessen in het geheugen. Zodra de limiet is bereikt, is uw computerkan geen nieuw proces maken en zo kom je in een zeer slechte staat: zelfs voor het doden van een proces, moet u daarvoor mogelijk een nieuw proces maken. Als u bijvoorbeeld uw taakbeheer wilt openen om een ​​proces te beëindigen, kunt u dat niet, omdat uw taakbeheer een nieuw proces nodig heeft. Nog erger, je kan niet dood een zombieproces.

Daarom is het aanroepen van wait belangrijk: het staat de kernel toe schoonmaken het onderliggende proces in plaats van zich op te stapelen met een lijst met beëindigde processen. En wat als de ouder vertrekt zonder ooit te bellen? wacht()?

Gelukkig, aangezien de ouder is beëindigd, kan niemand anders wait() aanroepen voor deze kinderen, dus er is geen reden om deze zombieprocessen te behouden. Daarom, wanneer een ouder vertrekt, alle resterende zombieprocessen gekoppeld aan deze ouder zijn verwijderd. Zombie-processen zijn: werkelijk alleen nuttig om ouderprocessen te laten vinden dat een kind is beëindigd voordat de ouder wait() heeft genoemd.

Nu wilt u misschien liever enkele veiligheidsmaatregelen kennen, zodat u de vork probleemloos kunt gebruiken.

Eenvoudige regels om de vork te laten werken zoals bedoeld

Ten eerste, als je multithreading kent, gebruik dan alsjeblieft geen programma met threads. Vermijd in het algemeen om meerdere gelijktijdigheidstechnologieën te combineren. fork gaat ervan uit dat het in normale C-programma's werkt, het is alleen bedoeld om één parallelle taak te klonen, niet meer.

Ten tweede, vermijd om bestanden te openen of te openen voordat fork(). Bestanden is een van de enige dingen gedeeld en niet gekloond tussen ouder en kind. Als u 16 bytes in ouder leest, wordt de leescursor 16 bytes naar voren verplaatst beide in de ouder en bij het kind. slechtste, als kind en ouder bytes schrijven naar de hetzelfde bestand tegelijkertijd kunnen de bytes van de ouder zijn gemengd met bytes van het kind!

Voor alle duidelijkheid: buiten STDIN, STDOUT en STDERR wil je echt geen geopende bestanden delen met klonen.

Ten derde, wees voorzichtig met stopcontacten. stopcontacten zijn ook gedeeld tussen ouder en kind. Het is handig om naar een poort te luisteren en vervolgens meerdere onderliggende werkers klaar te hebben om een ​​nieuwe clientverbinding af te handelen. Echter, als je het verkeerd gebruikt, kom je in de problemen.

Ten vierde, als je fork() binnen een lus wilt aanroepen, doe dit dan met uiterste zorg. Laten we deze code nemen:

/* COMPILEER DIT NIET */
const int targetFork = 4;
pid_t forkResultaat
 
voor (int i = 0; i < targetFork; i++)
forkResult = fork();
/*… */
 

Als je de code leest, zou je verwachten dat er 4 kinderen worden gemaakt. Maar het zal eerder creëren 16 kinderen. Het is omdat kinderen dat willen ook voer de lus uit en dus zullen childs op hun beurt fork() aanroepen. Als de lus oneindig is, wordt deze a . genoemd vork bom en is een van de manieren om een ​​Linux-systeem te vertragen zo erg dat het niet meer werkt en zal opnieuw moeten worden opgestart. Houd er in een notendop rekening mee dat Clone Wars niet alleen gevaarlijk is in Star Wars!

Nu heb je gezien hoe een eenvoudige lus fout kan gaan, hoe je lussen gebruikt met fork()? Als je een lus nodig hebt, controleer dan altijd de retourwaarde van de fork:

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

Conclusie

Nu is het tijd om je eigen experimenten te doen met fork()! Probeer nieuwe manieren om de tijd te optimaliseren door taken over meerdere CPU-kernen uit te voeren of wat achtergrondverwerking uit te voeren terwijl u wacht op het lezen van een bestand!

Aarzel niet om de handleidingen te lezen via het man-commando. Je leert hoe fork() precies werkt, welke fouten je kunt krijgen, etc. En geniet van gelijktijdigheid!

Beste gameconsole-emulators voor Linux
Dit artikel bevat een lijst van populaire emulatiesoftware voor gameconsoles die beschikbaar is voor Linux. Emulatie is een softwarecompatibiliteitsla...
Beste Linux-distributies voor gaming in 2021
Het Linux-besturingssysteem heeft een lange weg afgelegd van zijn oorspronkelijke, eenvoudige, servergebaseerde uiterlijk. Dit besturingssysteem is de...
Hoe u uw gamesessie op Linux kunt vastleggen en streamen
In het verleden werd het spelen van games alleen als een hobby beschouwd, maar met de tijd zag de game-industrie een enorme groei in termen van techno...