355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Олег Цилюрик » QNX/UNIX: Анатомия параллелизма » Текст книги (страница 3)
QNX/UNIX: Анатомия параллелизма
  • Текст добавлен: 8 октября 2016, 23:59

Текст книги "QNX/UNIX: Анатомия параллелизма"


Автор книги: Олег Цилюрик


Соавторы: Владимир Зайцев,Егор Горошко
сообщить о нарушении

Текущая страница: 3 (всего у книги 23 страниц) [доступный отрывок для чтения: 9 страниц]

Создание нового процесса

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

Использование командного интерпретатора

Самый простой способ – запустить из программного кода дочернюю копию командного интерпретатора, которому затем передать команду запуска процесса. Для этого используется вызов:

int system(const char* command);

где command– текстовая строка, содержащая команду, которую предполагается выполнить ровно в том виде, в котором мы вводим ее командному интерпретатору с консоли.

Примечание

Функция имеет еще одну специфическую форму вызова, когда в качестве commandзадается NULL. По коду возврата это позволяет выяснить, присутствует ли (и доступен ли) командный интерпретатор в системе (возвращается 0, если интерпретатор доступен).

На время выполнения вызова system()вызывающий процесс приостанавливается. После завершения порожденного процесса функция возвращает код завершения вновь созданной копии интерпретатора (или -1, если сам интерпретатор не может быть выполнен), то есть младшие 8 бит возвращаемого значения содержат код завершения выполняемого процесса. Возврат вызова system()может анализироваться макросом WEXITSTATUS(), определенным в файле . Например:

#include

int main(void) {

 int rc = system(«ls»);

 if (rc == -1) cout << «shell could not be run» << endl;

 else

  cout << "result of running command is " << WEXITSTATUS(rc) << endl;

 return EXIT_SUCCESS;

}

Примечание

Эта функция использует вызов spawnlp()для загрузки новой копии командного интерпретатора, то есть «внутреннее устройство» должно быть в общем виде вам понятно. Особенностью QNX-реализации является то, что spawnlp()всегда использует вызов /bin/sh, независимо от конкретного вида интерпретатора, устанавливаемого переменной окружения SHELL (ksh, bash…). Это обеспечивает независимость поведения родительского приложения от конкретных установок системы, в которой это приложение выполняется.

Вызов system()является не только простым, но и очень наглядным, делающим код легко читаемым. Программисты часто относятся к нему с пренебрежением [10]10
  Здесь многое зависит от расстановки приоритетов. Если вы хотите, чтобы всякий, читающий ваш код, тут же воскликнул: «Ну и крутой же парень написал такое!», заведомо используйте spawn(). При желании сделать код максимально элегантным используйте fork(), а если ставится задача хорошей читаемости и ясности кода, то очень часто достаточно и system().


[Закрыть]
, отмечая множество его недостатков. Однако в относительно простых случаях это может быть оптимальным решением, а недостатки не так и существенны:

• Используя копию командного интерпретатора, вызов system()может инициировать процесс, исполняющий и бинарную программу, и скрипт на языке самого командного интерпретатора (shell), а также программный код на интерпретирующих языках, таких как Perl, Tcl/Tk и др. Многие из рассматриваемых ниже «чисто программных» способов могут загружать и исполнять только бинарный исполняемый код (по крайней мере, без использования ими весьма громоздких искусственных и альтернативных возможностей).

• Остановка родительского процесса в ожидании завершения порожденного также легко разрешается: просто запускайте дочерний процесс из отдельного потока [11]11
  Детали создания потока и и частности передачи ему параметра обстоятельно рассматриваются далее.


[Закрыть]
:

#include

void* process(void* command) {

 system((char*)command);

 delete command;

 return NULL;

}

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

 ...

 char* comstr = «ls -l»;

 pthread_create(NULL, NULL, strdup(comstr), &process);

 ...

}

• Часто в качестве недостатка этого способа отмечают «автономность» и невозможность взаимодействия родительского и порожденного процессов.

Но для расширения возможностей взаимосвязи процессов можно прежде всего воспользоваться вызовом popen()(POSIX 1003.1a), являющимся в некотором роде эквивалентом, расширяющим возможности system(). Возможности popen()часто упускаются из виду, так как в описаниях этот вызов относится не к области создания процессов, а к области программных каналов (pipe). Синтаксис этого вызова таков:

FILE* popen(const char* command, const char* mode);

где command– командная строка, как и у system(); mode– режим создаваемого программного канала со стороны порождающего процесса: ввод ( mode= «r») или вывод ( mode= «w»). Любые другие значения, указанные для mode, дают непредсказуемый результат.

В результате выполнения этой функции создается открытый файловый дескриптор канала (pipe), из которого породивший процесс может ( mode= «r») читать (стандартный поток вывода дочернего процесса STDOUT_FILENO) или в который может ( mode= «w») писать (стандартный поток ввода дочернего процесса STDIN_FILENO) стандартным образом, как это делается для типа FILE (в частности, с отработкой ситуации EOF).

Рассмотрим следующий пример. Конечно, посимвольный ввод/вывод – это не лучшее решение, и здесь мы используем его только для простоты:

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

 FILE* f = popen(«ls -l», "r");

 if (f == NULL) perror(«popen»), exit(EXIT_FAILURE);

 char c;

 while((с = fgetc(f)) != EOF )

  cout << (islower(с) ? toupper(с) : c);

 pclose(f);

 return EXIT_SUCCESS;

}

Примечание

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

spawnlp(P_NOWAIT, shell_command, shell_command, «-с», command, (char*)NULL);

где shell_command– командный интерпретатор, специфицированный переменной окружения SHELL или утилита /bin/sh. В этом кроется причина возможного различия в выполнении вызовов system()и popen().

Если popen()возвращает не NULL, то выполнение прошло успешно. В противном случае устанавливается errno: EINVAL– недопустимый аргумент mode, ENOSYS– в системе не выполняется программа менеджера каналов. После завершения работы с каналом, созданным popen(), он должен быть закрыт парной операцией pclose().

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

Клонирование процесса

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

#include

pid_t fork(void);

Действие вызова fork()следующее:

• Порождается дочерний процесс, которому системой присваивается новое уникальное значение PID.

• Дочерний процесс получает собственные копии файловых дескрипторов, открытых в родительском процессе в точке выполнения fork(). Каждый дескриптор ссылается на тот же файл, который соответствует аналогичному дескриптору родителя. Блокировки файлов (locks), установленные в родительском процессе, наследуются дочерним процессом.

• Для дочернего процесса его значения tms_utime, tms_stime, tms_cutimeи tms_cstimeустанавливаются в значение ноль. Выдержки (alarms) для этих таймеров, установленные к этому времени в родительском процессе, в дочернем процессе очищаются.

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

Если вызов функции завершился неудачно, функция возвращает -1 и устанавливает errno: EAGAIN– недостаточно системных ресурсов; ENOMEM– процессы требуют большее количество памяти, чем доступно в системе; ENOSYS– функция fork()не реализуется в этой модели памяти, например в физической модели адресации памяти (напомним, что QNX – многоплатформенная ОС и число поддерживаемых ею платформ все возрастает).

А вот с кодом возврата этой функции в случае удачи сложнее и гораздо интереснее. Дело в том, что для одного вызова fork()одновременно имеют место два возврата в двух различных копиях (но в текстуально едином коде!): в копии кода, соответствующей дочернему процессу, возвращается 0, а в копии кода родителя – PID успешно порожденного дочернего процесса. Это и позволяет разграничить в едином программном коде фрагменты, которые после точки ветвления надлежит выполнять в родительском процессе и которые относятся к дочернему процессу. Типичный шаблон кода, использующего fork(), выглядит примерно так:

pid_t pid = fork();

if (pid == -1) perror(«fork»), exit(EXIT_FAILURE);

if (pid == 0) {

 // ... этот код выполняется в дочернем процессе ...

 exit(EXIT_SUCCESS);

}

if (pid > 0) {

 // ... этот код выполняется в родительском процессе ...

 do { // ожидание завершения порожденного процесса

  wpid = waitpid(pid, &status, 0);

 } while(WIFEXITED(status) == 0);

 exit(WEXITSTATUS(status));

}

Эта схема порождения процесса, его клонирование, настолько широко употребляется, особенно при построении самых разнообразных серверов, что для нее была создана специальная техника, построенная на вызове fork(). Заметьте, что во всех многозадачных ОС обязательно присутствует та или иная техника программного создания нового процесса, однако не во всех существует техника именно клонирования, то есть создания полного дубликата порождающего процесса.

Вот как выглядит простейший ретранслирующий TCP/IP-сервер, заимствованный из нашей более ранней публикации [4] (обработка ошибок полностью исключена, чтобы упростить пример):

Ретранслирующий TCP/IP-сервер [12]12
  Напоминаем, что листинги, названия которых выделены подобным образом (на сером фоне), представляют собой законченные приложения. Соответствующие файлы можно найти в архивах; они могут быть воспроизведены или модифицированы для тонкого анализа результатов.


[Закрыть]

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

 // создание и подготовка прослушивающего сокета:

 int rc, ls = socket(AF_INET, SOCK_STREAM, 0);

 setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &rc, sizeof(rc));

 struct sockaddr_in addr;

 memset(&addr, 0, sizeof(addr));

 addr.sin_len = sizeof(addr); // специфика QNX

 addr.sin_family = AF_INET;

 addr.sin_port = htons(PORT); // PORT – константа

 addr.sin_addr.s_addr = htonl(INADDR_ANY);

 bind(ls, (struct sockaddr*)&addr, sizeof(sockaddr));

 listen(ls, 25);

 while(true) {

  rc = accept(ls, NULL, NULL);

  pid_t pid = fork();

  if (pid < 0) ...; // что-то произошло!

  if (pid == 0) {

   close(ls);

   char data[MAXLINE];

   int nd = read(rc, &data, MAXLINE);

   if (nd > 0) write(rc, &data, nd);

   close(rs);

   exit(EXIT_SUCCESS);

  }

  else close(rs); // единственное действие родителя

 }

 exit(EXIT_SUCCESS);

}

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

Примечание

Операция fork()должна создать не только структуру адресного пространства нового процесса, но и побайтную копию этой области. В операционных системах общего назначения (Win32, Linux, FreeBSD) для облегчения этого трудоемкого процесса используется виртуализация страниц по технологии COW (copy on write), детально описанная, например, применительно к Win32, Джеффри Рихтером. Накладные расходы процесса копирования здесь демпфированы тем, что копирование каждой физической страницы памяти фактически производится только при записи в эту страницу, то есть затраты на копирование «размазываются» достаточно случайным образом по ходу последующего выполнения дочернего процесса (здесь нет практически никакого итогового выигрыша а производительности, есть только сокрытие от пользователя одноразового размера требуемых затрат).

Системы реального времени не имеют права на такую роскошь: непредсказуемое рассредоточение копирующих операций по всему последующему выполнению, а поэтому и использование в них COW вообще, выглядит весьма сомнительно. В [4] мы описывали эксперименты в QNX, когда в код сервера, построенного на fork(), была внесена «пассивная» строка, никак не используемая в программе, но определяющая весьма протяженную инициализированную область данных:

static long MEM[2500000];

При этом время реакции (ответа) сервера (затраты времени на выполнение fork()) возросло в 50 раз и составило 0,12 сек на процессоре 400 МГц. Еще раз, но в другом контексте эта особенность будет обсуждена ниже при сравнении затрат производительности на создание процессов и потоков.

Дополнительным вызовом этого класса (для полноты обзора) является использование функции:

pid_t vfork(void);

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

Функция vfork()может быть реализована на аппаратных платформах с физической моделью памяти (без виртуализации памяти), a fork()– не может (или реализуется с большими накладными расходами), так как требует создания абсолютной копии области адресного пространства, что в физической модели повлечет сложную динамическую модификацию адресных полей кода. Тем не менее (при некоторых кажущихся достоинствах) в BSD также подчеркивалось, что vfork()таит в себе серьезную потенциальную опасность, поскольку позволяет одному процессу использовать или даже модифицировать адресное пространство другого, то есть фактически нарушает парадигму защищенных адресных пространств.

Запуск нового программного кода

Наконец, рассмотрим запуск на выполнение нового, отличного от родительского процесса программного кода, образ которого содержится в отдельном исполняемом файле в качестве дочернего процесса. Для этой цели в QNX существуют две группы функций: exec()(их всего 8: execl(), execle(), execlp(), execlpe(), execv(), execve(), execvp(), execvpe()) и spawn()(их 10: spawn(), spawnl(), spawnle(), spawnlp(), spawnlpe(), spawnp(), spawnv(), spawnve(), spawnvp(), spawnvpe()).

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

•  l– список аргументов определяется через список параметров, заданных непосредственно в самом вызове. Этот список завершается нулевым аргументом NULL;

•  e– окружение для процесса указывается посредством определения массива переменных окружения;

•  p– относительный путь поиска: если не указан полный путь к файлу программы (то есть имя файла не содержит разделителей « /»), для его поиска используется переменная окружения PATH;

•  v– список аргументов определяется через указатель на массив аргументов.

В нашу задачу не входит описание всех возможностей вызовов, тем более что они обстоятельно описаны в [1, 2, 5, 6], и мы будем использовать по тексту любую, более удобную для нас форму без дополнительных объяснений.

Большинство форм функции exec()являются POSIX-совместимыми, а большая часть форм функции spawn()представляет собой специфическое расширение QNX. Более того, даже для тех функций группы spawn(), которые часто называют POSIX-совместимыми [1], техническая документация QNX определяет степень совместимости примерно в таких терминах: « …функция spawn() является функцией QNX Neutrino (основанной на POSIX 1003.1d черновом стандарте).»

Функции семейства exec(), напротив, подменяют исполняемый код текущего процесса (не изменяя его идентификатор PID, права доступа, внешние ресурсы процесса, а также находящийся в том же адресном пространстве) исполняемым кодом из другого файла. Поэтому используются эти вызовы непосредственно после fork()для замены копии вызывающего процесса новым (это классическая UNIX-технология использования).

Функции семейства spawn(), напротив, порождают новый процесс (с новым идентификатором PID и в новом адресном пространстве). Все формы вызовов spawn() после подготовительной работы (иногда очень значительной) в конечном итоге ретранслируются в вызов базовой формы spawn() [13]13
  Тем не менее это вовсе не означает, что следует непосредственно использовать вызов spawn(), ведь он самый трудоемкий и чреват ошибками.


[Закрыть]
, который последним действием своего выполнения и посылает сообщение procnto(менеджер процессов QNX, «территориально» объединенный с микроядром системы в одном файле).

Базовый вызов spawn()определяется следующим образом:

#include

pid_t spawn(const char* path, int fd_count, const int fd_map[],

 const struct inheritance* inherit, char* const argv[],

 char* const envp[]);

где path– полное имя исполняемого бинарного файла;

fd_count– размерность следующего за ним массива fd_map;

fd_map– массив файловых дескрипторов, которые вы хотели бы унаследовать в дочернем процессе от родительского. Если fd_countне равен 0 (то есть может иметь значения вплоть до константы OPEN_MAX), то fd_mapдолжен содержать список из fd_countфайловых дескрипторов. Если же fd_countравен 0, то дочерний процесс наследует все родительские дескрипторы, исключая те, которые созданы с флагом PD_CLOEXECфункции fcntl();

inherit– системная структура (см. системные определения) типа struct inheritance, содержащая как минимум:

 unsigned long flags– один или более установленных бит:

  SPAWN_CHECK_SCRIPT– позволить spawn()запускать требуемый командный интерпретатор, интерпретируя pathкак скрипт (интерпретатор указан в первой строке скрипта path);

  SPAWN_SEARCH_PATH– использовать переменную окружения PATHдля поиска выполняемого файла path;

  SPAWN_SETGROUP– установить для дочернего процесса значение группы, специфицируемое членом (структуры) pgroup. Если этот флаг не установлен, дочерний процесс будет частью текущей группы родительского процесса;

  SPAWN_SETND– запустить дочерний процесс на удаленном сетевом узле QNET, сам же удаленный узел специфицируется членом (структуры) nd(см. команду удаленного запуска on);

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

  SPAWN_SETSIGMASK– использовать sigmaskв качестве сигнальной маски дочернего процесса.

 pid_t pgroup– группа дочернего процесса; имеет смысл, только если установлен флаг SPAWN_SETGROUP. Если флаг SPAWN_SETGROUPустановлен и inherit.pgroupустановлен как SPAWN_NEWPGROUP, то дочерний процесс открывает новую группу процессов с идентификатором группы (GID), равным PID этого нового процесса.

 sigset_t sigmask– сигнальная маска дочернего процесса, если установлен флаг SPAWN_SETSIGMASK.

 sigset_t sigdefault– набор сигналов дочернего процесса, для которых определяется реакция по умолчанию, если установлен флаг SPAWN_SETSIGDEF.

 uint32_t nd– это совершенно уникальный (относительно других ОС, а значит, и всего POSIX) параметр QNX – дескриптор узла сети QNET, на котором должен быть запущен новый процесс. Это поле используется, только если установлен флаг SPAWN_SETND.

argv– указатель массива аргументов. Значение argv[0]должно быть строкой ( char*), содержащей имя файла, загружаемого как процесс (но может быть NULL, если аргументы не передаются). Последний элемент массива argvобязан быть NULL. Само значение argvникогда не может быть NULL.

envp– указатель массива символьных строк переменных системного окружения (environment). Последний элемент массива envpобязан быть NULL. Каждый элемент массива является строкой ( char*) вида: variable = value. Если само значение указателя envpравно NULL, то дочерний процесс полностью наследует копию окружения родителя. (Окружение процесса – всегда «копия», поэтому любые изменения, внесенные в окружение дочерним процессом, никак не отражаются на окружении его родителя.)

Примечание

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

Правила наследования (и ненаследования) параметров дочернего процесса от родителя (RID, RGID и других атрибутов) жестко регламентированы, достаточно сложны (в зависимости от флагов) и могут быть уточнены в технической документации QNX. Отметим, что безусловно наследуются такие параметры, как: а) приоритет и дисциплина диспетчеризации; б) рабочий и корневой каталоги файловой системы. Не наследуются: установки таймеров процесса tms_utime, tms_stime, tms_cutimeи tms_cstime, значение взведенного сигнала SIGALRM(это значение сбрасывается в ноль), файловые блокировки, блокировки и отображения памяти (shared memory), установленные родителем.

При успешном завершении вызов функции возвращает PID порожденного процесса. При неудаче возвращается -1 и errnoустанавливается:

•  E2BIG– количество байт, заданное в списке аргументов или переменных окружения и превышающее ARG_MAX;

•  EACCESS– нет права поиска в каталогах префикса имени файла, или для файла не установлены права на выполнение, или файловая система по указанному пути была смонтирована с флагом ST_NOEXEC;

•  EAGAIN– недостаточно системных ресурсов для порождения процесса;

•  ERADF– недопустим хотя бы один из файловых дескрипторов в массиве fd_map;

•  EFAULT– недопустима одна из буферных областей, указанных в вызове;

•  ELOOP– слишком глубокий уровень символических ссылок к файлу или глубина префиксов (каталогов) в полном пути к файлу;

•  EMFILE– недостаточно ресурсов для отображения файловых дескрипторов в дочерний процесс;

•  ENAMETOOLONG– длина полного пути превышает PATH_MAXили длина компонента имени файла и пути превышает NAME_MAX;

•  ENOENT– файл нулевой длины или несуществующий префиксный компонент в полном пути;

•  ENOEXEC– файл, указанный как программа, имеет ошибочный для исполняемого файла формат;

•  ENOMEM– в системе недостаточно свободной памяти для порождения процесса;

•  ENOSYS– файловая система, специфицированная полным путевым именем файла, не предназначена для выполнения spawn();

•  ENOTDIR– префиксные компоненты пути исполняемого файла не являются каталогами;

Даже из этого очень краткого обзора вызова spawn()становятся очевидными некоторые вещи:

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

• Она же и самая громоздкая форма, тяжеловесная для практического кодирования, поэтому в реальных текстах в большинстве случаев вы вместо нее встретите ее конкретизации: spawnl(), spawnle(), spawnlp(), spawnlpe(), spawnp(), spawnv(), spawnve(), spawnvp(), spawnvpe(). Все эти формы достаточно полно описаны в [1]. Функционально они эквивалентны spawn(), поэтому мы не станем на них детально останавливаться.

• Хотя вызов spawn()и упоминается в описаниях как POSIX-совместимый, в QNX он существенно расширен и модифицирован и поэтому в лучшем случае может квалифицироваться как «выполненный по мотивам» POSIX.

В качестве примера приведем использованную в [4] (глава Д. Алексеева «Утилита on») форму вызова для запуска программы (с именем, заданным в строке command) на удаленном узле node(например, /net/xxx) сети QNET (как вы понимаете, это совершенно уникальная возможность QNX, говорить о которой в рамках POSIX-совместимости просто бессмысленно):

int main() {

 char* command = «...», *node = «...»;

 // параметры запуска не используются

 char* const argv[] = { NULL };

 struct inheritance inh;

 inh.flags = 0;

 // флаг удаленного запуска

 inh.flags |= SPAWN_SETND;

 // дескриптор хоста

 inh.nd = netmgr_strtond(node, NULL);

 pid_t pid = spawnp(command, 0, NULL, &inh, argv, NULL);

 ...

}

Использованная здесь форма spawnp()наиболее близка к базовой spawn()и отличается лишь тем, что для поиска файла программы используется переменная системного окружения PATH.

Приведем характерный пример вызова группы exec*():

int execl(const char* path, const char* arg0, const char* arg1, ...

 const char* argn, NULL);

где path– путевое имя исполняемого файла; arg0, …, argn– символьные строки, доступные процессу как список аргументов. Список аргументов должен завершаться значением NULL. Аргумент arg0должен быть именем файла, ассоциированного с запускаемым процессом.

Примечание

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

Примечание

Если вызов exec*()выполняется из многопоточного родительского процесса, то все выполняющиеся потоки этого процесса предварительно завершаются. Никакие функции деструкторов для них не выполняются.

Если вызов exec*()успешен, управление никогда уже не возвращается в точку вызова. В случае неудачи возвращается -1 и errnoустанавливается так же, как описано выше для spawn().

В качестве примера работы вызова spawn*()(использование exec()аналогично) рассмотрим приложение ( файлы p1.cc, p1ch.cc), в котором:

• Родительский процесс ( p1) порождает дочерний ( p1ch) и ожидает от него поступления сигнала SIGUSR1(сигналы детально обсуждаются позже, но здесь попутно «вскроем» одну из их особенностей).

• Дочерний процесс периодически посылает родителю сигнал SIGUSR1.

• Родительский процесс может переустановить (с помощью параметров командной строки запуска) для дочернего: период посылки сигнала (1-й параметр задан в нашем приложении константой) и приоритет, с которым будет выполняться дочерний процесс (2-й параметр, в качестве которого ретранслируется единственный параметр команды запуска родителя).

Примечание

В данный момент нас интересует только то приложение, в котором дочерний процесс порождается вызовом spawnl(). Используемые приложением механизмы и понятия – сигналы UNIX приоритеты, наследование и инверсия приоритетов – будут рассмотрены позже, поэтому при первом чтении их можно опустить. Нам не хотелось перегружать текст дополнительными «пустыми» примерами, лишь иллюстрирующими применение одной функции. Это приложение, созданное «на будущее», позволит нам отследить крайне актуальный для систем реального времени вопрос о наличии (или отсутствии) наследования приоритетов при посылке сигналов (допустимо как одно, так и другое решение, но оно должно быть однозначно единственным для ОС).

Итак, родительское приложение ( файл p1.cc):

Сигналы и наследование приоритетов

#include

#include

#include

#include

#include

#include

// обработчик сигнала

static void handler(int signo, siginfo_t* info, void* context) {

 int oldprio = getprio(0);

 setprio(0, info->si_value, sival_int);

 cout << "SIG = " << signo << " old priority = "

  << oldprio << " new priority = " << getprio(0) << endl;

 setprio(0, oldprio);

}

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

 // установить обработчик сигнала

 sigset_t sig;

 sigemptyset(&sig);

 //определение #define SIGUSR1 16

 sigaddset(&sig, SIGUSR1);

 sigprocmask(SIG_BLOCK, &sig, NULL);

 struct sigaction act;

 act.sa_mask = sig;

 act.sa_sigaction = handler;

 act.sa_flags = SA_SIGINFO;

 if (sigaction(SIGUSR1, &act, NULL) < 0)

  perror(«set signal handler»), exit(EXIT_FAILURE);

 // создать новый (дочерний) процесс

 const char* prg = «./p1ch», *sdelay = "3";

 pid_t pid =

  ((argc > 1 ) && (atoi(argv[1]) >= sched_get_priority_min(SCHED_RR)) &&

  (atoi(argv[1]) <= sched_get_priority_max(SCHED_RR))) ?

  spawnl(P_NOWAIT, prg, prg, sdelay, argv[1], NULL) :

  spawnl(P_NOWAIT, prg, prg, sdelay, NULL);

 if (pid == -1)

  perror(«spawn child process»), exit(EXIT_FAILURE);

 // размаскировать и ожидать сигнала.

 sigprocmask(SIG_UNBLOCK, &sig, NULL);

 while (true) {

  if (sleep(3) != 0) continue;

  cout << "parent main loop: priority = " << getprio(0) << endl;

 }

}

Дочернее приложение ( файл p1ch.cc), которое и будет запускать показанный выше родительский процесс:

#include

#include

#include

#include

#include

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

 int val, del = 5;

 if ((argc > 1) &&

  (sscanf(argv[1], «%i», &val) == 1) && (val > 0)) del = val;

 if ((argc > 2) &&

  (sscanf(argv[2], «%i», &val) == 1 ) && (val > 0) &&

  (val <= sched_get_priority_max(SCHED_RR)))

  if (setprio(0, val) == -1) perror(«set priority»);

 // периодически уведомлять родителя SIGUSR1, используя

 // его как сигнал реального времени (с очередью):

 while(true) {

  sleep(del);

  union sigval val;

  val.sival_int = getprio(0);

  // #define SIGUSR1 16

  sigqueue(getppid(), SIGUSR1, val);

 }

}

Примечание

Для многих сигналов действием на их получение, предопределенным по умолчанию, является завершение процесса. (Реже встречается действие по умолчанию – игнорировать полученный сигнал при отсутствии явно установленной для него функции обработчика.) Достаточно странно, что завершение процесса предусмотрено как реакция по умолчанию на получение «пользовательских» сигналов SIGUSR1 и SIGUSR2. Если показанное выше приложение в процессе отладки запустить вызовом из командной строки (из командного интерпретатора или, например, файлового менеджера mqc), то результатом (на первый взгляд не столь ожидаемым) станет завершение интерпретатора командной строки (родительского процесса) и, как следствие, самого тестируемого приложения.

Вот как выглядит начальный участок совместной работы двух процессов:

# p1 15

parent main loop: priority = 10

SIG = 16: old priority = 10, new priority = 15

SIG = 16: old priority = 10, new priority = 15

parent main loop: priority = 10

SIG = 16: old priority = 10, new priority = 15

parent main loop: priority = 10

SIG = 16: old priority = 10, new priority = 15

parent main loop priority = 10

SIG = 16: old priority = 10, new priority = 15

parent main loop: priority = 10


    Ваша оценка произведения:

Популярные книги за неделю