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

Ваша первая программа на C, использующая системный вызов Fork

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

Так, как хороший разработчик, у вас возникнет соблазн указать вашей программе на C делать что-то более полезное во время ожидания. Вот где вам на помощь приходит параллельное программирование - и делает ваш компьютер недовольным, потому что он должен работать больше.

Здесь я покажу вам системный вызов форка Linux, один из самых безопасных способов параллельного программирования.

Параллельное программирование может быть небезопасным?

Да, оно может. Например, есть еще один способ вызова многопоточность. У него есть преимущество в том, что он легче, но он может В самом деле пойти не так, если вы используете его неправильно. Если ваша программа по ошибке считывает переменную и записывает в та же переменная в то же время ваша программа станет бессвязной и ее почти невозможно будет обнаружить - один из худших кошмаров разработчиков.

Как вы увидите ниже, fork копирует память, поэтому таких проблем с переменными быть не может. Кроме того, fork делает независимый процесс для каждой параллельной задачи. Из-за этих мер безопасности запуск новой параллельной задачи с помощью fork примерно в 5 раз медленнее, чем с многопоточностью. Как видите, это не так много для тех преимуществ, которые он приносит.

Теперь, достаточно объяснений, пора протестировать вашу первую программу на C с помощью вызова fork.

Пример форка Linux

Вот код:

#включать
#включать
#включать
#включать
#включать
int main ()
pid_t forkStatus;
forkStatus = fork ();
/* Ребенок… */
if (forkStatus == 0)
printf ("Ребенок запущен, обрабатывает.\ n ");
сон (5);
printf ("Ребенок готов, выход.\ n ");
/ * Родитель… * /
else if (forkStatus != -1)
printf («Родитель ждет… \ n»);
ждать (NULL);
printf ("Родитель завершает работу… \ n");
еще
perror («Ошибка при вызове функции fork»);

возврат 0;

Я приглашаю вас протестировать, скомпилировать и выполнить приведенный выше код, но если вы хотите увидеть, как будет выглядеть результат, и вы слишком «ленивы», чтобы его компилировать - в конце концов, вы, может быть, усталый разработчик, который целый день компилировал программы на C - вы можете найти вывод программы на C ниже вместе с командой, которую я использовал для ее компиляции:

$ gcc -std = c89 -Wpedantic -Wall forkSleep.c -o forkSleep -O2
$ ./ forkSleep
Родитель ждет…
Ребенок бежит, обрабатывает.
Ребенок готов, уходит.
Родитель покидает…

Пожалуйста, не бойтесь, если результат не на 100% идентичен моему выводу выше. Помните, что одновременное выполнение задач означает, что задачи выполняются вне очереди, предопределенного порядка нет. В этом примере вы можете увидеть, что ребенок работает перед родитель ждет, и в этом нет ничего плохого. Как правило, порядок зависит от версии ядра, количества ядер ЦП, программ, запущенных в данный момент на вашем компьютере, и т. Д.

Хорошо, теперь вернемся к коду. Перед строкой с fork () эта программа на C совершенно нормальная: за раз выполняется 1 строка, для этой программы есть только один процесс (если была небольшая задержка перед fork, вы могли бы подтвердить это в своем диспетчере задач).

После fork () теперь есть 2 процесса, которые могут работать параллельно. Во-первых, есть дочерний процесс. Это тот процесс, который был создан при fork (). Этот дочерний процесс особенный: он не выполнил ни одной строки кода над строкой с fork (). Вместо того, чтобы искать основную функцию, она скорее выполнит строку fork ().

А как насчет переменных, объявленных перед форком?

Что ж, Linux fork () интересен тем, что умно отвечает на этот вопрос. Переменные и, собственно, вся память в программах на C копируется в дочерний процесс.

Позвольте мне в двух словах определить, что делает fork: он создает клон процесса, вызывающего это. Два процесса почти идентичны: все переменные будут содержать одинаковые значения, и оба процесса выполнят строку сразу после fork (). Однако после процесса клонирования, они разделены. Если вы обновляете переменную в одном процессе, другой процесс не будет обновить свою переменную. Это действительно клон, копия, процессы почти ничего не разделяют. Это действительно полезно: вы можете подготовить много данных, а затем выполнить fork () и использовать эти данные во всех клонах.

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

С одной стороны, в приведенном выше примере кода ребенок выполняет задачу, которая занимает 5 секунд, и печатает сообщение. Чтобы имитировать процесс, который занимает много времени, я использую функцию сна. Затем ребенок успешно уходит.

С другой стороны, родитель печатает сообщение, ждет, пока дочерний элемент не выйдет, и, наконец, печатает другое сообщение. Важно то, что родители ждут своего ребенка. Например, родитель большую часть времени ожидает своего дочернего элемента. Но я мог бы проинструктировать родителя о выполнении любых долгосрочных задач, прежде чем сказать ему подождать. Таким образом, он выполнял бы полезные задачи вместо того, чтобы ждать - в конце концов, именно поэтому мы используем fork (), нет?

Однако, как я сказал выше, очень важно, чтобы родитель ждет своих детей. И это важно из-за зомби процессы.

Как важно ждать

Родители обычно хотят знать, закончили ли дети обработку. Например, вы хотите запускать задачи параллельно, но ты определенно не хочешь родительский элемент должен выйти до того, как дочерние элементы будут выполнены, потому что, если это произойдет, оболочка вернет приглашение, пока дочерние элементы еще не завершены - что странно.

Функция ожидания позволяет дождаться завершения одного из дочерних процессов. Если родитель 10 раз вызывает fork (), ему также нужно будет вызвать 10 раз wait (), один раз для каждого ребенка созданный.

Но что произойдет, если родитель вызовет функцию ожидания, пока все дочерние элементы имеют уже вышел? Вот где нужны зомби-процессы.

Когда дочерний элемент завершает работу до того, как родитель вызывает wait (), ядро ​​Linux позволяет дочернему элементу выйти но он сохранит билет сказать, что ребенок вышел. Затем, когда родитель вызывает wait (), он найдет билет, удалит этот билет, и функция wait () вернет немедленно потому что он знает, что родитель должен знать, когда ребенок закончил. Этот билет называется зомби-процесс.

Вот почему важно, чтобы родительский вызов wait (): если он этого не делает, зомби-процессы остаются в памяти и ядре Linux не могу хранить в памяти много зомби-процессов. По достижении лимита ваш компьютер is не может создать новый процесс и так вы будете в очень плохая форма: четный для того, чтобы убить процесс, вам может потребоваться создать новый процесс для этого. Например, если вы хотите открыть диспетчер задач, чтобы убить процесс, вы не можете, потому что диспетчеру задач потребуется новый процесс. Даже худшее, ты не можешь убить процесс зомби.

Вот почему так важен вызов wait: он позволяет ядру убирать дочерний процесс вместо того, чтобы накапливать список завершенных процессов. А что, если родитель уйдет, даже не позвонив ждать()?

К счастью, когда родительский элемент завершен, никто другой не может вызвать wait () для этих дочерних элементов, поэтому существует нет причин чтобы сохранить эти зомби процессы. Поэтому, когда родитель уходит, все остальное зомби процессы связан с этим родителем удалены. Зомби-процессы В самом деле полезно только для того, чтобы родительские процессы могли обнаружить, что дочерний процесс завершился до того, как родитель вызвал wait ().

Теперь вы можете предпочесть знать некоторые меры безопасности, которые позволят вам наилучшим образом использовать вилку без каких-либо проблем.

Простые правила, чтобы вилка работала должным образом

Во-первых, если вы знакомы с многопоточностью, пожалуйста, не разветвляйте программу с использованием потоков. Фактически, как правило, избегайте смешивания нескольких технологий параллелизма. fork предполагает работу в обычных программах на C, он намеревается клонировать только одну параллельную задачу, не более.

Во-вторых, избегайте открытия или открытия файлов перед fork (). Файлы - это одно из немногих общий и нет клонированный между родителем и ребенком. Если вы прочитаете 16 байт в родительском элементе, он переместит курсор чтения вперед на 16 байтов оба в родительском и в ребенке. Худший, если ребенок и родитель записывают байты в тот же файл в то же время байты родительского элемента могут быть смешанный с байтами ребенка!

Чтобы было ясно, за пределами STDIN, STDOUT и STDERR вы действительно не хотите делиться какими-либо открытыми файлами с клонами.

В-третьих, будьте осторожны с розетками. Розетки также поделился между родителем и детьми. Это полезно для прослушивания порта, а затем для того, чтобы несколько дочерних работников были готовы обработать новое клиентское соединение. тем не мение, если вы воспользуетесь им неправильно, у вас будут проблемы.

В-четвертых, если вы хотите вызвать fork () внутри цикла, сделайте это с помощью крайняя осторожность. Возьмем этот код:

/ * НЕ СОБИРАЙТЕ ЭТО * /
const int targetFork = 4;
pid_t forkResult
 
для (int i = 0; i < targetFork; i++)
forkResult = fork ();
/ *… * /
 

Если вы прочитаете код, вы можете ожидать, что он создаст 4 дочерних элемента. Но скорее создаст 16 детей. Это потому, что дети будут также выполнить цикл, и дочерние элементы, в свою очередь, вызовут fork (). Когда цикл бесконечен, он называется вилка бомба и это один из способов замедлить работу системы Linux настолько, что это больше не работает и потребуется перезагрузка. Короче говоря, имейте в виду, что Войны клонов опасны не только в Звездных войнах!

Теперь вы увидели, как простой цикл может пойти не так, как использовать циклы с fork ()? Если вам нужен цикл, всегда проверяйте возвращаемое значение fork:

const int targetFork = 4;
pid_t forkResult;
int я = 0;
делать
forkResult = fork ();
/ *… * /
i ++;
while ((forkResult != 0 && forkResult != -1) && (i < targetFork));

Заключение

Пришло время провести собственные эксперименты с fork ()! Попробуйте новые способы оптимизации времени, выполняя задачи на нескольких ядрах ЦП или выполняя некоторую фоновую обработку, пока вы ждете чтения файла!

Не стесняйтесь читать справочные страницы с помощью команды man. Вы узнаете, как работает fork (), какие ошибки могут возникнуть и т. Д. И наслаждайтесь параллелизмом!

Как установить Doom и играть в него в Linux
Введение в Doom Серия Doom возникла в 90-х годах после выхода оригинальной Doom. Это мгновенно стал хитом, и с тех пор серия игр получила множество на...
Vulkan для пользователей Linux
С каждым новым поколением видеокарт мы видим, как разработчики игр расширяют границы графической точности и приближаются на шаг ближе к фотореализму. ...
OpenTTD против Simutrans
Создание собственного транспортного симулятора может быть увлекательным, расслабляющим и чрезвычайно увлекательным занятием. Вот почему вам нужно попр...