Главная » Программирование для UNIX » Сигналы и прерывания в системе UNIX

0

В этом  разделе поэтапно рассмотрим процесс обработки сигналов (таких как прерывания), поступающих из внешнего мира, а также ошибок программы. Ошибки программ возникают в основном из-за непра-

вильных обращений к памяти, при  выполнении специфических инст рукций или  из-за  операций с плавающей точкой. Наиболее  распространенные сигналы,  поступающие из  внешнего мира:  прерывание (interrupt)  –  этот  сигнал посылается, когда вы  нажимаете  клавишу DEL;  выход (quit)– порождается  символом  FS  (ctl-\);  отключение (hangup) – вызван тем, что повешена телефонная трубка, и завершение (terminate) –  порождается  командой  kill. Когда  происходит одно  из вышеуказанных  событий, сигнал посылается  всем  процессам, запу щенным с данного терминала, и если не существует соглашений, пред писывающих иное, сигнал завершает процесс. Для  большинства сигналов создается  дамп  памяти, который может потребоваться  для  отладки. (См. adb(1) и sdb(l).)

Системный вызов signal изменяет действие, выполняемое по умолчанию.  Он имеет  два аргумента: первый – это номер, который определяет сигнал, второй – это или  адрес функции, или же код,  предписывающий  игнорировать сигнал или  восстанавливать  действия по умолчанию.  Файл <signal.h> содержит описания различных аргументов. Так,

#include <signal.h>

signal(SIGINT, SIG_IGN);

приводит к игнорированию прерывания, в то время как

signal(SIGINT, SIG_DFL);

восстанавливает действие по  умолчанию –  завершение процесса. Во всех  случаях signal  возвращает предыдущее значение сигнала.  Если второй аргумент – это имя  функции (которая должна быть объявлена в этом  же  исходном файле), то она  будет  вызвана при  возникновении сигнала. Чаще всего эта возможность используется для того, чтобы позволить программе подготовиться к выходу,  например удалить временный файл:

#include <signal.h>

char  *tempfile  = "temp.XXXXXX";

main()

{

extern onintr();

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

mktemp(tempfile);

/*  Обработка  …  */ exit(0);

}

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

{

unlink(tempfile); exit(1);

}

Зачем нужны проверка и повторный вызов signal в main? Вспомните, что сигналы посылаются во все процессы, запущенные на данном терминале. Соответственно, когда программа запущена не в интерактивном режиме (а с помощью &), командный процессор позволяет ей игно рировать прерывания, таким образом, программа не будет  остановлена прерываниями, предназначенными для  не фоновых процессов. Если  же  программа начинается с анонсирования того,  что все прерывания должны быть  посланы в onintr, невзирая ни на что, это сводит  на нет  попытки  командного процессора защитить  программу,  работающую в фоновом режиме.

Решение, представленное выше, позволяет  проверить состояние управления прерываниями и продолжать игнорировать прерывания, если они игнорировались ранее. Код учитывает тот факт, что 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("\nInterrupt\n");

longjmp(sjbuf,  0);        /*  возврат  в сохраненное  состояние */  }

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

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

Некоторые программы просто не могут  быть остановлены в произвольном  месте, например  в  процессе обработки сложной структуры  данных, поэтому необходимо иметь возможность обнаруживать сигналы. Возможно следующее  решение –  надо  сделать так, чтобы  программа обработки прерываний установила флаг  и возвратилась обратно вместо того, чтобы вызывать exit или longjmp. Выполнение будет продолжено с того самого  места, в котором оно было  прервано, а флаг  прерывания может быть проверен позже.

С таким  подходом связана одна  трудность.  Предположим, что  программа  читает с  терминала в  то  время, когда послано  прерывание. Надлежащим образом вызывается указанная подпрограмма, которая устанавливает флаги и возвращается обратно. Если  бы дело  действительно обстояло так, как было указано выше, то есть выполнение программы возобновлялось бы «с того самого места, где оно было прервано»,  то программа должна была  бы продолжать читать с терминала до тех пор,  пока  пользователь не напечатал бы новую строку. Такое поведение  может сбивать с толку, ведь пользователь может и не знать, что программа читает, и он, вероятно, предпочел бы,  чтобы  сигнал вступал  в силу незамедлительно. Чтобы разрешить эту проблему, система завершает чтение, но  со  статусом ошибки,  который указывает,  что произошло; errno   устанавливается в  EINTR,  определенный в  errno.h, чтобы обозначить прерванный системный вызов.

Поэтому программы, которые перехватывают сигналы и возобновляют работу  после  них, должны быть  готовы к «ошибкам», вызванным прерванными системными вызовами. (Системные вызовы, по отношению к которым надо проявлять осторожность, – это чтение с терминала,   ожидание и  пауза). Такая  программа может использовать код, приведенный ниже, для чтения стандартного ввода:

#include  <errno.h> 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);    /*  восстановить прерывания  */

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

#include <signal.h>

system(s)   /*  выполнить  командную  строку 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– nal. Два значения, SIG_IGN и SIG_DFL, имеют правильный тип, но выби раются таким образом, чтобы  они не совпадали ни с какими возможными  реальными функциями.  Для особо  интересующихся приведем пример  того,   как  они  описываются для   PDP-11 и  VAX;  описания должны быть достаточно отталкивающими, чтобы  побудить к использованию signal.h.

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

#define SIG_IGN  (int (*)())1

Сигналы alarm

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

Чтобы пояснить ситуацию, рассмотрим программу, называемую time– out, она запускает некоторую другую команду; если эта команда не закончилась к определенному времени, она  будет  аварийно прервана, когда alarm  выключится.  Например, вспомните команду  watchfor  из главы 5. Вместо  того  чтобы  запускать ее  на  неопределенное время, можно установить часовой лимит:

$ timeout  -3600  watchfor  dmg   &

Код в timeout иллюстрирует практически все,  о чем говорилось в двух  предыдущих разделах. Потомок создан; предок устанавливает аварийный  сигнал и ждет, пока  потомок закончит  свою  работу. Если  alarm приходит раньше,  то  потомок уничтожается.  Предпринимается  попытка вернуть статус выхода потомка.

/*  timeout:  устанавливает  временное ограничение для  процесса */

#include <stdio.h>

#include <signal.h>

int pid;           /*  идентификатор  дочернего процесса  */

char  *progname; main(argc,  argv)

int argc;

char  *argv[];

{

int sec  = 10,  status,  onalarm();

progname = argv[0];

if  (argc >  1  &&  argv[1][0]  == ‘–’)  { sec  =  atoi(&argv[1][1]);

argc––; argv++;

}

if  (argc < 2)

error("Usage: %s  [–10]  command",  progname); if  ((pid=fork()) == 0)  {

execvp(argv[1],  &argv[1]); error("couldn’t  start  %s",  argv[1]);

}

signal(SIGALRM,  onalarm); alarm(sec);

if (wait(&status) == –1  || (status &   0177)  !=  0) error("%s  killed", argv[1]);

exit((status >> 8)  &   0377);

}

onalarm()     /*  завершить дочерний процесс в случае  получения alarm  */

{

kill(pid, SIGKILL);

}

Упражнение 7.18.  Можете ли вы предположить, как реализован sleep? Подсказка: pause(2). При каких условиях (если такие условия существуют) sleep и alarm могут  создавать помехи друг для друга? ~

История и библиография

В книге не  представлено  подробного описания  реализации системы UNIX, в частности из-за того,  что  существуют имущественные права на код. Доклад Кена Томпсона (Ken Thompson) «UNIX implementation» (Реализация UNIX), изданный в «BSTJ» в июле  1978  года, описывает основные идеи. Эту же  тему поднимают статьи «The  UNIX system – a retrospective»   (Система UNIX   в   ретроспективе)   в  том   же   номере

«BSTJ» и «The  evolution of the  UNIX time-sharing system» (Эволюция UNIX  –  системы разделения  времени), напечатанная  в  материалах Symposium on Language Design and Programming Methodology в журнале издательства Springer-Verlag «Lecture Notes in Computer Science»

№ 79 в 1979  году.  Оба труда  принадлежат перу Денниса Ритчи (Dennis Ritchie).

Программа  readslow  была   придумана Питером  Вейнбергером  (Peter Weinberger) в качестве простого средства для демонстрации зрителям игры шахматной программы Belle  Кена Томсона и Джо  Кондона (Joe  Condon) во время шахматного турнира. Belle записывала состояние игры в файл; наблюдатели опрашивали файл с помощью readslow, чтобы  не занимать слишком много  драгоценных циклов. (Новая версия оборудования для Belle осуществляет небольшие расчеты на  своей главной машине, поэтому больше такой проблемы не существует.)

Том Дафф (Tom Duff) вдохновил нас на написание spname. Статья Айво ра Дерхема (Ivor Durham), Дэвида Лэмба  (David Lamb) и Джеймса Сакса (James Saxe)  «Spelling correction in  user  interfaces» (Проверка орфографии в пользовательских интерфейсах), изданная CACM в октябре 1983  года,  представляет несколько отличающийся от привычного проект реализации исправления орфографических ошибок в контексте почтовой программы.

Источник: Керниган Б., Пайк Р., UNIX. Программное окружение. – Пер. с англ. – СПб: Символ-Плюс, 2003. – 416 с., ил.

По теме:

  • Комментарии