Запуск процесса

Запуск процесса

Теперь обратим внимание на функции, предназначенные для работы с потоками и процессами. Любой поток может осуществить запуск процесса; единственные налагаемые здесь ограничения вытекают из основных принципов защиты (правила доступа к файлу, ограничения на привилегии и т.д.). По всей вероятности, вам уже доводилось запускать процессы — либо из системного сценария, либо из командного интерпретатора, или из программы от своего имени.

Запуск процесса из командной строки

Например, при запуске процесса из командного интерпретатора вы можете ввести командную строку:

$ program1

Это предписывает командному интерпретатору запустить программу program1 и ждать завершения ее работы. Или, вы могли набрать:

$ program2 &

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

Если вы пожелаете скорректировать приоритет программы до ее запуска, вы можете применить команду nice — точно так же, как в UNIX:

$ nice program3

Это предписывает командному интерпретатору запустить программу program3 с заниженным приоритетом.

Или нет?

Если посмотреть, что происходит в действительности, то мы велели командному интерпретатору выполнить программу, называемую nice, с обычным приоритетом. Команда nice затем занизила свой собственный приоритет (отсюда и имя программы «nice» — «благовоспитанная») и затем запустила программу program3 с этим заниженным приоритетом.

Запуск процесса из программы

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

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

Рассмотрим некоторые из функций, которые QNX/Neutrino обеспечивает для запуска других процессов (или подмены одного процесса другим):

• system();

• семейство функций exec();

• семейство функций spawn();

• fork();

• vfork().

Какую из этих функций применять, зависит от двух требований: переносимости и функциональности. Как обычно, между этими двумя требованиями возможен компромисс.

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

Запуск процесса с помощью вызова функции system()

Функция system() — самая простая функция; она получает на вход одну командную строку, такую же, которую вы набрали бы в ответ на подсказку командного интерпретатора, и выполняет ее.

Фактически, для обработки команды, которую вы желаете выполнить, функция system() запускает копию командного интерпретатора.

Редактор, который я использую при написании данной книги, использует вызов system(). При редактировании мне может понадобиться выйти в командный интерпретатор, проверить некоторые фрагменты программ, и затем снова вернуться в редактор. Все это необходимо сделать, не потеряв позицию курсора в тексте. В этом редакторе я, к примеру, могу дать команду «:!pwd» для отображения текущего рабочего каталога. Редактор при этом выполнит следующий код:

system("pwd");

Подходит ли функция system() для всех дел в Поднебесной? Конечно же, нет. Однако, ее применение может быть очень полезно для множества задач, требующих создания процессов.

Запуск процесса с помощью вызова функций exec() и spawn()

Давайте рассмотрим ряд других функций создания процессов.

Следующие функции создания процессов, которые следует рассмотреть, принадлежат к семействам exec() и spawn(). Прежде, чем мы обратимся к подробностям их применения, рассмотрим суть различий между этими двумя группами функций.

Семейство функций exec() подменяет текущий процесс другим. Я подразумеваю под этим то, что когда процесс вызывает функцию семейства exec(), этот процесс прекращает выполнение текущей программы и начинает выполнять другую. Идентификатор процесса (PID) при этом не меняется, просто процесс преобразуется в другую программу. Что произойдет с потоками в данном процессе? Мы вернемся к этой теме после того, как рассмотрим функцию fork().

С другой стороны, семейство функций spawn() так не делает. Вызов функции семейства spawn() создает другой процесс (с новым идентификатором), который соответствует программе, указанной в аргументах функции.

Spawn POSIX Exec POSIX spawn() Да spawnl() Нет execl() Да spawnle() Нет execle() Да spawnlp() Нет execlp() Да spawnlpe() Нет execlpe() Нет spawnp() Да spawnv() Нет execv() Да spawnve() Нет execve() Да spawnvp() Нет execvp() Да spawnvpe() Нет execvpe() Нет

Рассмотрим различные варианты функций exec() и spawn(). В таблице, представленной ниже, вы увидите, что некоторые функции из них предусмотрены POSIX, а некоторые — нет. Конечно, для максимальной переносимости, следует использовать только POSIX-совместимые функции.

При том, что названия функций могут показаться малопонятными, в их суффиксах есть логика.

Суффикс: Смысл: l (нижний регистр «L») Список аргументов определяется через список параметров, заданный непосредственно в самом вызове и завершаемый нулевым аргументом NULL. е Указывается окружение. p Если не указано полное имя пути программы, для ее поиска используется переменная окружения PATH. v Список аргументов определяется через указатель на вектор (массив) аргументов.

Список аргументов здесь — список аргументов командной строки, передаваемых программе.

Заметьте, что в библиотеке языка Си функции spawnlp(), spawnvp() и spawnlpe() все вызывают функцию spawnvpe(), которая, в свою очередь, вызывает POSIX-функцию spawnp(). Функции spawnle(), spawnv() и spawnl() все в конечном счете вызывают функцию spawnve(), которая затем вызывает POSIX-функцию spawn(). И, наконец, POSIX-функция spawnp() вызывает POSIX-функцию spawn(). Таким образом, в основе всех возможностей семейства spawn() лежит сам вызов spawn().

Рассмотрим теперь различные варианты функций spawn() и exec() более подробно так, чтобы вы смогли получить навык свободного использования различных суффиксов. Затем мы перейдем непосредственно к рассмотрению вызова функции spawn().

Суффикс «l»

Например, если я хочу вызвать команду ls с аргументами -t, -r, и -l (означает — «сортировать выходные данные по времени в обратном порядке и показывать выходные данные в длинном формате»), я мог бы определить это в программе так:

/* Вызвать ls и продолжить выполнение */

spawnl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",

 NULL);

/* Заменить себя на ls */

execl(P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l",

 NULL);

Или, вариант с применением суффикса v:

char *argv[] = {

 "/bin/ls",

 "-t",

 "-r",

 "-l",

 NULL

};

/* Вызвать ls и продолжить выполнение */

spawnv(P_WAIT, "/bin/ls", argv);

/* Заменить себя на ls */

execv(P_WAIT, "/bin/ls", "/bin/ls", argv);

Почему именно такой выбор? Он дан для удобства восприятия. У вас может быть синтаксический анализатор, уже встроенный в вашу программу, и может быть удобно сразу оперировать массивами строк. В этом случае я бы рекомендовал применять варианты с суффиксом «v». Или вам может понадобиться запрограммировать вызов программы, когда вам известно, где он находится и какие имеет параметры. В этом случае, зачем вам утруждать себя созданием массива строк, когда вы знаете точно, какие нужны аргументы? Просто передайте их варианту функции с суффиксом «l».

Отметим, что мы передаем реальное имя пути программы (/bin/ls), а затем имя программы еще раз в качестве первого аргумента. Это делается для поддержки программ, которые ведут себя по-разному в зависимости от того, под каким именем они были вызваны.

Например, GNU-утилиты компрессии и декомпрессии (gzip и gunzip) фактически привязаны к одному и тому же исполняемому модулю. Когда исполняемый модуль стартует, он анализирует аргумент argv[0] (передаваемый функции main()) и принимает решение, следует ли выполнять компрессию или декомпрессию.

Суффикс «е»

Варианты с суффиксом «е» передают программе окружение. Окружение — это только своего рода «контекст», в котором работает программа. Например, у вас может быть программа проверки орфографии, у которой есть эталонный словарь. Вместо описания каждый раз в командной строке местоположения словаря вы могли бы сделать это в окружении:

$ export DICTIONARY=/home/rk/.dict

$ spellcheck document.1

Команда export предписывает командному интерпретатору создать новую переменную окружения (в нашем случае DICTIONARY) и присвоить ей значение (/home/rk/.dict).

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

$ export DICTIONARY=/home/rk1.altdict

$ spellcheck document.1

Но как сделать это из ваших собственных программ? Для того чтобы применять «e»-версии функций spawn() и exec(), вам следует определить массив строк, представляющих собой окружение:

char *env[] = {

 "DICTIONARY=/home/rk/.altdict",

 NULL

};

// Запуск проверки в отдельном процессе:

spawnle(P_WAIT, "/usr/bin/spellcheck",

 "/usr/bin/spellcheck", "documents.1", NULL, env);

// Запуск проверки вместо себя:

execle("/usr/bin/spellcheck", "/usr/bin/spellcheck",

 "document.1", NULL, env);

Суффикс «p»

Версии с суффиксом «p» будут искать исполняемый модуль программы в списке каталогов, приведенном в переменной окружения PATH. Вы, вероятно, отметили, что во всех примерах местоположение исполняемых модулей строго определено — /bin/ls и /usr/bin/spellcheck. А как быть с другими исполняемыми модулями? Если вы не хотите сразу определить точный путь к нужной программе, было бы лучше сделать так, чтобы места поиска исполняемых модулей вашей программе сообщил пользователь. Стандартная системная переменная PATH для этого и предназначена. Ниже приведено ее значение для минимальной системы:

PATH=/proc/boot:/bin

Это сообщает командному интерпретатору, что когда я набираю команду, он в первую очередь должен просмотреть каталог /proc/boot/, и если не сможет найти команду там, то должна просмотреть каталог бинарных файлов /bin. Переменная PATH представляет собой разделяемый двоеточиями список каталогов для поиска команд. К переменной PATH вы можете добавлять столько элементов, сколько пожелаете, но имейте в виду, что при поиске файла будут проанализированы все элементы (в приведенной последовательности).

Если вы не знаете путь к выполнимой программе, вы можете использовать варианты с суффиксом «p».

Например:

// Использование явного пути:

execl("/bin/ls", "/bin/ls", "-l", "-t", "-r", NULL);

// Поиск пути в PATH:

execp("ls", "ls", "-l", "-t", "-r", NULL) ;

Если функция execl() не сможет найти ls в /bin, она завершится с ошибкой. Функция execlp() просмотрит все каталоги, указанные в PATH, в поисках ls, и завершится с ошибкой только в том случае, если не сможет найти ls ни в одном из этих каталогов. Это также прекрасная вещь для многоплатформенной поддержки — вашей программе не обязательно знать имена каталогов, принятых на разных машинах, она просто выполнит поиск.

А что произойдет, если сделать так?

execlp("/bin/ls", "ls", "-l", "-t", "-r", NULL);

Выполняет ли этот вызов поиск в окружении? Нет. Вы явно указали execlp() имя пути, что отменяет правило поиска в PATH. Если ls не будет найдена в /bin (а здесь это будет именно так), то никаких других попыток поиска не выполняется — эта ситуация подобна варианту с использованием функции execl()).

Опасно ли смешивать явный путь с простым именем команды (например, указывать путь как /bin/ls, а имя — как ls вместо /bin/ls)? Обычно нет, потому что:

• значительное число программ так или иначе игнорирует argv[0];

• те программы, поведение которых зависит от их имени, обычно вызывают функцию basename(), которая удаляет каталоговую часть argv[0] и возвращает только имя.

Единственная обоснованная причина использования полного имени пути в качестве первого параметра заключается в том, что программа может выводить диагностические сообщения, содержащие этот первый параметр, который немедленно укажет вам, откуда она была вызвана. Это может быть важно, если копии программы располагаются в нескольких каталогах из перечисленных в PATH.

Функции семейства spawn() имеют дополнительный параметр; во всех приведенных выше примерах я всегда указывал P_WAIT. Имеются четыре флага, которые вы можете придать функции spawn(), чтобы изменить ее поведение:

P_WAIT Вызывающий процесс (ваша программа) будет блокирован до тех пор, пока вновь созданный процесс не отработает и не завершится. P_NOWAIT Вызывающая программа не будет блокирована на время выполнения вновь созданной. Это позволяет вам запустить программу в фоновом режиме и продолжать выполнение, пока она делает свое дело. P_NOWAITO Аналогично P_NOWAIT за исключением того, что устанавливается флаг SPAWN_NOZOMBIE. Это означает, что вы не должны беспокоить себя вызовом функции waitpid() для очистки кода завершения процесса. P_OVERLAY Этот флаг превращает вызов функции spawn() в соответствующей вызов exec()! Ваша программа преобразуется в указанную программу без изменения идентификатора процесса ID. Вообще-то, если вы хотите сделать именно так, то, наверное, будет более корректно использовать вызов exec(), поскольку это избавит будущих читателей ваших исходных текстов от необходимости искать P_OVERLAY в справочном руководстве по библиотеке языка Си!

Просто spawn()

Как мы упомянули выше, все функции семейства spawn(), в конечном счете, вызывают базовую функцию spawn(). Ниже приведен прототип функции spawn():

#include <spawn.h>

pid_t spawn(const char *path, int fd_count,

 const int fd_map[], const struct inheritance *inherit,

 char* const argv[], char* const envp[]);

Мы можем не обращать внимание на параметры path, argv, и envp — мы уже рассмотрели их выше как местоположение исполняемого модуля (path), вектор параметров (argv) и окружение (envp).

Параметры fd_count и fd_map идут вместе. Если вы задаете нуль в fd_count, тогда fd_map игнорируется, и это означает, что вновь создаваемый процесс унаследует от родительского все дескрипторы файлов (кроме тех, которые модифицированы флагом FD_CLOEXEC функции fcntl()). Если параметр fd_count имеет ненулевое значение, то он задает число дескрипторов файлов, содержащихся в fd_map, и будут унаследованы только они.

Параметр inherit — это указатель на структуру, которая содержит набор флагов, маски сигналов, и т.д. Для получения более подробной информации об этом вам следует обратиться за помощью к справочному руководству по библиотеке языка Си.

Запуск процесса при помощи функции fork()

Предположим, что вы решили создать новый процесс, который был бы идентичен работающему в настоящее время процессу, и сделать это так, чтобы эти два процесса выполнялись одновременно. Вы могли бы решить эту проблему с помощью функции spawn() (и параметра P_NOWAIT), передав вновь создаваемому процессу достаточно информации о точном состоянии вашего процесса, чтобы новый процесс мог настроить себя сам. Однако, такой подход может оказаться чрезвычайно сложным, потому что описание «текущего состояния» процесса может потребовать большого количества данных.

Существует более простой способ — применение функции fork() которая просто копирует текущий процесс. У результирующего процесса как код, так и данные полностью совпадают с таковыми для родительского процесса.

Конечно же, невозможно создать процесс, который во всем был бы идентичен родительскому. Почему? Наиболее очевидное различие между этими двумя процессами должно быть в идентификаторе процесса — мы не можем создать два процесса с одним и тем же идентификатором. Если вы посмотрите документацию на функцию fork() в справочном руководстве по библиотеке Си, вы увидите, что между этими двумя процессами будет иметь место ряд различий. Внимательно изучите этот список, чтоб быть уверенным в корректном применении функции fork().

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

int main (int argc, char **argv) {

 int retval;

 printf("Это определенно родительский процесс ");

 fflush(stdout);

 retval = fork();

 printf("Кто это сказал? ");

 return (EXIT_SUCCESS);

}

После вызова fork() оба процесса выполнят второй вызов printf()! Если вы запустите эту программу на выполнение, она выведет на экран примерно следующее:

Это определенно родительский процесс

Кто это сказал?

Кто это сказал?

Иными словами, оба процесса выведут вторую строку.

Существует только один способ различить эти два процесса — он заключается в использовании возвращаемого функцией fork() значения, размещенного в retval. Во вновь созданном дочернем процессе retval будет иметь нулевое значение, а в родительском она будет содержать идентификатор дочернего.

Китайская грамота, да? Проясним этот момент еще одним фрагментом программы:

printf("PID родителя равен %d ", getpid());

fflush(stdout);

if (child_pid = fork()) {

 printf("Это родитель, PID сына %d ", child_pid);

} else {

 printf("Это сын, PID %d ", getpid());

}

Эта программа выведет на экран примерно следующее:

PID родителя равен 4496

Это родитель, PID сына 8197

Это сын, PID 8197

Таким образом, после применения функции fork() вы можете определить, в каком процессе находитесь («отец» это или «сын»), анализируя значение, возвращаемое функцией fork().

Запуск процесса с помощью функции vfork()

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

Функция vfork() создает «сына», но затем приостанавливает родительский поток до тех пор, пока «сын» не вызовет функцию exec() или не завершится (с помощью exit() или его друзей). В дополнение к этому, функция vfork() будет работать в системах с физической моделью памяти, в то время как функция fork() не сможет, потому что нуждается в создании такого же адресного пространства, а это в физической модели памяти просто невозможно.

Создание процесса и потоки

Предположим, что у вас есть процесс, и вы еще не создали никаких потоков (т.е., вы работаете с одним потоком — тем, который вызвал функцию main()). Если вызвать функцию fork(), то будет создан другой процесс, и тоже с одним потоком.

Это был простейший пример.

Теперь предположим, что в вашем процессе вы вызвали pthread_create() для создания другого потока. Если вы теперь вызовете функцию fork(), она возвратит ENOSYS (что означает, что функция не поддерживается)! Почему так?

Вы можете верить этому или нет, но это POSIX-совместимая ситуация. POSIX утверждает, что функция fork() может возвращать ENOSYS. На самом же деле происходит вот что: Си-библиотека QNX/Neutrino не рассчитана на ветвление процесса с потоками. Когда вы вызываете pthread_create(), эта функция устанавливает флаг, сигнализирующий что-то типа «не позволяйте этому процессу применять fork(), потому что механизм ветвления в данном случае не определен». Затем, при вызове fork(), этот флаг проверяется и, если он установлен, это принуждает fork() возвратить значение ENOSYS.

Такая реализация была сделана преднамеренно, и причина этого кроется в потоках и мутексах. Если бы этого ограничения не было (и оно может быть снято в будущих версиях QNX/ Neutrino), то вновь созданный процесс, как и предполагается, имел бы то же самое число потоков, что и исходный. Однако, тут возникает сложность, потому что некоторые из исходных потоков могут являться владельцами мутексов. Поскольку вновь создаваемый процесс имеет ту же область данных, что и исходный, библиотека должна была бы отслеживать, какие мутексы принадлежат каким потокам в исходном процессе, и затем дублировать принадлежность мутексов в новом процессе. Это не является невозможным: есть функция, называемая pthread_atfork(), которая обеспечивает процессу возможность обрабатывать такие ситуации. Однако, на момент написания этой книги функциональные возможности pthread_atfork() используются не всеми мутексами в Си-библиотеке QNX/Neutrino.

Так что же использовать?

Очевидно, если вы переносите в QNX/Neutrino программу из другой ОС, вы пожелаете использовать те же механизмы, что и в исходной программе. Я бы посоветовал избегать в новом коде применения функции fork(), и вот почему:

• функция fork() не работает с несколькими потоками — см. выше;

• при работе с fork() в условиях многопоточности вы должны будете зарегистрировать обработчик pthread_atfork() и локировать каждый мутекс по отдельности перед собственно ветвлением, а это усложнит структуру программы;

• дочерние процессы, созданные fork(), копируют все открытые дескрипторы файлов. Как мы увидим позже в главе «Администратор ресурсов», это требует много дополнительных усилий, которые может быть совершенно напрасными, если дочерний процесс затем сразу сделает exec() и тем самым закроет все открытые дескрипторы.

Выбор между семействами функций vfork() и spawn() сводится к переносимости, а также того, что должны делать родительский и дочерний процесс. Функция vfork() задержит выполнение до тех пор, пока дочерний процесс не вызовет exec() или не завершится, тогда как семейство spawn() может позволить работать обоим процессам одновременно. Впрочем, в разных ОС поведение функции vfork() может несколько отличаться.