UNIX — страница 75 из 115

Более сложная программа может перехватить прерывание и интерпретировать его как запрос на прекращение своих действий и возврат к основному циклу обработки команд. Подумаем о текстовом редакторе: прерывание длинного вывода на печать не должно вызывать завершения редактирования и потерю уже отредактированного текста. Программа для такого случая может быть написана следующим образом:

#include 

#include 


jmp_buf sjbuf;


main() {

 int onintr();


 if(signal(SIGINT, SIG_IGN) != SIG_IGN)

  signal(SIGINT, onintr);

 setjmp(sjbuf);

 /* сохранить текущую позицию стека */


 for(;;) {

  /* главный рабочий цикл */

 }

 ...

}


onintr() { /* установить если прервано */

 signal(SIGINT, onintr); /* установить

                            для следующего прерывания */

 printf("\nInterrupt\n");

 longjmp(sjbuf, 0); /* вернуться

                       в сохраненное состояние */

}

Файл

описывает тип
jmp_buf
как объект, в котором сохраняется позиция стека;
sjbuf
считается таким объектом. Функция
setjmp(3)
сохраняет запись о том, где выполняется программа. Значения переменных не сохраняются. Когда происходит прерывание, выполняется обращение к подпрограмме
onintr
, которая может печатать сообщения, устанавливать флаги и т.д. Функция
longjmp
берет в качестве аргумента объект, сохраненный
setjmp
, и возвращает управление в ячейку после вызова
setjmp
. Поэтому управление (и значение уровня стека) будет возвращено обратно в основную программу — ко входу в головной цикл.

Отметим, что после прерывания сигнал вновь настраивается на

onintr
. Это обусловлено тем, что когда сигналы возникают, они автоматически настраиваются на реакцию по умолчанию.

Некоторые программы, которые "хотят" обнаружить сигналы, просто не могут быть остановлены в произвольный момент, например в середине обновления сложных составных данных. Решение состоит в том, что подпрограмма обработки прерывания должна установить флаг и вернуться к месту вызова

exit
или
longjmp
. Выполнение программы продолжится точно с того места, где оно было прервано, а флаг прерывания будет проверен позднее.

С этим подходом связана одна трудность. Предположим, что, когда посылается сигнал прерывания, программа читается с терминала. Описанная подпрограмма непременно вызывается; она устанавливает свой флаг и возвращается. Если бы, как отмечалось выше, было верно то, что выполнение возобновляется точно с того места, где оно прервалось, программа продолжала бы чтение с терминала до ввода пользователем другой строки. Однако здесь возникает недоразумение, поскольку пользователь может не знать, что программа читает, и предположительно предпочел бы, чтобы сигнал сразу оказал действие. Для разрешения проблемы система должна закончить

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

Так, программы, которые "ловят" сигналы и продолжают после этого свою работу, должны быть готовы к появлению ошибок, вызванных прерванными системными вызовами. (Следует остерегаться системных вызовов

read
— чтение с терминала,
wait
,
pause
). Такая программа при чтении стандартного входного потока могла бы использовать фрагмент, подобный следующему:

#include 

extern int errno;


...

if (read(0, &c, 1) <= 0) /* EOF или прерывание */

 if (errno == EINTR) { /* EOF, вызванный прерыванием */

  errno = 0; /* устанавливается для следующего раза */

 } else { /* настоящий конец файла */

  ...

 }

Очень сложно постоянно следить за тем, как реакция на сигнал комбинируется с выполнением других программ. Предположим, программа ловит сигналы прерывания и располагает средствами (типа "

!
ed
) для выполнения других программ. Тогда программа могла бы выглядеть так:

if (fork() == 0)

 execlp(...);

signal(SIGINT, SIG_IGN); /* родитель игнорирует прерывание */

wait(&status); /* пока потомок не завершился */

signal(SIGINT, onintr); /* восстанавливает прерывания */

Почему? Сигналы посылаются всем вашим процессам. Предположим, программа, которую вы вызвали, ловит свои собственные сигналы прерывания, как это делает редактор. Если вы прервете выполнение подпрограммы, она получит сигнал, вернется к своему главному циклу и, возможно, начнет читать с вашего терминала. Но вызывающая программа также перейдет от

wait
к подпрограмме и будет читать с терминала. Два процесса, читающие с вашего терминала, создадут трудную ситуацию, так как в результате системе придется гадать, к кому попадет та или иная строка входного потока. Решение состоит в том, чтобы родительская программа игнорировала прерывания, пока не завершился процесс-потомок. Это решение нашло свое отражение при обработке сигнала в
system
:

#include 


system(s) /* run command line s */

 char *s;

{

 int status, pid, w, tty;

 int (*istat)(), (*qstat)();


 ...

 if ((pid = fork()) == 0) {

  ...

  execlp("sh", "sh", "-c", s, (char*)0);

  exit(127);

 }

 ...

 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;

}

Несколько слов по поводу описаний: функция

signal
, очевидно, имеет довольно странный второй аргумент. Фактически он представляет собой указатель на функцию, поставляющую целое значение, и в то же время это тип самой подпрограммы сигнала. Две величины,
SIG_IGN
и
SIG_DFL
, имеют правильный тип, но выбраны так, что не совпадают ни с одной из существующих функции. Для любознательных покажем, как они определены для PDP-11 и VAX: определения, видимо, достаточно "неуклюжи", чтобы стимулировать использование
.

#define SIG_DFL (int(*)())0

#define SIG_IGM (int(*)())1

Будильники

Системный вызов

alarm(n)
обеспечивает посылку сигнала
SIGALRM
вашему процессу через n секунд. Сигнал будильника может быть использован для того, чтобы удостовериться в возникновении каких-то событий за соответствующее время. Если что-нибудь произошло, сигнал будильника может быть выключен; в противном случае процесс может получить управление, перехватив этот сигнал.

Для иллюстрации приведем программу

timeout
, которая запускает другую команду; если команда не закончила свое выполнение за определенное время, она будет завершена по звонку будильника. Например, вспомните команду
watchfor