C Програмиране

Вашата първа програма C, използваща Fork System Call

Вашата първа програма C, използваща Fork System Call
По подразбиране програмите C нямат паралелизъм или паралелизъм, изпълнява се само по една задача наведнъж, всеки ред код се чете последователно. Но понякога трябва да прочетете файл или - дори най-лошото - гнездо, свързано към отдалечен компютър и това отнема наистина много време за компютър. Обикновено отнема по-малко от секунда, но не забравяйте, че едно ядро ​​на процесора може изпълняват 1 или 2 милиарда инструкции през това време.

Така, като добър разработчик, ще се изкушите да инструктирате вашата програма C да направи нещо по-полезно, докато чака. Ето къде е тук програмирането на паралелността за вашето спасяване - и прави компютъра ви нещастен, защото трябва да работи повече.

Тук ще ви покажа системното повикване на Linux fork, един от най-безопасните начини за едновременно програмиране.

Едновременното програмиране може да бъде опасно?

Да, може. Например има и друг начин за обаждане многопоточност. Предимството е, че е по-лек, но може наистина ли сбъркайте, ако го използвате неправилно. Ако вашата програма по погрешка чете променлива и пише в същата променлива в същото време вашата програма ще стане несъгласувана и е почти неоткриваема - един от най-лошите кошмари на разработчика.

Както ще видите по-долу, fork копира паметта, така че не е възможно да имате такива проблеми с променливите. Също така, fork прави независим процес за всяка едновременна задача. Поради тези мерки за сигурност е приблизително 5 пъти по-бавно да стартирате нова едновременна задача с помощта на вилица, отколкото с многопоточност. Както можете да видите, това не е много за ползите, които носи.

Сега, достатъчно обяснения, е време да тествате първата си програма на C, като използвате fork call.

Пример за вилицата на Linux

Ето кода:

#include
#include
#include
#include
#include
int main ()
pid_t forkStatus;
forkStatus = fork ();
/ * Дете ... * /
ако (forkStatus == 0)
printf ("Детето работи, обработва.\н");
сън (5);
printf ("Детето е готово, излиза.\н");
/ * Родител ... * /
иначе ако (forkStatus != -1)
printf ("Родителят чака ... \ n");
изчакайте (NULL);
printf ("Родителят излиза ... \ n");
друго
perror ("Грешка при извикване на функцията за разклоняване");

връщане 0;

Каня ви да тествате, компилирате и изпълните кода по-горе, но ако искате да видите как ще изглежда изходът и сте твърде „мързеливи“, за да го компилирате - в края на краищата вие сте може би уморен разработчик, който компилира C програми по цял ден - можете да намерите изхода на програмата C по-долу заедно с командата, която използвах, за да я компилирам:

$ gcc -std = c89 -Wpedantic -Wall forkSleep.c -o forkSleep -O2
$ ./ forkSleep
Родителят чака ..
Детето работи, обработва.
Детето е готово, излиза.
Родителят излиза ..

Моля, не се страхувайте, ако изходът не е 100% идентичен с моя изход по-горе. Не забравяйте, че едновременното изпълнение на нещата означава, че задачите се изчерпват, няма предварително дефинирано подреждане. В този пример може да видите, че детето работи преди родител чака и няма нищо лошо в това. Като цяло подреждането зависи от версията на ядрото, броя на ядрата на процесора, програмите, които в момента се изпълняват на вашия компютър и т.н.

Добре, сега се върнете към кода. Преди реда с fork (), тази програма C е напълно нормална: 1 ред се изпълнява наведнъж, има само един процес за тази програма (ако е имало малко закъснение преди fork, можете да потвърдите това във вашия диспечер на задачите).

След fork (), сега има 2 процеса, които могат да работят паралелно. Първо, има детски процес. Този процес е този, който е създаден при fork (). Този дъщерен процес е специален: не е изпълнил нито един от редовете на кода над реда с fork (). Вместо да търси основната функция, тя по-скоро ще изпълни реда fork ().

Ами променливите, декларирани преди fork?

Е, Linux fork () е интересен, защото умно отговаря на този въпрос. Променливите и всъщност цялата памет в C програмите се копират в дъщерния процес.

Позволете ми да дефинирам това, което прави вилица с няколко думи: тя създава a клон на процеса, който го извиква. Двата процеса са почти идентични: всички променливи ще съдържат едни и същи стойности и двата процеса ще изпълнят реда непосредствено след fork (). След процеса на клониране обаче, те са разделени. Ако актуализирате променлива в единия процес, другия процес няма актуализирайте нейната променлива. Това наистина е клонинг, копие, процесите не споделят почти нищо. Наистина е полезно: можете да подготвите много данни и след това да fork () и да използвате тези данни във всички клонинги.

Разделянето започва, когато fork () връща стойност. Оригиналният процес (той се нарича родителския процес) ще получи идентификатора на процеса на клонирания процес. От другата страна, клонираният процес (този се нарича детския процес) ще получи числото 0. Сега трябва да започнете да разбирате защо съм сложил if / else if изрази след реда fork (). Използвайки върната стойност, можете да инструктирате детето да направи нещо различно от това, което прави родителят - и повярвайте ми, полезно е.

От едната страна, в примерния код по-горе, детето изпълнява задача, която отнема 5 секунди и отпечатва съобщение. За да имитирам процес, който отнема много време, използвам функцията за сън. След това детето излиза успешно.

От другата страна родителят отпечатва съобщение, изчаква докато детето излезе и накрая отпечатва друго съобщение. Фактът, че родителят чака детето си е важен. Като пример, родителят чака по-голямата част от това време да изчака детето си. Но можех да наредя на родителя да прави всякакви дългосрочни задачи, преди да му кажа да изчака. По този начин би направил полезни задачи, вместо да чака - в края на краищата, затова използваме вилица (), не?

Както обаче казах по-горе, наистина е важно това родителят чака своите деца. И е важно заради зомби процеси.

Как е важно чакането

Родителите обикновено искат да знаят дали децата са завършили обработката си. Например искате да изпълнявате задачи паралелно, но със сигурност не искате родителят да излезе, преди да завършат деца, защото ако това се случи, черупката ще даде подкана, докато децата все още не са завършили - което е странно.

Функцията за изчакване позволява да се изчака, докато един от дъщерните процеси бъде прекратен. Ако родител извика 10 пъти fork (), ще трябва да извика и 10 пъти wait (), веднъж за всяко дете създаден.

Но какво се случва, ако родителските повиквания изчакват, докато всички деца имат вече излезе? Там са необходими зомби процеси.

Когато дете излезе преди родителските повиквания wait (), ядрото на Linux ще позволи на детето да излезе но ще запази билет казвайки, че детето е излязло. След това, когато родителят извика wait (), той ще намери билета, изтрие този билет и функцията wait () ще се върне веднага защото знае, че родителят трябва да знае кога детето е завършило. Този билет се нарича a зомби процес.

Ето защо е важно родителските повиквания да изчакат (): ако не го направи, зомби процесите остават в паметта и ядрото на Linux не може запази много зомби процеси в паметта. След достигане на лимита, вашият компютър iне може да създаде нов процес и така ще бъдете в много лоша форма: дори за убиване на процес може да се наложи да създадете нов процес за това. Например, ако искате да отворите диспечера на задачите си, за да убиете процес, не можете, защото вашият диспечер на задачи ще се нуждае от нов процес. Дори най-лошото, не можеш убий зомби процес.

Ето защо извикването на изчакване е важно: то позволява ядрото почисти процесът дете, вместо да продължава да се трупа със списък на прекратени процеси. И какво, ако родителят излезе, без никога да се обади изчакайте()?

За щастие, тъй като родителят е прекратен, никой друг не може да се обади на wait () за тези деца, така че има без причина за да запазите тези зомби процеси. Следователно, когато родител излезе, всички останали зомби процеси свързана с този родител се премахват. Зомби процесите са наистина ли полезно само за разрешаване на родителски процеси да открият, че дете е прекратено преди родител, наречен wait ().

Сега може да предпочетете да знаете някои мерки за безопасност, които да ви позволят най-доброто използване на вилицата без никакъв проблем.

Прости правила, за да работи вилицата по предназначение

Първо, ако знаете многопоточност, моля, не разклонявайте програма, използваща нишки. Всъщност избягвайте като цяло да смесвате множество едновременни технологии. fork предполага, че работи в нормални C програми, той възнамерява да клонира само една паралелна задача, не повече.

Второ, избягвайте да отваряте или отваряте файлове преди fork (). Файловете са единственото нещо споделени и не клониран между родител и дете. Ако прочетете 16 байта в родител, това ще премести курсора за четене напред от 16 байта и двете в родителя и в детето. Най-лошото, ако дете и родител пишат байтове в един и същ файл в същото време байтовете на родител могат да бъдат смесени с байтове на детето!

За да бъдем ясни, извън STDIN, STDOUT и STDERR, наистина не искате да споделяте отворени файлове с клонинги.

Трето, бъдете внимателни с гнездата. Гнездата са също споделено между родители и деца. Полезно е, за да слушате порт и след това да имате няколко деца, готови да се справят с нова клиентска връзка. въпреки това, ако го използвате погрешно, ще си навлечете неприятности.

Четвърто, ако искате да извикате fork () в рамките на цикъл, направете това с изключително внимание. Нека вземем този код:

/ * НЕ СЪСТАВЯЙТЕ ТОВА * /
const int targetFork = 4;
pid_t fork Резултат
 
за (int i = 0; i < targetFork; i++)
forkResult = fork ();
/ * ... * /
 

Ако прочетете кода, може да очаквате да създаде 4 деца. Но по-скоро ще създаде 16 деца. Това е така, защото децата ще го направят също изпълнете цикъла и така чейлдовете от своя страна ще извикат fork (). Когато цикълът е безкраен, той се нарича a вилка бомба и е един от начините за забавяне на Linux система толкова много, че вече не работи и ще се нуждае от рестартиране. Накратко, имайте предвид, че Войните на клонинги не са опасни само в Междузвездни войни!

Сега видяхте как един обикновен цикъл може да се обърка, как да използвате цикли с fork ()? Ако имате нужда от цикъл, винаги проверявайте връщаната стойност на fork:

const int targetFork = 4;
pid_t forkResult;
int i = 0;
правя
forkResult = fork ();
/ * ... * /
i ++;
докато ((forkResult != 0 && forkResult != -1) && (т.е < targetFork));

Заключение

Сега е време да направите свои собствени експерименти с fork ()! Опитайте нови начини за оптимизиране на времето, като изпълнявате задачи в множество ядра на процесора или извършете някаква фонова обработка, докато чакате да прочетете файл!

Не се колебайте да прочетете страниците с ръководството чрез командата man. Ще научите как точно работи fork (), какви грешки можете да получите и т.н. И се наслаждавайте на едновременността!

Бутонът на левия бутон на мишката не работи в Windows 10
Ако използвате специална мишка с вашия лаптоп или настолен компютър, но бутонът на левия бутон на мишката не работи на Windows 10/8/7 по някаква причи...
Курсорът скача или се движи произволно, докато пишете в Windows 10
Ако установите, че курсорът на вашата мишка скача или се движи самостоятелно, автоматично, произволно, докато пишете в лаптоп или компютър на Windows,...
Как да обърнете посоката на превъртане на мишката и тъчпада в Windows 10
Мишка и Тъчпадs не само правят изчисленията лесни, но и по-ефективни и отнемат по-малко време. Не можем да си представим живот без тези устройства, но...