Читаем без скачивания UNIX — универсальная среда программирования - Брайан Керниган
Шрифт:
Интервал:
Закладка:
Именно свойство наследования дескрипторов файлов через 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.
7.5 Сигналы и прерывания
Теперь мы рассмотрим работу с сигналами извне (такими, как прерывания) и ошибками программы. Последние возникают главным образом из-за некорректных обращений к памяти, выполнения привилегированных команд или при выполнении операций с плавающей запятой. Наиболее распространенными внешними сигналами являются прерывание, посылаемый при печати символа del, выйти, генерируемый символом FS (ctrl-), отбой, вызываемый завершением телефонной связи, и закончить, генерируемый командой kill. Когда происходит одно из этих событий, посылается сигнал всем процессам, запущенным с того же терминала, и если не были приняты другие меры, процесс завершается. Для большинства сигналов пишется файл образа памяти, который может потребоваться при поиске ошибок (см. справочное руководство по adb(1), sdb(1)).
Системный вызов signal изменяет действие, заданное по умолчанию. Он имеет два аргумента: номер, определяющий сигнал, и адрес функции или код, предписывающий игнорировать сигнал либо запустить процедуру, принятую по умолчанию. Файл <signal.h> содержит определения для различных аргументов. Так,
#include <signal.h>
signal(SIGINT, SIG_IGN);
Специфицирует игнорирование прерываний, тогда как
signal(SIGINT, SIG_DEL);
восстанавливает действие по умолчанию, означающее завершение процесса. В любом случае signal возвращает предыдущее значение сигнала. Если второй аргумент signal представляет собой имя функции, которая уже должна быть описана в том же самом исходном файле, то функция будет вызвана, когда возникнет сигнал. Это практикуется довольно часто, чтобы программа могла "подчищать" неоконченные работы перед своим завершением, например удалять временный файл:
#include <signal.h>
char *tempfile = "temp.xxxxxx";
main() {
extern onintr();
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
mktemp(tempfile);
/* Process ... */
exit(0);
}
onintr() { /* почистить, если прервано */
unlink(tempfile);
exit(1);
}
Почему в main имеют место проверки и двойной вызов signal? Вспомните, что сигналы посылаются всем процессам, запущенным с данного терминала. Соответственно если программа должна быть запущена не в диалоговом режиме (с помощью &), shell делает так, что она будет игнорировать прерывания. Поэтому сигналы прерывания, посланные основным процессам, не остановят ее. Если бы эта программа началась с объявления о том, что все прерывания, которые должны быть посланы подпрограмме onintr, не принимаются во внимание, были бы сведены на нет все усилия shell защитить ее при запуске в фоновом режиме.
Решение, показанное выше, состоит в том, чтобы проверить состояние обработки прерываний, если они игнорировались ранее. Функции программы в том виде, в каком она написана, зависят от возвращаемого signal предыдущего состояния конкретного сигнала. Если сигналы уже игнорировались, процесс должен продолжить это дело; в противном случае их следует перехватывать.
Более сложная программа может перехватить прерывание и интерпретировать его как запрос на прекращение своих действий и возврат к основному циклу обработки команд. Подумаем о текстовом редакторе: прерывание длинного вывода на печать не должно вызывать завершения редактирования и потерю уже отредактированного текста. Программа для такого случая может быть написана следующим образом:
#include <signal.h>
#include <setjmp.h>
jmp_buf sjbuf;
main() {
int onintr();
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
setjmp(sjbuf);
/* сохранить текущую позицию стека */
for(;;) {
/* главный рабочий цикл */
}
...
}
onintr() { /* установить если прервано */
signal(SIGINT, onintr); /* установить
для следующего прерывания */
printf("nInterruptn");
longjmp(sjbuf, 0); /* вернуться
в сохраненное состояние */
}
Файл <setjmp.h> описывает тип jmp_buf как объект, в котором сохраняется позиция стека; sjbuf считается таким объектом. Функция setjmp(3) сохраняет запись о том, где выполняется программа. Значения переменных не сохраняются. Когда происходит прерывание, выполняется обращение к подпрограмме onintr, которая может печатать сообщения, устанавливать флаги и т.д. Функция longjmp берет в качестве аргумента объект, сохраненный setjmp, и возвращает управление в ячейку после вызова setjmp. Поэтому управление (и значение уровня стека) будет возвращено обратно в основную программу — ко входу в головной цикл.
Отметим, что после прерывания сигнал вновь настраивается на onintr. Это обусловлено тем, что когда сигналы возникают, они автоматически настраиваются на реакцию по умолчанию.