• 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 <stdlib.h>
#include <stdio.h>
#include <iostream.h>
#include <signal.h>
#include <unistd.h>
#include <sched.h>
// обработчик сигнала
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 <stdio.h>
#include <iostream.h>
#include <sched.h>
#include <unistd.h>
#include <signal.h>
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
SIG = 16: old priority = 10, new priority = 15
parent main loop: priority = 10
Отчетливо видно, что при посылке сигналов реального времени наследование приоритета посылающего процесса не происходит (дочернее приложение, посылающее сигнал, выполняется с приоритетом 15, а обработчик полученного сигнала в родительском процессе выполняется с приоритетом по умолчанию, равным 10).