Книга: UNIX — универсальная среда программирования

7.4 Процессы

7.4 Процессы

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

Создание процесса низкого уровня: execlp и execvp

Самая важная операция - выполнение другой программы без возврата с помощью системного вызова execlp. Например, чтобы напечатать дату и выполнить тем самым последнее действие запущенной программы, используют

execlp("date", "date", (char*)0);

Первый аргумент execlp есть имя файла команды; execlp выбирает путь поиска (т.е. $PATH) из вашего окружения и выполняет такой же поиск, как shell. Второй и последующие аргументы — это имена и аргументы команд; для новой программы они становятся массивом argv. Конец списка отмечен аргументом 0. (См. справочное руководство по exec(2), и вы поймете конструкцию execlp.)

Вызов execlp перекрывает существующую программу новой, запускает ее и затем завершается. Первоначальная программа получает управление обратно только при возникновении ошибки, например, когда файл не удается найти или он является невыполнимым:

execlp("date", "date", (char*)0);
fprintf(stderr, "Не удалось выполнить 'date'n");
exit(1);

Если число аргументов вам заранее не известно, полезно применить execvp (вариант execlp). Вызов выглядит так:

execvp(filename, argp);

где argp означает массив указателей к аргументам (таким, как argv). Последним в массиве должен быть указатель NULL, так что execvp может отметить конец списка. Как и для execlp, filename — это файл, в котором находится программа, argp — массив argv для новой программы, a argp[0] — имя программы.

Ни одна из перечисленных выше программ не обеспечивает расширения в списке аргументов метасимволов типа <, >, *, кавычки и т.п. Если они вам нужны, воспользуйтесь execlp и вызовите /bin/sh из shell, которая выполнит эту работу. Сконструируйте строку commandline, содержащую полную команду, как если бы она была напечатана на терминале, например:

execlp("/bin/sh/", "sh", "-с", commandline, (char*)0);

Аргумент предписывает трактовать следующий аргумент как целую командную строку.

В качестве иллюстрации exec рассмотрим программу waitfile. Команда

$ waitfile filename [command]

периодически проверяет поименованный файл. Если он не менялся после последней проверки, выполняется command. В том случае, когда команда не указана, файл копируется в стандартный выходной поток. С помощью waitfile мы контролируем работу troff, как в

$ waitfile troff .out echo troff done &

Программа waitfile использует fstat, чтобы выявить время последнего изменения файла.

/* waitfile: wait until file stops changing */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
char *progname;
main(argc, argv)
 int argc;
 char *argv[];
{
 int fd;
 struct stat stbuf;
 time_t old_time = 0;
 progname = argv[0];
 if (argc < 2)
  error("Usage: %s filename [cmd]", progname);
 if ((fd = open(argv[1], 0)) == -1)
  error("can't open %s", argv[1]);
 fstat(fd, &stbuf);
 while(stbuf.st_mtime != old_time) {
  old_time = stbuf.st_mtime;
  sleep(60);
  fstat(fd, &stbuf);
 }
 if (argc == 2) { /* copy file */
  execlp("cat", "cat", argv[1], (char*)0);
  error("can't execute cat %s", argv[1]);
 } else { /* run process */
  execvp(argv[2], &argv[2]);
  error("can't execute %s", argv[2]);
 }
 exit(0);
}

Мы рассмотрели пример работы как execlp, так и execvp. Эта программа выбрана в качестве иллюстрации, поскольку она весьма полезна, но возможны и другие варианты. Так, waitfile могла бы просто завершиться по окончании изменения файла.

Упражнение 7.17

Модифицируйте watchfile (упр. 7.12) так, чтобы она имела то же свойство, что и waitfile: в отсутствие command копируется файл, в противном случае выполняется команда. Могли бы watchfile и waitfile разделять исходную программу? Подсказка: argv[0].

Управление процессами: fork и wait

Следующий шаг — вновь получить управление после запуска программы с помощью execlp и execvp. Так как эти программы просто "перекрывают" старую программу новой, для сохранения старой требуется сначала разбить ее на две копии. Одна из копий может быть перекрыта, в то время как другая ждет новую, перекрывающую ее программу, чтобы завершиться. Разбиение выполняется с помощью системного вызова fork:

proc_id = fork();

Программа разбивается на две копии, каждая из которых продолжает работать. Они отличаются лишь значением, возвращаемым fork, — номером процесса process-id. В первом процессе (потомке) proc_id равен нулю, во втором (родительском) proc_id есть номер процесса-потомка. Итак, вызвать другую программу и вернуться можно следующим образом:

if (fork() == 0)
 execlp("/bin/sh", "sh", "-с", commandline, (char*)0);

Фактически этого достаточно, за исключением обработки ошибок. Fork делает две копии программы. В процессе-потомке fork возвращает нуль, так что он вызывает execlp, которая выполняет commandline и затем завершается. В родительском процессе fork возвращает не нуль, поэтому execlp пропускается. (При наличии ошибки fork возвращает -1-)

Чаще родительский процесс ожидает, пока потомок закончит работу, прежде чем продолжить свое выполнение, для чего используется системный вызов wait:

int status;
if (fork() == 0)
 execlp(...); /* потомок */
wait(&status); /* родитель */

Однако при этом не контролируются ошибки, такие, как сбои execlp и fork, или возможность одновременной работы нескольких процессов-потомков (wait возвращает номер завершившегося процесса-потомка, если вы хотите сравнить его со значением, возвращенным fork). Тем не менее эти три строки являются сердцевиной стандартной функции system.

Значение status, возвращаемое wait, содержит в своих младших восьми разрядах системное представление кода завершения процесса-потомка; оно равно нулю при нормальном завершении и не равно нулю при разного рода затруднениях. Следующие старшие восемь битов берутся из аргумента вызова exit или возвращаются из main, которая вызывает окончание выполнения процесса-потомка.

Если программа вызывается из shell, три дескриптора файла, 0, 1 и 2, ссылаются на соответствующие файлы, и все остальные дескрипторы доступны для использования. Когда эта программа вызывает другую, в соответствии с профессиональной этикой указанные условия должны быть соблюдены. Ни fork, ни exec не влияют никоим образом на открытые файлы; оба процесса, родитель и потомок, имеют одни и те же открытые файлы. Если процесс-родитель буферизует выходной поток, который необходимо вывести до процесса-потомка, родитель должен очистить свой буфер ранее execlp. И, наоборот, при буферизации родителем входного потока потомок потеряет информацию, которая читалась родителем. Выходной поток может быть выведен, но входной нельзя "положить назад". Обе ситуации являются следствием реализации входного или выходного потока стандартной библиотекой ввода-вывода, обсуждавшейся в гл. 6, поскольку при этом и ввод, и вывод буферизуются обычным образом.

Именно свойство наследования дескрипторов файлов через execlp используется в system: если у вызывающей программы стандартные входной и выходной потоки не связаны с терминалом, то этим же свойством обладает команда, вызванная из system. Возможно, такой вариант нам и нужен. В списке команд редактора ed, например, входной поток команды, начинающейся с символа !, вероятно, должен поступить из того же списка. Даже тогда ed должен считывать из своего входного потока по одному символу во избежание возникновения проблем буферизации ввода.

Для диалоговых программ, подобных p, system должна тем не менее вновь связать стандартный входной и выходной потоки с терминалом, в частности /dev/tty.

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

int fd;
fd = open("file", 0);
close(0);
dup(fd);
close(fd);

Вызов close(fd) освобождает дескриптор файла 0 (стандартный входной поток), но, как правило, не влияет на процесс-родитель. Здесь приведена наша версия system для диалоговых программ, использующая progname для вывода сообщений об ошибках. Вам следует игнорировать те части функции, которые имеют дело с сигналами (мы вернемся к ним позднее).

/*
 * Safer version of system for interactive programs
 */
#include <signal.h>
#include <stdio.h>
system(s) /* run command line s */
 char *s;
{
 int status, pid, w, tty;
 int (*istat)(), (*qstat)();
 extern char *progname;
 fflush(stdout);
 tty = open("/dev/tty", 2);
 if (tty == -1) {
  fprintf(stderr, "%s: can't open /dev/ttyn", progname);
  return -1;
 }
 if ((pid = fork()) == 0) {
  close(0);
  dup(tty);
  close(1);
  dup(tty);
  close(2);
  dup(tty);
  close(tty);
  execlp("sh", "sh", "-c", s, (char*)0);
  exit(127);
 }
 close(tty);
 istat = signal(SIGINT, SIG_IGN);
 qstat = signal(SIGQUIT, SIG_IGN);
 while ((w = wait(&status)) != pid && w != -1)
  ;
 if (w == -1)
  status = -1;
 signal(SIGINT, istat);
 signal(SIGQUIT, qstat);
 return status;
}

Отметим, что /dev/tty открыта с режимом 2 — чтение и запись. С помощью dup формируются стандартный входной и выходной потоки. Здесь можно провести аналогию со сборкой системой стандартных входного и выходного потоков и потока ошибок, когда вы в нее входите. Поэтому в ваш стандартный входной поток можно писать:

$ echo hello 1>&0
hello
$

Это означает, что вам следует применить dup к дескриптору файла 2, чтобы вновь связать стандартные ввод и вывод, но открытие /dev/tty является более естественным и безопасным. Даже system имеет потенциальные проблемы: открытые файлы в вызывающей программе, такие, как tty в подпрограмме ttin программы p, будут передаваться процессу-потомку.

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

Оглавление книги


Генерация: 0.209. Запросов К БД/Cache: 2 / 0
поделиться
Вверх Вниз