Московский физико-технический институт
Опубликован: 16.09.2004 | Доступ: свободный | Студентов: 7345 / 1504 | Оценка: 4.44 / 4.28 | Длительность: 15:33:00
Лекция 9:

Организация ввода-вывода в UNIX. Файлы устройств. Аппарат прерываний. Сигналы в UNIX

Прогон программы с пользовательской обработкой сигнала SIGINT

Рассмотрим теперь другую программу – 13–14-3.c:

/* Программа с пользовательской обработкой
   сигнала SIGINT */
#include <signal.h>
#include <stdio.h>
/* Функция my_handler – пользовательский 
   обработчик сигнала */ 
void my_handler(int nsig){ 
    printf("Receive signal %d, 
           CTRL-C pressed\n", nsig);
}
int main(void){ 
    /* Выставляем реакцию процесса на 
       сигнал SIGINT */ 
    (void)signal(SIGINT, my_handler);
    /*Начиная с этого места, процесс будет 
      печатать сообщение о возникновении 
      сигнала SIGINT */ 
    while(1);
    return 0;
}
Листинг 13-14.3. Программа (13–14-3.c) с пользовательской обработкой сигнала SIGINT.

Эта программа отличается от программы из раздела "Прогон программы, игнорирующей сигнал SIGINT " тем, что в ней введена обработка сигнала SIGINT пользовательской функцией. Наберите, откомпилируйте и запустите эту программу, проверьте ее реакцию на нажатие клавиш <CTRL> и <C> и на нажатие клавиш <CTRL> и <4>.

Модификация предыдущей программы для пользовательской обработки сигналов SIGINT и SIGQUIT

Модифицируйте программу из предыдущего раздела так, чтобы она печатала сообщение и о нажатии клавиш <CTRL> и <4>. Используйте одну и ту же функцию для обработки сигналов SIGINT и SIGQUIT . Откомпилируйте и запустите ее, проверьте корректность работы. Снимать программу также придется с другого терминала командой kill .

Восстановление предыдущей реакции на сигнал

До сих пор в примерах мы игнорировали значение, возвращаемое системным вызовом signal() . На самом деле этот системный вызов возвращает указатель на предыдущий обработчик сигнала, что позволяет восстанавливать переопределенную реакцию на сигнал. Рассмотрим пример программы 13—14-4.c, возвращающей первоначальную реакцию на сигнал SIGINT после 5 пользовательских обработок сигнала.

/* Программа с пользовательской обработкой 
сигнала SIGINT, возвращающаяся к первоначальной 
реакции на этот сигнал после 5 его обработок*/
#include <signal.h>
#include <stdio.h>
int i=0; /* Счетчик числа обработок сигнала */
void (*p)(int); /* Указатель, в который будет 
занесен адрес предыдущего обработчика сигнала */ 
/* Функция my_handler – пользовательский обработчик 
    сигнала */ 
void my_handler(int nsig){ 
    printf("Receive signal %d, CTRL-C pressed\n", nsig); 
    i = i+1;
    /* После 5-й обработки возвращаем первоначальную 
    реакцию на сигнал */
    if(i == 5) (void)signal(SIGINT, p);
}
int main(void){ 
    /* Выставляем свою реакцию процесса на сигнал 
SIGINT, запоминая адрес предыдущего обработчика */ 
    p = signal(SIGINT, my_handler);
    /*Начиная с этого места, процесс будет 5 раз 
печатать сообщение о возникновении сигнала SIGINT */ 
    while(1);
    return 0;
}
Листинг 13-14.4. Программа (13—14-4.c) с пользовательской обработкой сигнала SIGINT.

Наберите, откомпилируйте программу и запустите ее на исполнение.

Сигналы SIGUSR1 и SIGUSR2. Использование сигналов для синхронизации процессов

В операционной системе UNIX существует два сигнала, источниками которых могут служить только системный вызов kill() или команда kill , – это сигналы SIGUSR1 и SIGUSR2 . Обычно их применяют для передачи информации о происшедшем событии от одного пользовательского процесса другому в качестве сигнального средства связи.

В материалах семинара 5 (раздел "Написание, компиляция и запуск программы для организации двунаправленной связи между родственными процессами через pipe"), когда рассматривалась связь родственных процессов через pipe, речь шла о том, что pipe является однонаправленным каналом связи, и что для организации связи через один pipe в двух направлениях необходимо задействовать механизмы взаимной синхронизации процессов. Организуйте двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы SIGUSR1 и SIGUSR2 , модифицировав программу из раздела. "Прогон программы для организации однонаправленной связи между родственными процессами через pipe" семинара 5.

Задача повышенной сложности: организуйте побитовую передачу целого числа между двумя процессами, используя для этого только сигналы SIGUSR1 и SIGUSR2 .

При реализации нитей исполнения в операционной системе Linux (см. семинары 6–7, начиная с раздела "Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self() ") сигналы SIGUSR1 и SIGUSR2 используются для организации синхронизации между процессами, представляющими нити исполнения, и процессом-координатором в служебных целях. Поэтому пользовательские программы, применяющие в своей работе нити исполнения, не могут задействовать сигналы SIGUSR1 и SIGUSR2 .

Завершение порожденного процесса. Системный вызов waitpid(). Сигнал SIGCHLD

В материалах семинаров 3–4 (раздел "Завершение процесса. Функция exit() ") при изучении завершения процесса говорилось о том, что если процесс-ребенок завершает свою работу прежде процесса-родителя, и процесс-родитель явно не указал, что он не заинтересован в получении информации о статусе завершения процесса-ребенка, то завершившийся процесс не исчезает из системы окончательно, а остается в состоянии закончил исполнение (зомби-процесс) либо до завершения процесса-родителя, либо до того момента, когда родитель соблаговолит получить эту информацию.

Для получения такой информации процесс-родитель может воспользоваться системным вызовом waitpid() или его упрощенной формой wait() . Системный вызов waitpid() позволяет процессу-родителю синхронно получить данные о статусе завершившегося процесса-ребенка либо блокируя процесс-родитель до завершения процесса-ребенка, либо без блокировки при его периодическом вызове с опцией WNOHANG. Эти данные занимают 16 бит и в рамках нашего курса могут быть расшифрованы следующим образом:

  • Если процесс завершился при помощи явного или неявного вызова функции exit(), то данные выглядят так (старший бит находится слева)     :

  • Если процесс был завершен сигналом, то данные выглядят так (старший бит находится слева):

Каждый процесс-ребенок при завершении работы посылает своему процессу-родителю специальный сигнал SIGCHLD , на который у всех процессов по умолчанию установлена реакция "игнорировать сигнал ". Наличие такого сигнала совместно с системным вызовом waitpid() позволяет организовать асинхронный сбор информации о статусе завершившихся порожденных процессов процессом-родителем.

Системные вызовы wait() и waitpid()

Прототипы системных вызовов

#include <sys/types.h>
#include <wait.h>
pid_t waitpid(pid_t pid, int *status, 
              int options);
pid_t wait(int *status);

Описание системных вызовов

Это описание не является полным описанием системных вызовов, а адаптировано применительно к нашему курсу. Для получения полного описания обращайтесь к UNIX Manual.

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

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

  • Если pid > 0 ожидаем завершения процесса с идентификатором pid.
  • Если pid = 0, то ожидаем завершения любого порожденного процесса в группе, к которой принадлежит процесс-родитель.
  • Если pid = -1, то ожидаем завершения любого порожденного процесса.
  • Если pid < 0, но не –1, то ожидаем завершения любого порожденного процесса из группы, идентификатор которой равен абсолютному значению параметра pid.

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

Если системный вызов обнаружил завершившийся порожденный процесс, из числа специфицированных параметром pid, то этот процесс удаляется из вычислительной системы, а по адресу, указанному в параметре status, сохраняется информация о статусе его завершения. Параметр status может быть задан равным NULL, если эта информация не имеет для нас значения.

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

Системный вызов wait является синонимом для системного вызова waitpid со значениями параметров pid = -1, options = 0.

Используя системный вызов signal() , мы можем явно установить игнорирование этого сигнала ( SIG_IGN ), тем самым проинформировав систему, что нас не интересует, каким образом завершатся порожденные процессы. В этом случае зомби-процессов возникать не будет, но и применение системных вызовов wait() и waitpid() будет запрещено.

Прогон программы для иллюстрации обработки сигнала SIGCHLD

Для закрепления материала рассмотрим пример программы 13—14-5.c с асинхронным получением информации о статусе завершения порожденного процесса.

/* Программа с асинхронным получением информации о 
статусе двух завершившихся порожденных процессов */
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <signal.h>
#include <stdio.h>
/* Функция my_handler – обработчик сигнала SIGCHLD */ 
void my_handler(int nsig){
    int status;
    pid_t pid;
    /* Опрашиваем статус завершившегося процесса и 
    одновременно узнаем его идентификатор */ 
    if((pid = waitpid(-1, &status, 0)) < 0){
        /* Если возникла ошибка – сообщаем о ней и 
        продолжаем работу */ 
        printf("Some error on waitpid errno = %d\n", 
            errno);
    } else {
        /* Иначе анализируем статус завершившегося процесса */ 
        if ((status & 0xff) == 0) {
            /* Процесс завершился с явным или неявным 
вызовом функции exit() */ 
            printf("Process %d was exited with status %d\n", 
            pid, status >> 8);
        } else if ((status & 0xff00) == 0){
            /* Процесс был завершен с помощью сигнала */ 
            printf("Process %d killed by signal %d %s\n", 
            pid, status &0x7f,(status & 0x80) ? 
            "with core file" : "without core file");
        }
    }
}
int main(void){
    pid_t pid;
    /* Устанавливаем обработчик для сигнала SIGCHLD */ 
    (void) signal(SIGCHLD, my_handler);
    /* Порождаем Сhild 1 */ 
    if((pid = fork()) < 0){
        printf("Can\'t fork child 1\n");
        exit(1);
    } else if (pid == 0){
        /* Child 1 – завершается с кодом 200 */
        exit(200);
    }
    /* Продолжение процесса-родителя – порождаем Сhild 2 */ 
    if((pid = fork()) < 0){
        printf("Can\'t fork child 2\n");
        exit(1);
    } else if (pid == 0){
        /* Child 2 – циклится, необходимо удалять с помощью 
        сигнала! */
        while(1);
    }
    /* Продолжение процесса-родителя – уходим в цикл */ 
    while(1);
    return 0;
}
Листинг 13-14.5. Программа (13—14-5.c) с асинхронным получением информации о статусе двух завершившихся порожденных процессов.

В этой программе родитель порождает два процесса. Один из них завершается с кодом 200, а второй зацикливается. Перед порождением процессов родитель устанавливает обработчик прерывания для сигнала SIGCHLD , а после их порождения уходит в бесконечный цикл. В обработчике прерывания вызывается waitpid() для любого порожденного процесса. Так как в обработчик мы попадаем, когда какой-либо из процессов завершился, системный вызов не блокируется, и мы можем получить информацию об идентификаторе завершившегося процесса и причине его завершения. Откомпилируйте программу и запустите ее на исполнение. Второй порожденный процесс завершайте с помощью команды kill с каким-либо номером сигнала. Родительский процесс также будет необходимо завершать командой kill .