Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учеб.пос.СП.doc
Скачиваний:
28
Добавлен:
31.03.2015
Размер:
1.33 Mб
Скачать
    1. Системный вызов sigaction

Реакция приложения на сигналы задается с помощью системного вызова sigaction. Он вызывается для каждого сигнала, действие которого необходимо изменить:

sigaction - устанавливает реакцию приложения на сигнал

#include <signal.h>

int sigaction (

int signum, /* номер сигнала */

const struct sigaction *act, /* новое действие */

struct sigaction *oact /* старое действие */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

struct sigaction - структура для системного вызова sigaction

struct sigaction {

void (*sa_handler)(int); /* SIG_DFL, SIG_IGN или указатель на функцию */

sigset_t sa_mask; /* сигналы, которые будут заблокированы */

int sa_flags; /* флаги */

void (*sa_sigaction)(int, siginfo_t *, void *); /* разработчик сигнала реального времени */

};

В аргументе act передается указатель на структуру, которая определяет реакцию на сигнал для всего процесса. Если аргумент oact не является пустым указателем, по заданному адресу записывается описание прежней реакции процесса на сигнал. Если нужно лишь получить описание действия сигнала, передайте в аргументе act значение NULL, в этом случае никаких изменений вноситься не будет. В структуре sigaction поле sa_handler определяет реакцию на сигнал и может иметь одно из следующих значений:

  • SIG_DFL - реакция по умолчанию, которая зависит от вида сигнала. Приложение всегда либо игнорирует сигнал, либо завершается, либо приостанавливается, либо возобновляет работу;

  • SIG_IGN - приложение игнорирует сигнал, в результате доставка сигнала не оказывает влияния на приложение;

  • функция - указатель на функцию-обработчик. В этом случае говорят, сигнал будет перехвачен.

Функции-обработчики сигналов выглядят следующим образом:

static void fcn(int signum)

{

(void)write(STDOUT_FILENO, “Получен сигнал\n”, 11);

_exit(EXIT_FAILURE);

}

В момент доставки сигнала вызывается функция-обработчик, которой в качестве аргумента передается номер сигнала. Можно предусмотреть отдельные функции для каждого из сигналов, а можно все сигналы обрабатывать одной функцией. Перехваченный сигнал блокируется на время исполнения функции-обработчика, но имеется возможность дополнительно указать список сигналов, которые должны быть блокированы. Для этого их нужно занести в набор sa_mask. Ниже приводится список переносимых флагов для поля sa_flags. Обратите внимание на то, что первые два применимы только для сигнала SIGCHLD:

  • SA_NOCLDSTOP - не посылать сигнал при остановке и возобновлении дочернего процесса;

  • SA_NODEFER - не добавлять сигнал к маске при вызове обработчика, если он явно не указан в поле sa_mask;

  • SA_ONSTACK - доставлять сигнал на альтернативном стеке сигналов, если таковой был объявлен обращением к функции sigaltstack;

  • SA_RESETHAND - установить реакцию на сигнал в значение SIG_DFL и на входе в функцию-обработчик сбрасывать флаг SA_SIGINFO;

  • SA_RESTART - не прерывать выполнение системных вызовов;

  • SA_SIGINFO - указатель на функцию обработчик следует брать из поля sa_sigaction.

Реакция на сигнал всегда относится ко всему процессу целиком. Она может быть одной из следующих: процесс перехватывает сигнал, игнорирует его, завершается, приостанавливается или возобновляет работу после остановки. Маски сигналов назначаются для отдельных потоков. С помощью обращения к sigaction можно заставить процесс игнорировать сигнал или назначить функцию-обработчик. Если для сигнала выбрана реакция по умолчанию (SIG_DFL), процесс может игнорировать сигнал, завершиться, приостановиться или возобновить работу, но невозможно выбрать конкретную реакцию из этих четырех. Реакции игнорирования, завершения, приостановки и возобновления всегда относятся к процессу целиком. Обращение к обработчику сигнала производится всегда в рамках только одного потока. Если сигнал доставляется процессу и в процессе имеется более одного потока, не заблокировавшего сигнал, заранее предугадать, какому из них сигнал будет доставлен, невозможно.

Сигнал перехвачен, что делать с ним дальше? Это во многом зависит от типа сигнала и причин, его породивших:

  1. Сигнал может быть сгенерирован ядром при обнаружении ошибки. Например, SIGFPE (ошибка выполнения арифметической операции) или SIGPIPE (запись в канал, который не открыт на чтение с другой стороны). В подобных случаях наиболее приемлемая реакция - завершить работу потока или процесса с сообщением об ошибке. Возврат из обработчика в таком состоянии может привести к ошибкам в вычислениях. А в случае ошибок, обнаруженных аппаратными средствами, процесс все равно может быть завершен системой по возвращении из обработчика.

  2. Сигнал может быть вызван действиями пользователя, например, нажатием комбинации клавиш CTRL+C (SIGINT). В этой ситуации можно завершить приложение после сборки мусора или можно прервать длительные вычисления и ожидать новых команд пользователя. Это лишь один из примеров реакции приложения на сигнал – в любом случае, действия, выполняемые по сигналу, тесно связаны с логикой работы приложения.

  3. Сигнал может быть порожден самим приложением. Например, чтобы сообщить о готовности файла к обработке, можно послать сигнал SIGUSR1.

  4. Сигнал может быть сгенерирован таймером.

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

  1. Сообщения об ошибках должны выводиться системным вызовом write, а завершение работы приложения - системным вызовом _exit.

  2. Глобальные переменные, которые изменяются внутри обработчика, должны иметь тип volatile sig_atomic_t, а возврат в приложение должен производиться при условии, что для сигнала установлен флаг SA_RESTART.

  3. Лучше отказаться от создания обработчиков сигналов. Вместо них используйте потоки и системный вызов sigwait.

Приведем пример обработчика, который сообщает номер полученного сигнала. Несмотря на то, что для сигнала установлен флаг SA_RESTART, системный вызов sleep все равно будет прерван, потому что на него действие этого флага не распространяется:

static volatile sig_atomic_t gotsig = -1;

static void handler(int signum)

{

gotsig = signum;

}

int main(void)

{

struct sigaction act;

time_t start, stop;

memset(&act, 0, sizeof(act));

act.sa_handler = handler;

act.sa_flags = SA_RESTART;

sigaction(SIGINT, &act, NULL);

printf(“Нажмите комбинацию клавиш CTRL+C в течение 10 сек. \n”);

start = time(NULL);

printf(“Ожидание длилось %ld сек. \n”, (long)(stop - start));

if (gotsig > 0)

printf(“Получен сигнал с номером %ld \n”, (long)gotsig);

else

printf(“Сигнал не был получен \n”);

exit(EXIT_SUCCESS);

EC_CLEANUP_BGN

exit(EXIT_FAILURE);

EC_CLEANUP_END

}

Ниже приводится результат работы программы – была нажата комбинация клавиш CTRL+C сразу после того, как вышла первая строка:

Нажмите, комбинацию клавиш CTRL+C в течение 10 сек.

Ожидание длилось, 4 сек.

Получен сигнал с номером 2.

    1. Искусственная генерация сигналов

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

kill - посылает сигнал процессу

#include <signal.h>

int kill (

pid_t pid, /* идентификатор процесса ID или группы процессов */

int signum /* сигнал */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

killpg - посылает сигнал группе процессов

#include <signal.h>

int killpg (

pid_t pgrp, /* идентификатор группы процессов */

int signum /* сигнал */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

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

  • если pid > 0, то сигнал будет передан процессу с идентификатором, равным содержимому аргумента pid;

  • если pid = 0, то сигнал будет передан всем процессам, принадлежащим к той же группе, что и процесс отправитель;

  • если pid < -1, то сигнал будет передан всем процессам, с идентификатором группы, равным абсолютному значению аргумента pid;

  • если pid = -1, то сигнал будет передан всем процессам, которым передающий процесс имеет право посылать сигналы, за исключением некоторых системных процессов, перечень которых определяется каждой конкретной реализацией.

Если в аргументе signum передается значение 0, то системный вызов просто проверяет правильность аргумента pid. Это один из способов убедиться в наличии процесса или группы процессов. Если передающий процесс не обладает необходимыми правами, вызов kill возвращает признак ошибки, но при этом код ошибки может свидетельствовать о наличии искомого процесса или группы процессов: ESRCH – процесс (группа процессов), соответствующий заданному аргументу pid, не найден, EPERM - процесс (группа процессов) найден, но передающий процесс не обладает необходимыми правами.

Системный вызов killpg посылает сигнал процессам, которые принадлежат к группе, с идентификатором, равным содержимому аргумента pgrp. Таким образом, он совершенно идентичен обращению: kill(-pgrp, signum);

Рассмотрим системные вызовы, которые позволяют процессу дождаться доставки того или иного сигнала. Имеются системные вызовы, которые могут быть заблокированы в ожидании некоторого события. Например, при чтении строк с терминального устройства, системный вызов read обычно блокируется в ожидании окончания ввода строки. Системный вызов pause не делает ничего особенного и не ожидает чего-то конкретного – он просто блокирует исполнение процесса:

pause - ожидает доставки сигнала

#include <unistd.h>

int pause (void);

/* Возвращает -1 в случае ошибки (код ошибки - в errno) */

Поскольку в момент доставки сигнала практически любой системный вызов будет прерван, можно утверждать, что системный вызов pause ожидает прибытия сигнала. Если функция-обработчик сигнала возвращает управление в вызывающую программу, то системный вызов pause возвращает признак ошибки с кодом EINTR, но поскольку это единственный способ выхода из pause, нет нужды проверять возвращаемое им значение. В реальных приложениях чаще используется более сложный системный вызов sigwait. Системный вызов sigwait позволяет указать, какой сигнал следует ожидать. При его использовании отпадает необходимость в обработчиках сигналов, поскольку по возвращении из sigwait точно будет известно, какой сигнал был доставлен.

sigwait - ожидает доставки сигнала

#include <signal.h>

int sigwait (

const sigset_t *set, /* набор ожидаемых сигналов */

int *signum /* номер полученного сигнала */

);

/* Возвращает 0 в случае успеха или код ошибки (код ошибки - в errno) */

Аргумент set представляет собой набор сигналов, для ожидания которых был вызван sigwait. Когда появляется один из заданных сигналов, его номер возвращается через аргумент signum. Если к моменту вызова sigwait в состоянии ожидания находился один или более сигналов, sigwait возвращает управление немедленно, передавая через signum первый попавшийся сигнал. При использовании в программах системного вызова sigwait, нужно предусмотреть блокировку сигнала, чтобы сигнал не мог быть доставлен процессу. Системный вызов sigwait используется в следующих случаях:

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

  • когда обработкой сигналов занимается отдельный поток. То есть, когда вместо функций-обработчиков используется отдельный поток. Но такой прием срабатывает, только если сигналы посылаются всему процессу – если сигнал посылается конкретному потоку, принять его сможет только этот поток.

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

    1. Часы и таймеры

В этом разделе будут описаны системные часы и таймеры, которые посылают сигналы через установленные интервалы времени:

alarm - планирует выдачу сигнала

#include <unistd.h>

unsigned alarm (

unsigned secs /* через какое количество секунд выдать сигнгал */

);

/* Возвращает количество секунд, оставшееся до сигнала, запланированного предыдущим обращением к вызову или 0, если выдача сигнала не запланирована */

Каждый процесс получает в свое распоряжение один будильник, предназначенный для использования системным вызовом alarm. Когда заданное время истекает, процесс получает сигнал SIGALRM. Дочерний процесс наследует от предка включенный будильник, но сами часы не разделяются процессами. Время, оставшееся до появления сигнала SIGALRM, передается и через системный вызов exec.

Системный вызов alarm устанавливает интервал времени, который передается в аргументе secs, и возвращает количество секунд, установленное предыдущим обращением к нему. Значение 0 возвращается в том случае, если время уже истекло или если выдача сигнала не была запланирована. Возвращаемое значение используется для того, чтобы восстановить значение, имевшееся до обращения к вызову alarm.

Если в аргументе secs передается число 0, будильник выключается. Очень важно не забывать делать это. Например, допустим, что у нас имеется блокируемый системный вызов, пусть это будет read, и нужно, чтобы он находился в заблокированном состоянии не более определенного времени. Тогда можно назначить перехват сигнала SIGALRM и вызвать alarm, передав ему число 5 (секунд), после чего вызвать системный вызов read, который будет заблокирован. По истечении заданного интервала времени сигнал SIGALRM прервет исполнение вызова read, который вернет в вызывающую программу признак ошибки с кодом EINTR. Но если вызов read выполнит свою работу намного раньше и вы забудете отключить будильник, сигнал будет доставлен процессу позже, поскольку 5 секунд – огромное время по меркам компьютеров и какой-нибудь другой системный вызов окажется прерванным. Рассмотрим пример программы, которая использует будильник:

int main(void)

{

struct sigaction act;

char buf[100];

ssize_t rtn;

memset(&act, 0, sizeof(act));

act.sa_handler = handler;

sigaction(SIGALRM, &act, NULL);

alarm(5);

if((rtn = read(STDIN_FILENO, buf, sizeof(buf) – 1)) == -1) {

if(errno == EINTR)

printf(“Время истекло… в следующий раз печатайте быстрее!\n”);

else

EC_FAIL

}

alarm(0);

if (rtn == 0)

printf(“Получен признак конца файла\n”);

else if (rtn > 0) {

buf[rtn] = “\0”;

printf(“Получена строка %s”, buf);

}

exit(EXIT_SUCCESS);

EC_CLEANUP_BGN

exit(EXIT_FAILURE);

EC_CLEANUP_END

}

static void handler(int signum)

{

}

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

$ alarm_test

Время истекло… в следующий раз печатайте быстрее!

$ alarm_test

быстрее

Получена строка быстрее

$

Основное ограничение вызова alarm в том, что процесс обладает единственным будильником.

Любая UNIX система имеет в своем распоряжении обычные системные часы, показания которых можно получить с помощью системных вызовов time и gettimeofday. Все системы поддерживают как минимум один идентификатор - CLOCK_REALTIME, который соответствует часам, отслеживающим время суток. Чтобы получить время в наносекундах, нужно обратиться к системному вызову clock_gettime:

clock_gettime - возвращает время из заданных часов

#include <time.h>

int clock_gettime(

clockid_t clock_id, /* идентификатор часов */

struct timespec *tp /* время */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

Установить время на часах можно с помощью системного вызова clock_settime:

clock_settime - устанавливает часы

#include <time.h>

int clock_settime(

clockid_t clock_id, /* идентификатор часов */

const struct timespec *tp /* время */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

Рассмотрим системные вызовы для работы с таймерами. Начнем с создания таймера:

timer_create - создает таймер внутри процесса

#include <signal.h>

#include <time.h>

int timer_create(

clockid_t clock_id, /* идентификатор часов */

struct sigevent *sig, /* NULL или генерируемый сигнал */

timer_t *timer_id /* возвращает идентификатор таймера */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

В случае успешного завершения, системный вызов timer_create возвращает идентификатор таймера в аргументе timer_id. Удаляется таймер системным вызовом timer_delete:

timer_delete - удаляет таймер процесса

#include <time.h>

int timer_delete (

timer_t timer_id /* идентификатор таймера */

);

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */