Продолжая использовать наш сайт, вы даете согласие на обработку файлов cookie, которые обеспечивают правильную работу сайта. Благодаря им мы улучшаем сайт!
Принять и закрыть

Читать, слущать книги онлайн бесплатно!

Электронная Литература.

Бесплатная онлайн библиотека.

Читать: QNX/UNIX: Анатомия параллелизма - Олег Иванович Цилюрик на бесплатной онлайн библиотеке Э-Лит


Помоги проекту - поделись книгой:

• Порождается дочерний процесс, которому системой присваивается новое уникальное значение 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]

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], который последним действием своего выполнения и посылает сообщение procnto (менеджер процессов QNX, «территориально» объединенный с микроядром системы в одном файле).

Базовый вызов 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 — полное имя исполняемого бинарного файла;

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;



Поделиться книгой:

На главную
Назад