355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Андрей Робачевский » Операционная система UNIX » Текст книги (страница 13)
Операционная система UNIX
  • Текст добавлен: 6 октября 2016, 02:43

Текст книги "Операционная система UNIX"


Автор книги: Андрей Робачевский


Жанр:

   

ОС и Сети


сообщить о нарушении

Текущая страница: 13 (всего у книги 39 страниц)

Выделение памяти

При обсуждении формата исполняемых файлов и образа программы в памяти мы отметили, что сегменты данных и стека могут изменять свои размеры. Если для стека операцию выделения памяти операционная система производит автоматически, то приложение имеет возможность управлять ростом сегмента данных, выделяя дополнительную память из хипа (heap – куча). Рассмотрим этот программный интерфейс.

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

1. Переменная объявлена как глобальная, и ей присвоено начальное значение в исходном тексте программы, например:

char ptype = «Unknown file type»;

Строка ptype размещается в сегменте инициализированных данных исполняемого файла, и для нее выделяется соответствующая память при создании процесса.

2. Значение глобальной переменной неизвестно на этапе компиляции, например:

char ptype[32];

В этом случае место в исполняемом файле для ptype не резервируется, но при создании процесса для данной переменной выделяется необходимое количество памяти, заполненной нулями, в сегменте BSS.

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

func1() {

 int a;

 char *b;

 static int с = 4;

 ...

}

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

4. Выделение памяти явно запрашивается некоторыми системными вызовами или библиотечными функциями. Например, функция malloc(3C) запрашивает выделение дополнительной памяти, которая в дальнейшем используется для динамического размещения данных. Функция ctime(3C), предоставляющая системное время в удобном формате, также требует выделения памяти для размещения строки, содержащей значения текущего времени, указатель на которую возвращается программе.

Напомним, что дополнительная память выделяется из хипа (heap) – области виртуальной памяти, расположенной рядом с сегментом данных, размер которой меняется для удовлетворения запросов на размещение. Следующий за сегментом данных адрес называется разделительным или брейк-адресом (break address). Изменение размера сегмента данных по существу заключается в изменении брейк-адреса. Для изменения его значения UNIX предоставляет процессу два системных вызова – brk(2) и sbrk(2).

#include

int brk(void *endds);

void *sbrk(int incr);

Системный вызов brk(2) позволяет установить значение брейк-адреса равным endds и, в зависимости от его значения, выделяет или освобождает память (рис. 2.11). Функция sbrk(2) изменяет значение брейк-адреса на величину incr. Если значение incr больше 0, происходит выделение памяти, в противном случае, память освобождается.[23]23
  Заметим, что в некоторых системах дополнительная память выделяется (или освобождается) в порциях, кратных размеру страницы. Например, выделение всего 100 байтов на самом деле приведет к выделению 4096 байтов, если размер страницы равен 4K.


[Закрыть]


Рис 2.11. Динамическое выделение памяти с помощью brk(2)

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

#include

void *malloc(size_t size);

void *calloc(size_t nelem, size_t elsize);

void *realloc(void *ptr, size_t size);

void free(void *ptr);

Функция malloc(3C) выделяет указанное аргументом size число байтов.

Функция calloc(3C) выделяет память для указанного аргументом nelem числа объектов, размер которых elsize. Выделенная память инициализируется нулями.

Функция realloc(3C) изменяет размер предварительно выделенной области памяти (увеличивает или уменьшает, в зависимости от знака аргумента size). Увеличение размера может привести к перемещению всей области в другое место виртуальной памяти, где имеется необходимое свободное непрерывное виртуальное адресное пространство.

Функция free(3C) освобождает память, предварительно выделенную с помощью функций malloc(3C), calloc(3C) или realloc(3C), указатель на которую передается через аргумент ptr.

Указатель, возвращаемый функциями malloc(3C), calloc(3C) и realloc(3C), соответствующим образом выровнен, таким образом выделенная память пригодна для хранения объектов любых типов. Например, если наиболее жестким требованием по выравниванию в системе является размещение переменных типа double по адресам, кратным 8, то это требование будет распространено на все указатели, возвращаемыми этими функциями.

Упомянутые библиотечные функции обычно используют системные вызовы sbrk(2) или brk(2). Хотя эти системные вызовы позволяют как выделять, так и освобождать память, в случае библиотечных функций память реально не освобождается, даже при вызове free(3C). Правда, с помощью функций malloc(3C), calloc(3C) или realloc(3C) можно снова выделить и использовать эту память и снова освободить ее, но она не передается обратно ядру, а остается в пуле malloc(3C).

Для иллюстрации этого положения приведем небольшую программу, выделяющую и освобождающую память с помощью функций malloc(3C) и free(3C), соответственно. Контроль действительного значения брейк-адреса осуществляется с помощью системного вызова sbrk(2):

#include

#include

main() {

 char *obrk;

 char *nbrk;

 char *naddr;

 /* Определим текущий брейк-адрес */

 obrk = sbrk(0);

 printf(«Текущий брейк-адрес= 0x%xn», obrk);

 /* Выделим 64 байта из хипа */

 naddr = malloc(64);

 /* Определим новый брейк-адрес */

 nbrk = sbrk(0);

 printf(«Новый адрес области malloc= 0x%x,»

  « брейк-адрес= 0х%x (увеличение на %d байтов)n»,

  naddr, nbrk, nbrk – obrk);

 /* «Освободим» выделенную память и проверим, что произошло

    на самом деле */

 free(naddr);

 printf(«free(0x%x)n», naddr);

 obrk = sbrk(0);

 printf(«Новый брейк-адрес= 0x%x (увеличение на %d байтов)n»,

  obrk, obrk – nbrk);

}

Откомпилируем и запустим программу:

$ a.out

Текущий брейк-адрес= 0x20ac0

malloc(64)

Новый адрес области malloc = 0x20ac8, брейк-адрес = 0x22ac0

(увеличение на 8192 байтов)

free(0x20ac8)

Новый брейк-адрес = 0x22ac0 (увеличение на 0 байтов)

$

Как видно из вывода программы, несмотря на освобождение памяти функцией free(3C), значение брейк-адреса не изменилось. Также можно заметить, что функция malloc(3C) выделяет больше памяти, чем требуется. Дополнительная память выделяется для необходимого выравнивания и для хранения внутренних данных malloc(3C), таких как размер области, указатель на следующую область и т.п.

Создание и управление процессами

Работая в командной строке shell вы, возможно, не задумывались, каким образом запускаются программы. На самом деле каждый раз порождается новый процесс, а затем загружается программа. В UNIX эти два этапа четко разделены. Соответственно система предоставляет два различных системных вызова: один для создания процесса, а другой для запуска новой программы.

Новый процесс порождается с помощью системного вызова fork(2):

#include

#include

pid_t fork(void);

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

□ идентификаторы пользователя и группы,

□ переменные окружения,

□ диспозицию сигналов и их обработчики,

□ ограничения, накладываемые на процесс,

□ текущий и корневой каталог,

□ маску создания файлов,

□ все файловые дескрипторы, включая файловые указатели,

□ управляющий терминал.

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

Легче перечислить немногочисленные различия между этими процессами, а именно:

□ дочернему процессу присваивается уникальный идентификатор PID.

□ идентификаторы родительского процесса PPID у этих процессов различны,

□ дочерний процесс свободен от сигналов, ожидающих доставки,

□ значение, возвращаемое системным вызовом fork(2) различно для родителя и потомка.

Последнее замечание требует объяснения. Как уже говорилось, возврат из функции fork(2) происходит как в родительский, так и в дочерний процесс. При этом возвращаемое родителю значение равно PID дочернего процесса, а дочерний, в свою очередь, получает значение, равное 0. Если fork(2) возвращает -1, то это свидетельствует об ошибке (естественно, в этом случае возврат происходит только в процесс, выполнивший системный вызов).

В возвращаемом fork(2) значении заложен большой смысл, поскольку оно позволяет определить, кто является родителем, а кто – потомком, и соответственно разделить функциональность. Поясним это на примере:

main() {

 int pid;

 pid = fork();

 if (pid == -1) {

  perror(«fork»);

  exit(1);

 }

 if (pid == 0) {

  /* Эта часть кода выполняется дочерним процессом */

  printf(«Потомокn»);

 } else {

  /* Эта часть кода выполняется родительским процессом */

  printf(«Родительn»);

 }

}

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

UNIX предлагает системный вызов, предназначенный исключительно для запуска программ, т.е. загрузки другого исполняемого файла. Это системный вызов exec(2), представленный на программном уровне несколькими модификациями:

#include

int execl(const char *path, const char *arg0, ... ,

 const char *argn, char * /* NULL */);

int execv(const char* path, char* const argv[]);

int execle(const char *path, char *const arg0[], ... ,

 const char *argn, char* /* NULL */, char *const envp[]);

int execve(const char* path, char const argv[],

 char *const envp[]);

int execlp(const char *file, const char *arg0, ... ,

 const char* argn, char * /* NULL */);

int execvp(const char *file, char *const argv[]);

Все эти функции по существу являются надстройками системного вызова execve(2), который в качестве аргументов получает имя запускаемой программы (исполняемого файла), набор аргументов и список переменных окружения. После выполнения execve(2) не создается новый процесс, а образ существующего полностью заменяется на образ, полученный из указанного исполняемого файла. На рис. 2.12 показано, как связаны между собой приведенные выше функции.

Рис. 2.12. Семейство функций exec(2)

В отличие от вызова fork(2), новая программа наследует меньше атрибутов. В частности, наследуются:

□ идентификаторы процесса PID и PPID,

□ идентификаторы пользователя и группы,

□ эффективные идентификаторы пользователя и группы (в случае, если для исполняемого файла не установлен флаг SUID или SGID),

□ ограничения, накладываемые на процесс,

□ текущий и корневой каталоги,

□ маска создания файлов,

□ управляющий терминал,

□ файловые дескрипторы, для которых не установлен флаг FD_CLOEXEC.

Наследование характеристик процесса играет существенную роль в работе операционной системы. Так наследование идентификаторов владельцев процесса гарантирует преемственность привилегий и, таким образом, неизменность привилегий пользователя при работе в UNIX. Наследование файловых дескрипторов позволяет установить направления ввода/вывода для нового процесса или новой программы. Именно так действует командный интерпретатор. Мы вернемся к вопросу о наследовании в главе 3.

В главе 1 уже говорилось о частом объединении вызовов fork(2) и exec(2), получившем специальное название fork-and-exec. Таким образом загружается подавляющее большинство программ, которые выполняются в системе.

При порождении процесса, который впоследствии может загрузить новую программу, "родителю" может быть небезынтересно узнать о завершении выполнения "потомка". Например, после того как запущена утилита ls(1), командный интерпретатор приостанавливает свое выполнение до завершения работы утилиты и только после этого выдает свое приглашение на экран. Можно привести еще множество ситуаций, когда процессам необходимо синхронизировать свое выполнение с выполнением других процессов. Одним из способов такой синхронизации является обработка родителем сигнала SIGCHLD, отправляемого ему при «смерти» потомка. Механизм сигналов мы рассмотрим в следующем разделе. Сейчас же остановимся на другом подходе.

Операционная система предоставляет процессу ряд функций, позволяющих ему контролировать выполнение потомков. Это функции wait(2), waitid(2) и waitpid(2):

#include

#include

pid_t wait(int* stat_loc);

int waitpid(idtype_t idtype, id_t id,

siginfo_t * infop, int options);

pid_t waitpid(pid_t pid, int *stat_loc, int options);

Первый из этих вызовов wait(2) обладает самой ограниченной функциональностью – он позволяет заблокировать выполнение процесса, пока кто-либо из его непосредственных потомков не прекратит существование. Вызов wait(2) немедленно возвратит состояние уже завершившегося дочернего процесса в переменной stat_loc, если последний находится в состоянии зомби. Значение stat_loc может быть проанализировано с помощью следующих макроопределений:


WIFEXITED(status) Возвращает истинное (ненулевое) значение, если процесс завершился нормально.
WEXITSTATUS(status) Если WIFEXITED(status) не равно нулю, определяет код возврата завершившегося процесса (аргумент функции exit(2)).
WIFSIGNALLED(status) Возвращает истину, если процесс завершился по сигналу.
WTERMSIG(status) Если WIFSIGNALLED(status) не равно нулю, определяет номер сигнала, вызвавшего завершение выполнения процесса.
WCOREDUMP(status) Если WIFSIGNALLED(status) не равно нулю, макрос возвращает истину в случае создания файла core.

Системный вызов waitid(2) предоставляет больше возможностей для контроля дочернего процесса. Аргументы idtype и id определяют, за какими из дочерних процессов требуется следить:


P_PID waitid(2) блокирует выполнение процесса, следя за потомком, PID которого равен id.
P_PGID waitid(2) блокирует выполнение процесса, следя за потомками, идентификаторы группы которых равны id.
P_ALL waitid(2) блокирует выполнение процесса, следя за всеми непосредственными потомками.

Аргумент options содержит флаги, объединенные логическим ИЛИ, определяющие, за какими изменениями в состоянии потомков следит waitid(2):


WEXITED Предписывает ожидать завершения выполнения процесса.
WTRAPPED Предписывает ожидать ловушки (trap) или точки останова (breakpoint) для трассируемых процессов.
WSTOPPED Предписывает ожидать останова процесса из-за получения сигнала.
WCONTINUED Предписывает вернуть статус процесса, выполнение которого было продолжено после останова.
WNOHANG Предписывает завершить свое выполнение, если отсутствует статусная информация (т.е. отсутствует ожидаемое событие).
WNOWAIT Предписывает получить статусную информацию, но не уничтожать ее, оставив дочерний процесс в состоянии ожидания.

Аргумент infop указывает на структуру siginfo_t, которая будет заполнена информацией о потомке. Мы рассмотрим эту структуру в следующем разделе.

Функция waitpid(2), как и функции wait(2) и waitid(2), позволяет контролировать определенное множество дочерних процессов.

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

...

/* Вывести приглашение shell*/

write(1, "$ ", 2);

/* Считать пользовательский ввод */

get_input(inputbuf);

/* Произвести разбор ввода: выделить команду cmd

   и ее аргументы arg[] */

parse_input(inputbuf, and, arg);

/* Породить процесс */

pid = fork();

if (pid == 0) {

 /* Запустить программу */

 execvp(cmd, arg);

 /* При нормальном запуске программы эта часть кода

    выполняться уже не будет – можно смело выводить

    сообщение об ошибке */

 pexit(cmd);

} else

 /* Родительский процесс (shell) ожидает завершения

    выполнения потомка */

 wait(&status);

...

Сигналы

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

Сигналы появились уже в ранних версиях UNIX, но их реализация не была достаточно надежной. Сигнал мог быть "потерян", возникали также определенные сложности с отключением (блокированием) сигналов на время выполнения критических участков кода. В последующие версии системы, как BSD, так и System V, были внесены изменения, позволившие реализовать надежные (reliable) сигналы. Однако модель сигналов, принятая в версиях BSD, была несовместима с моделью версий System V. В настоящее время стандарт POSIX.1 вносит определенность в интерфейс надежных сигналов.

Прежде всего, каждый сигнал имеет уникальное символьное имя и соответствующий ему номер. Например, сигнал прерывания, посылаемый процессу при нажатии пользователем клавиши <Del> или <Ctrl>+<C>, имеет имя SIGINT. Сигнал, генерируемый комбинацией <Ctrl>+<>, называется SIGQUIT. Седьмая редакция UNIX насчитывала 15 различных сигналов, а в современных версиях их число увеличилось вдвое.

Сигнал может быть отправлен процессу либо ядром, либо другим процессом с помощью системного вызова kill(2):

#include

#include

int kill(pid_t pid, int sig);

Аргумент pid адресует процесс, которому посылается сигнал. Аргумент sig определяет тип отправляемого сигнала.

К генерации сигнала могут привести различные ситуации:

□ Ядро отправляет процессу (или группе процессов) сигнал при нажатии пользователем определенных клавиш или их комбинаций. Например, нажатие клавиши <Del> (или <Ctrl>+<C>) приведет к отправке сигнала SIGINT, что используется для завершения процессов, вышедших из-под контроля.[24]24
  Сигналы этого рода генерируются драйвером терминала. Настройка терминального драйвера позволяет связать условие генерации сигнала с любой клавишей.


[Закрыть]

□ Аппаратные особые ситуации, например, деление на 0, обращение к недопустимой области памяти и т.д., также вызывают генерацию сигнала. Обычно эти ситуации определяются аппаратурой компьютера, и ядру посылается соответствующее уведомление (например, в виде прерывания). Ядро реагирует на это отправкой соответствующего сигнала процессу, который находился в стадии выполнения, когда произошла особая ситуация.

□ Определенные программные состояния системы или ее компонентов также могут вызвать отправку сигнала. В отличие от предыдущего случая, эти условия не связаны с аппаратной частью, а имеют чисто программный характер. В качестве примера можно привести сигнал SIGALRM, отправляемый процессу, когда срабатывает таймер, ранее установленный с помощью вызова alarm(2).

С помощью системного вызова kill(2) процесс может послать сигнал как самому себе, так и другому процессу или группе процессов. В этом случае процесс, посылающий сигнал, должен иметь те же реальный и эффективный идентификаторы, что и процесс, которому сигнал отправляется. Разумеется, данное ограничение не распространяется на процессы, обладающие привилегиями суперпользователя. Такие процессы имеют возможность отправлять сигналы любым процессам системы.

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

□ игнорировать сигнал,

□ перехватить и самостоятельно обработать

□ позволить действие по умолчанию.

Текущее действие при получении сигнала называется диспозицией сигнала.

Напомним, что сигналы SIGKILL и SIGSTOP невозможно ни игнорировать, ни перехватить. Сигнал SIGKILL является силовым методом завершения выполнения «непослушного» процесса, а от работоспособности SIGSTOP зависит функционирование системы управления заданиями.

Условия генерации сигнала и действие системы по умолчанию приведены в табл. 2.18. Как видно из таблицы, при получении сигнала в большинстве случаев по умолчанию происходит завершение выполнения процесса. В ряде случаев в текущем рабочем каталоге процесса также создается файл core (в таблице такие случаи отмечены как «Завершить+core»), в котором хранится образ памяти процесса. Этот файл может быть впоследствии проанализирован программой-отладчиком для определения состояния процесса непосредственно перед завершением. Файл core не будет создан в следующих случаях:

□ исполняемый файл процесса имеет установленный бит SUID, и реальный владелец-пользователь процесса не является владельцем– пользователем исполняемого файла;

□ исполняемый файл процесса имеет установленный бит SGID, и реальный владелец-группа процесса не является владельцем-группой исполняемого файла;

□ процесс не имеет права записи в текущем рабочем каталоге;

□ размер файла core слишком велик (превышает допустимый предел RLIMIT_CORE, см. раздел «Ограничения» далее в этой главе).

Таблица 2.18. Сигналы


SIGABRT Завершить+coreСигнал отправляется, если процесс вызывает системный вызов abort(2).
SIGALRM ЗавершитьСигнал отправляется, когда срабатывает таймер, ранее установленный с помощью системных вызовов alarm(2) или setitimer(2).
SIGBUS Завершить+coreСигнал свидетельствует о некоторой аппаратной ошибке. Обычно этот сигнал отправляется при обращении к допустимому виртуальному адресу, для которого отсутствует соответствующая физическая страница. Другой случай генерации этого сигнала упоминался при обсуждении файлов, отображаемых в память (сигнал отправляется процессу при попытке обращения к странице виртуальной памяти, лежащей за пределами файла).
SIGCHLD ИгнорироватьСигнал, посылаемый родительскому процессу при завершении выполнения его потомка.
SIGEGV Завершить+coreСигнал свидетельствует о попытке обращения к недопустимому адресу или к области памяти, для которой у процесса недостаточно привилегий.
SIGFPE Завершить+coreСигнал свидетельствует о возникновении особых ситуаций, таких как деление на 0 или переполнение операции с плавающей точкой.
SIGHUP ЗавершитьСигнал посылается лидеру сеанса, связанному с управляющим терминалом, когда ядро обнаруживает, что терминал отсоединился (потеря линии). Сигнал также посылается всем процессам текущей группы при завершении выполнения лидера. Этот сигнал иногда используется в качестве простейшего средства межпроцессного взаимодействия. В частности, он применяется для сообщения демонам о необходимости обновить конфигурационную информацию. Причина выбора именно сигнала SIGHUP заключается в том, что демон по определению не имеет управляющего терминала и, соответственно, обычно не получает этого сигнала.
SIGILL Завершить+coreСигнал посылается ядром, если процесс попытался выполнить недопустимую инструкцию.
SIGINT ЗавершитьСигнал посылается ядром всем процессам текущей группы при нажатии клавиши прерывания (<Del> или <Ctrl>+<C>).
SIGKILL ЗавершитьСигнал, при получении которого выполнение процесса завершается. Этот сигнал нельзя ни перехватить, ни игнорировать.
SIGPIPE ЗавершитьСигнал посылается при попытке записи в канал или сокет, получатель данных которого завершил выполнение (закрыл соответствующий дескриптор).
SIGPOLL ЗавершитьСигнал отправляется при наступлении определенного события для устройства, которое является опрашиваемым.
SIGPWR ИгнорироватьСигнал генерируется при угрозе потери питания. Обычно он отправляется, когда питание системы переключается на источник бесперебойного питания (UPS).
SIGQUIT Завершить+coreСигнал посылается ядром всем процессам текущей группы при нажатии клавиш <Ctrl>+<>.
SIGSTOP ОстановитьСигнал отправляется всем процессам текущей группы при нажатии пользователем клавиш +. Получение сигнала вызывает останов выполнения процесса.
SIGSYS Завершить+coreСигнал отправляется ядром при попытке недопустимого системного вызова.
SIGTERM ЗавершитьСигнал обычно представляет своего рода предупреждение, что процесс вскоре будет уничтожен. Этот сигнал позволяет процессу соответствующим образом «подготовиться к смерти» – удалить временные файлы, завершить необходимые транзакции и т.д. Команда kill(1) по умолчанию отправляет именно этот сигнал.
SIGTTIN ОстановитьСигнал генерируется ядром (драйвером терминала) при попытке процесса фоновой группы осуществить чтение с управляющего терминала.
SIGTTOU ОстановитьСигнал генерируется ядром (драйвером терминала) при попытке процесса фоновой группы осуществить запись на управляющий терминал.
SIGUSR1 ЗавершитьСигнал предназначен для прикладных задач как простейшее средство межпроцессного взаимодействия.
SIGUSR2 ЗавершитьСигнал предназначен для прикладных задач как простейшее средство межпроцессного взаимодействия.

Простейшим интерфейсом к сигналам UNIX является устаревшая, но по-прежнему поддерживаемая в большинстве систем функция signal(3C). Эта функция позволяет изменить диспозицию сигнала, которая по умолчанию устанавливается ядром UNIX. Порожденный вызовом fork(2) процесс наследует диспозицию сигналов от своего родителя. Однако при вызове exec(2) диспозиция всех перехватываемых сигналов будет установлена на действие по умолчанию. Это вполне естественно, поскольку образ новой программы не содержит функции-обработчика, определенной диспозицией сигнала перед вызовом exec(2). Функция signal(3C) имеет следующее определение:

#include

void(*signal(int sig, void (*disp)(int)))(int);

Аргумент sig определяет сигнал, диспозицию которого нужно изменить.

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


SIG_DFL Указывает ядру, что при получении процессом сигнала необходимо вызвать системный обработчик, т.е. выполнить действие по умолчанию.
SIG_IGN Указывает, что сигнал следует игнорировать. Напомним, что не все сигналы можно игнорировать.

В случае успешного завершения signal(3C) возвращает предыдущую диспозицию – это может быть функция-обработчик сигнала или системные значения SIG_DFL или SIG_IGN. Возвращаемое значение может быть использовано для восстановления диспозиции в случае необходимости.

Использование функции signal(3C) подразумевает семантику устаревших или ненадежных сигналов. Процесс при этом имеет весьма слабые возможности управления сигналами. Во-первых, процесс не может заблокировать сигнал, т. е. отложить получение сигнала на период выполнения критического участка кода. Во-вторых, каждый раз при получении сигнала, его диспозиция устанавливается на действие по умолчанию. Данная функция и соответствующая ей семантика сохранены для поддержки старых версий приложений. В связи с этим в новых приложениях следует избегать использования функции signal(3C). Тем не менее для простейшей иллюстрации использования сигналов, приведенный ниже пример использует именно этот интерфейс:

#include

/* Функция-обработчик сигнала */

static void sig_hndlr(int signo) {

 /* Восстановим диспозицию */

 signal(SIGINT, sig_hndlr);

 printf(«Получен сигнал SIGINTn»);

}

main() {

 /* Установим диспозицию */

 signal(SIGINT, sih_hndlr);

 signal(SIGUSR1, SIG_DFL);

 signal(SIGUSR2, SIG_IGN);

 /* Бесконечный цикл */

 while(1)

  pause();

}

В этом примере изменена диспозиция трех сигналов: SIGINT, SIGUSR1 и SIGUSR2. При получении сигнала SIGINT вызывается обработчик при получении сигнала SIGUSR1 производится действие по умолчанию (процесс завершает работу), а сигнал SIGUSR2 игнорируется. После установки диспозиции сигналов процесс запускает бесконечный цикл, в процессе которого вызывается функция pause(2). При получении сигнала, который не игнорируется, pause(2) возвращает значение -1, а переменная errno устанавливается равной EINTR. Заметим, что каждый раз при получении сигнала SIGINT мы вынуждены восстанавливать требуемую диспозицию, в противном случае получение следующего сигнала этого типа вызвало бы завершение выполнения процесса (действие по умолчанию).

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

$ а.out &

[1] 8365              PID порожденного процесса

$ kill -SIGINT 8365

Получен сигнал SIGINT Сигнал SIGINT перехвачен

$ kill -SIGUSR2 8365  Сигнал SIGUSR2 игнорируется

$ kill -SIGUSR1 8365

[1]+ User Signal 1    Сигнал SIGUSR1 вызывает завер-

a.out                 шение выполнения процесса

$

Для отправления сигналов процессу использована команда kill(1), описанная в предыдущей главе.


    Ваша оценка произведения:

Популярные книги за неделю