С дистрибутивом QNX поставляется очень полезная утилита on. Эта утилита разработана специально для QNX, и аналогов, во всяком случае, полных, в других системах не существует. В справочнике команд QNX об утилите on говорится как о своеобразной надстройке над интерпретатором shell. Я бы даже сказал, что это сетевое расширение интерпретатора. В процессе "эволюции" различных ветвей UNIX появилось очень много способов удаленного запуска процессов и обмена данными между ними. Это rsh, rlogin, процедуры RPC, просто telnet/ssh и многое, многое другое. В QNX развитие этой возможности вылилось в разработку и создание своей собственной, отличной от IP, сети. Сеть QNX настолько прозрачна, что наличие механизмов, подобных имеющимся в других UNIX-системах, в ней просто не нужно (хотя все они присутствуют для сохранения совместимости с другими системами): процессы "общаются" друг с другом посредством сообщений микроядра, причем находятся ли эти процессы на одном компьютере или на разных - совершенно не важно.
Какие возможности дает пользователю утилита on?
Вот список самых важных, с моей точки зрения, вещей, которые позволяет делать утилита on:
запускать процессы на удаленном узле QNX сети;
запускать процессы с удаленного узла QNX сети;
запускать процессы с установленным фиксированным уровнем приоритета (от 1 до 63);
запускать процессы на другом терминале (перенаправление ввода/вывода и установка управляющего терминала);
запускать процессы от имени другого пользователя (только при наличии привилегий администратора);
останавливать процесс сразу после запуска для отладочных целей (аналогично команде run в gdb). Это позволяет присоединиться к отлаживаемому процессу отладчиком, и продолжить его выполнение вручную;
Первые два пункта взаимно исключают друг друга, остальные же возможности могут использоваться одновременно. Вот как, например, можно запустить утилиту id на удаленном узле test от имени пользователя dmi с приоритетом 63 на терминале /dev/con4:
# on -f test -p 63 -t /dev/con4 -u dmi id
И действительно, на четвертом терминале узла test мы получим вывод программы id:
id=100(dmi), gid=100(users)
К сожалению, у меня не получилось запустить процесс с помощью on на открытом псевдотерминале (сессии pterm), так как on в ответ всегда сообщал, что устройство занято.
В чем же различие между первым методом запуска (на узле) и вторым (с узла)? Вот что говорит нам use on:
-n node spawn on remote node
-f node spawn from remote node
В обоих случаях выполнение происходит на удаленном узле, но есть два отличия:
при использовании ключа "-n" корневым каталогом остается текущий корневой каталог, в том время как при использовании ключа "-f" корневым каталогом становится корневой каталог удаленного узла. Разумеется, это относиться только к запускаемому процессу и его потомкам;
При задании ключа "-n" программа загружается с локального узла на удаленный и после этого запускается; в случае "-f" программа запускается напрямую с удаленного узла;
Запущенный таким образом процесс использует ресурсы (память и процессор) удаленного узла, но весь ввод/вывод направляется на ту консоль, откуда он был запущен, если не указан параметр "-t", то есть рабочий терминал. Также не стоит забывать, что в случае использования ключа "-n" не изменяется корневой каталог, и все файлы загружаются с файловой системы локального узла, если к файлу не указан полный сетевой путь.
Запуск на удаленном узле применяется обычно для разгрузки процессора или памяти на локальном узле, например, на маломощных машинах с небольшим количеством памяти и слабым процессором. Таким же образом, один компьютер может распределять свои задачи на целой группе машин, образуя своеобразный кластер. Команда on может быть полезна еще и для административных целей: запуска менеджеров ресурсов, демонов, утилит диагностики. Вот пример скрипта, запускающего программу проверки диска на всех видимых узлах QNX сети:
#!/usr/bin/perl
opendir(DIR, "/net") || die "No qnet running\n";
while(defined($node=readdir(DIR))) {
system( "on -f $node chkfsys /" );
}
Этот скрипт проверит корневые разделы жестких дисков на всех доступных узлах, причем вывод (и ввод) chkfsys будет производиться на консоль, с которой скрипт был запущен.
У on есть еще одна полезная опция - "-d":
-d detach (nozombie)
При ее использовании on не ждет завершения работы вызываемой программы, а сразу завершает свою работу. Эта опция обычно применяется для запуска менеджеров ресурсов.
Как видно из примеров, утилита on в ряде случаев действительно полезна. Часть ее функций просто незаменима в крупных, распределенных по сети проектах, однако применять ее в "чистом" виде, т.е. вызывая с помощью system() весьма неудобно и непрактично. Давайте рассмотрим программные реализации некоторых полезных свойств этой утилиты.
Ожидание устройства или файла.
Здесь и далее для краткости я буду приводить фрагменты программ, в которых не выполняются проверки на ошибки.
Часто для того, чтобы начать работу с устройством или файлом, надо дождаться пока это устройство или файл будут инициализированы. В противном случае программа может аварийно завершится на вызове open(). Ожидание устройства реализовано в утилите on следующим образом:
-W nsec number of seconds to wait for -w
-w name wait for name to exist.
Попробуем реализовать это сами. Для определения возможности доступа к файлу используется вызов stat(). Следовательно, нам надо определить структуру, в которую этот вызов в случае успешного завершения возвратит данные о файле:
struct stat st;
Определим переменную - указатель на путь к файлу path. Мы будем считать, что параметры передаются программе из командной строки.
char * path = NULL;
Количество секунд, после которых программа аварийно завершится, если файл так и не будет найден.
int sec;
Возвращаемое значение для вызова stat():
int retcode;
Здесь мы просто проверяем командную строку на количество параметров. Будем считать, что программа вызывается следующим образом:
% ./wait_file < устройство > < количество секунд >
if( argc < 3 )
exit( EXIT_FAILURE );
Копируем в переменную path первый аргумент командной строки, т.е. путь до устройства.
path = (char *)strdup( argv[1] );
Берем количество секунд из второго аргумента.
sec = atoi( argv[2] ) + 1;
Для ожидания будем использовать цикл while, уменьшая на единицу количество оставшихся секунд и запрашивая состояние файла в условии цикла. Цикл завершится, если файл будет обнаружен и информация о нем будет успешно получена, или истечет период ожидания (sec == 0).
while(((retcode = stat(path, &st)) == -1) && (sec--)){
Если устройство еще не обнаружено, то "засыпаем" на одну секунду.
sleep( 1 );
}
Мы вышли из цикла; проверяем, было ли найдено устройство.
if( retcode != 0 ){
fprintf( stderr, "device not found\n" );
}
Запуск на другом терминале
Во время работы программы может возникнуть необходимость автоматически запустить программу диагностики на другом терминале в отладочных целях, чтобы не "мусорить" на активном терминале. Следующий алгоритм показывает, как это cделать. Применять его напрямую не стоит, т.к. он изменяет все основные файловые дескрипторы (stdin, stdout, stderr).
Определим структуру с параметрами наследования для нового процесса:
struct inheritance inh;
Идентификатор запущенного нами процесса.
pid_t pid;
Не особенно нужная нам переменная, но сохраненная для целостности кода программы - файловый дескриптор.
int fd;
Карта файловых дескрипторов, передаваемых в созданный процесс. Вы, скорее всего, уже поняли, что для запуска на другом терминале надо просто "подменить" текущие значения stdin, stdout и stderr на дескриптор нужного нам терминала.
int fds[3] = {0, 1, 2};
Команда, которую мы будем выполнять.
char * command = NULL;
Будем считать, что программа запущена с именем терминала и запускаемого процесса в командной строке:
% ./ontty /dev/conX
if( argc < 3 )
exit( EXIT_FAILURE );
Закрываем дескриптор 0, он нам в этом примере больше не нужен.
close(0);
Теперь дескриптором открытого файла будет 0, т.е. первое свободное значение. Откроем указанный терминал.
if( -1 == (fd = open( argv[1], O_RDWR ))) {
perror("open");
exit(EXIT_FAILURE);
};
Если здесь поставить вызов sleep() и на другом терминале запустить утилиту sin с параметром fd (показать открытые файловые дескрипторы), то мы увидим, что дескриптор 0 нашей программы указывает на терминал с именем, указанным в argv[1]. Продублируем полученный дескриптор (а это у нас stdin, дескриптор 0!) в дескрипторы 1 и 2 (stdout и stderr):
dup2(0, 1);
dup2(1, 2);
Проинициализируем флаги запуска процесса. В нашем случае необходимо указать, что процесс будет запущен в своей группе и будет активным на своем терминале.
inh.flags = 0;
inh.flags |= SPAWN_SETSID | SPAWN_TCSETPGROUP;
Дублируем команду в переменную command.
command = (char *)strdup( argv[2] );
Сдвигаем указатель на аргументы командной строки на две позиции вперед (пропускаем имя нашего процесса и имя терминала). Теперь argv указывает на значение <command>, что нам и нужно. Последующие значение argv содержат переменные окружения (тип терминала, имя пользователя, пути поиска программ и библиотек, командный интерпретатор и т.д.), которые мы, скорее всего, тоже хотим сохранить, чтобы не устанавливать вручную.
argv += 2;
Вызываем spawnp():
pid = spawnp( command, 3, fds, &inh, argv, 0 );
Этот вызов ищет указанную команду по каталогам из переменной окружения PATH. Если в параметре <command> нашей программы будет указан полный путь, то она не выполнится.
Попробуем?
# ./ontty /dev/con4 id
Запуск с заданным приоритетом
Процесс также можно запустить с заданным приоритетом и методом диспетчеризации. Для этого в структуре inheritance указываются поля policy и param.sched_priority. Попробуем реализовать небольшой пример.
int main( int argc, char * argv[]) {
struct inheritance inh;
pid_t pid;
int nd;
char * command = NULL;
if( argc < 3 )
exit( EXIT_FAILURE );
Здесь на самом деле нам нужен не spawn(), а exec(). К всеобщему удовольствию, вызов spawn() может действовать как exec(), так что нам ничего не нужно переделывать. Просто устанавливаем флаг SPAWN_EXEC.
inh.flags = SPAWN_EXEC;
Установим для запускаемого процесса такой же метод диспетчеризации, какой установлен и для нашей программы. Разумеется, можно установить и любой другой.
inh.policy = sched_getscheduler(0);
Устанавливаем приоритет. Как и в предыдущих примерах, берем его из командной строки безо всяких проверок.
inh.param.sched_priority = atoi( argv[1] );
Устанавливаем флаг SPAWN_EXPLICIT_SCHED, который указывает вызову spawn(), что мы сами устанавливаем параметры диспетчеризации.
inh.flags |= SPAWN_EXPLICIT_SCHED;
command = (char *)strdup( argv[2] );
argv += 2;
pid = spawnp( command, 0, NULL, &inh, argv, 0 );
}//main
Вызов интерпретатора sh или утилиты pidin очень убедительно показывает, что приоритет установлен.
Запуск от имени другого пользователя
В принципе, в этом нет ничего сложного или нового. Просто получаем идентификатор пользователя и его группы с помощью стандартных для UNIX вызовов getpwnam(), setuid(), setgid() и initgroups(). Последний вызов в этом примере не используется. Для выполнения этого примера нужны права администратора.
int main( int argc, char * argv[]) {
struct inheritance inh;
pid_t pid;
int nd;
char * command = NULL;
struct passwd *pw;
if( argc < 3 )
exit( EXIT_FAILURE );
pw = getpwnam( (char*)argv[1] );
setgid( pw->pw_gid );
setuid( pw->pw_uid );
seteuid( pw->pw_uid );
inh.flags = SPAWN_EXEC;
command = (char *)strdup( argv[2] );
argv += 2;
pid = spawnp( command, 0, NULL, &inh, argv, 0 );
}//main
Запуск на удаленном узле QNX сети
Запуск на удаленном узле - это одна из ключевых возможностей утилиты on. Для этого используется тот же вызов spawn(), что и в предыдущих примерах. В структуре inheritance предусмотрено специальное поле для дескриптора удаленного узла - nd.
Для того чтобы получить дескриптор узла, зная его имя, используется функция netmgr_strtond(), описанная в заголовочном файле <sys/netmgr.h>. Возвращаемый этой функцией дескриптор помещается в поле nd структуры inheritance.
Дескриптор узла однозначно определяет сетевой узел, но только для текущего (локального) узла и текущего процесса. Дескриптор узла необходимо получать каждый раз при установке соединения. Дескриптор локального узла определяется макросом ND_NODE_LOCAL, и, как правило, равен нулю. Для сравнения сетевых дескрипторов используется макрос ND_NODE_CMP, который возвращает 0, если дескрипторы указывают на один и тот же узел. Кроме установки дескриптора удаленного узла, в поле flags структуры inheritance необходимо также установить флаг SPAWN_SETND.
Рассмотрим небольшой пример:
int main( int argc, char * argv[]) {
struct inheritance inh;
pid_t pid;
int nd;
char * command = NULL;
char * node = NULL;
if( argc < 3 )
exit( EXIT_FAILURE );
inh.flags = 0;
Устанавливаем флаг SPAWN_SETND - запуск на удаленном узле. Обратите внимание, что флаг SPAWN_EXEC мы не устанавливаем!
inh.flags |= SPAWN_SETND;
node = (char*)strdup(argv[1]);
Получаем сетевой идентификатор узла и записываем его в поле nd структуры inheritance.
if( -1 == ( nd = netmgr_strtond( node, NULL ))) {
perror( "node" );
exit( EXIT_FAILURE );
}
inh.nd = nd;
command = (char *)strdup( argv[2] );
argv += 2;
pid = spawnp( command, 0, NULL, &inh, argv, 0 );
}//main
Этот пример работает точно так же, как и вызов утилиты on с параметром "-n имя_узла"
В случае если необходимо продублировать функциональность утилиты on c параметром "-f", перед вызовом spawnp() добавляется вызов chroot() - изменение текущего корневого каталога с параметром "/net/имя_узла". Для этого в программу надо добавить всего несколько строк:
char buf[1024];
sprintf( buf, "/net/%s/", argv[1] );
chroot( buf ); // до вызова spawn!
Заключение
Затронутые в статье вопросы не раз обсуждались на форуме проекта QNX.ORG.RU (qnx.org.ru/forum). Именно благодаря этим обсуждениям, и основываясь на них, написана эта статья. Конечно же, она не содержит ответов на все вопросы, связанные с работой утилиты on и ее программных реализациях, а является чем-то вроде краткого введения в интересный и изящный мир внутреннего устройства QNX.
Присылайте Ваши вопросы и комментарии автору по электронной почте: dmi@qnx.org.ru. |