ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 06.12.2023
Просмотров: 31
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Процессы и потоки. Средства межпроцессного взаимодействия: каналы, сигналы, очереди сообщений, семафоры, разделяемые сегменты памяти
Содержание занятия
1.Теоретическая часть. Средства межпроцессного взаимодействия
1.1 Каналы
1.2 Сигналы
1.3 Очереди сообщений
1.4 Семафоры
1.5 Разделяемые сегменты памяти
канал сигнал очередь сообщений память
Цель работы: знакомство с основными средствами организации межпроцессного взаимодействия.
-
1. Теоретическая часть. Средства межпроцессного взаимодействия.
-
1.1 Каналы
Средства локального межпроцессного взаимодействия реализуют высокопроизводительную, детерминированную передачу данных между процессами в пределах одной системы.
К числу наиболее простых и в то же время самых употребительных средств межпроцессного взаимодействия принадлежат каналы, представляемые файлами соответствующего типа. Стандарт POSIX-2001 различает именованные и безымянные каналы. Напомним, что первые создаются функцией mkfifo() и одноименной служебной программой, а вторые - функцией pipe(). Именованным каналам соответствуют элементы файловой системы, ко вторым можно обращаться только посредством файловых дескрипторов. В остальном эти разновидности каналов эквивалентны.
Взаимодействие между процессами через канал может быть установлено следующим образом: один из процессов создает канал и передает другому соответствующий открытый файловый дескриптор. После этого процессы обмениваются данными через канал при помощи функций read() и write(). Примером подобного взаимодействия служит программа, показанная в пример 8.1.
#include
#include
#include
#include
/* Программа копирует строки со стандартного ввода на стандартный вывод, */
/* "прокачивая" их через канал. */
/* Используются функции ввода/вывода нижнего уровня */
#define MY_PROMPT "Вводите строки\n"
#define MY_MSG "Вы ввели: "
int main (void) {
int fd [2];
char buf [1];
int new_line = 1; /* Признак того, что надо выдать сообщение MY_MSG */
/* перед отображением очередной строки */
/* Создадим безымянный канал */
if (pipe (fd) < 0) {
perror ("PIPE");
exit (1);
}
switch (fork ()) {
case -1:
perror ("FORK");
exit (2);
case 0:
/* Чтение из канала и выдачу на стандартный вывод */
/* реализуем в порожденном процессе. */
/* Необходимо закрыть дескриптор, предназначенный */
/* для записи в канал, иначе чтение не завершится */
/* по концу файла */
close (fd [1]);
while (read (fd [0], buf, 1) == 1) {
if (write (1, buf, 1) != 1) {
perror ("WRITE TO STDOUT");
break;
}
}
exit (0);
}
/* Чтение со стандартного ввода и запись в канал */
/* возложим на родительский процесс. */
/* Из соображений симметрии закроем дескриптор, */
/* предназначенный для чтения из канала */
close (fd [0]);
if (write (fd [1], MY_PROMPT, sizeof (MY_PROMPT) - 1) !=
sizeof (MY_PROMPT) - 1) {
perror ("WRITE TO PIPE-1");
}
while (read (0, buf, 1) == 1) {
/* Перед отображением очередной строки */
/* нужно выдать сообщение MY_MSG */
if (new_line) {
if (write (fd [1], MY_MSG, sizeof (MY_MSG) - 1) != sizeof (MY_MSG) - 1) {
perror ("WRITE TO PIPE-2");
break;
}
}
if (write (fd [1], buf, 1) != 1) {
perror ("WRITE TO PIPE-3");
break;
}
new_line = (buf [0] == '\n');
}
close (fd [1]);
(void) wait (NULL);
return (0);
}
Листинг 8.1. Пример взаимодействия между процессами через канал с помощью функций ввода/вывода нижнего уровня.
Решение той же задачи, но с использованием функций буферизованного ввода/вывода, показано в пример 8.2.
#include
#include
#include
#include
#include
#include
/* Программа копирует строки со стандартного ввода на стандартный вывод, */
/* "прокачивая" их через канал. */
/* Используются функции буферизованного ввода/вывода */
int main (void) {
int fd [2];
FILE *fp [2];
char line [LINE_MAX];
/* Создадим безымянный канал */
if (pipe (fd) < 0) {
perror ("PIPE");
exit (1);
}
/* Сформируем потоки по файловым дескрипторам канала */
assert ((fp [0] = fdopen (fd [0], "r")) != NULL);
assert ((fp [1] = fdopen (fd [1], "w")) != NULL);
/* Отменим буферизацию вывода */
setbuf (stdout, NULL);
setbuf (fp [1], NULL);
switch (fork ()) {
case -1:
perror ("FORK");
exit (2);
case 0:
/* Чтение из канала и выдачу на стандартный вывод */
/* реализуем в порожденном процессе. */
/* Необходимо закрыть поток, предназначенный для */
/* записи в канал, иначе чтение не завершится */
/* по концу файла */
fclose (fp [1]);
while (fgets (line, sizeof (line), fp [0]) != NULL) {
if (fputs (line, stdout) == EOF) {
break;
}
}
exit (0);
}
/* Чтение со стандартного ввода и запись в канал */
/* возложим на родительский процесс. */
/* Из соображений симметрии закроем поток, */
/* предназначенный для чтения из канала */
fclose (fp [0]);
fputs ("Вводите строки\n", fp [1]);
while (fgets (line, sizeof (line), stdin) != NULL) {
if ((fputs ("Вы ввели: ", fp [1]) == EOF) ||
(fputs (line, fp [1]) == EOF)) {
break;
}
}
fclose (fp [1]);
(void) wait (NULL);
return (0);
}
Листинг 8.2. Пример взаимодействия между процессами через канал с помощью функций буферизованного ввода/вывода.
Если не указано противное, обмен данными через канал происходит в синхронном режиме: процесс, пытающийся читать из пустого канала, открытого кем-либо на запись, приостанавливается до тех пор, пока данные не будут в него записаны; с другой стороны, запись в полный канал задерживается до освобождения необходимого для записи места. Чтобы отменить подобный режим взаимодействия, надо связать с дескрипторами канала флаг статуса O_NONBLOCK (это может быть сделано при помощи функции fcntl()). В таком случае чтение или запись, которые невозможно выполнить немедленно, завершаются неудачей.
Подчеркнем, что при попытке чтения из пустого канала результат равен 0 (как признак конца файла), только если канал не открыт кем-либо на запись. Под "кем-либо" понимается и сам читающий процесс; по этой причине в приведенной выше программе потребовалось закрыть все экземпляры файлового дескриптора fd [1], возвращенного функцией pipe() как дескриптор для записи в канал.
Функция popen(), описанная выше, при рассмотрении командного интерпретатора, является более высокоуровневой по сравнению с pipe(). Она делает сразу несколько вещей: порождает процесс, обеспечивает выполнение в его рамках заданной команды, организует канал между вызывающим и порожденным процессами и формирует необходимые потоки для этого канала. Если при обращении к popen() задан режим "w", то стандартный ввод команды, выполняющейся в рамках порожденного процесса, перенаправляется на конец канала, предназначенный для чтения; если задан режим "r", то в канал перенаправляется стандартный вывод.
После вызова popen() процесс может писать в канал или читать из него посредством функций буферизованного ввода/вывода, используя сформированный поток. Канал остается открытым до момента вызова функции pclose() (пример 8.3).
#include
int pclose (FILE *stream);
Листинг 8.3. Описание функции pclose().
Функция pclose() не только закрывает поток, сформированный popen(), но и дожидается завершения порожденного процесса, возвращая его статус.
Типичное применение popen() - организация канала для выдачи динамически порождаемых данных на устройство печати командой lp (пример 8.4).
#include
/* Программа печатает несколько первых строк треугольника Паскаля */
#define T_SIZE 16
int main (void) {
FILE *outptr;
long tp [T_SIZE]; /* Массив для хранения текущей строки треугольника */
int i, j;
/* Инициализируем массив, чтобы далее все элементы */
/* можно было считать и выводить единообразно */
tp [0] = 1;
for (i = 1; i < T_SIZE; i++) {
tp [i] = 0;
}
/* Создадим канал с командой */
if ((outptr = popen ("lp", "w")) == NULL) {
perror ("POPEN");
return (-1);
}
(void) fprintf (outptr, "\nТреугольник Паскаля:\n");
for (i = 0; i < T_SIZE; i++) {
/* Элементы очередной строки нужно считать от конца к началу */
/* Элемент tp [0] пересчитывать не нужно */
for (j = i; j > 0; j--) {
tp [j] += tp [j - 1];
}
/* Вывод строки треугольника в канал */
for (j = 0; j <= i; j++) {
(void) fprintf (outptr, " %ld", tp [j]);
}
(void) fprintf (outptr, "\n");
}
return (pclose (outptr));
}
Листинг 8.4. Пример создания и использования канала для вывода данных.
Сходным образом можно организовать канал для чтения результатов выполнения команды (пример 8.5).
1 2 3 4 5
1.2 Сигналы
Как и каналы, сигналы являются внешне простым и весьма употребительным средством локального межпроцессного взаимодействия, но связанные с ними идеи существенно сложнее, а понятия - многочисленнее.
Согласно стандарту POSIX-2001, под сигналом понимается механизм, с помощью которого процесс или поток управления уведомляют о некотором событии, произошедшем в системе, или подвергают воздействию этого события. Примерами подобных событий могут служить аппаратные исключительные ситуации и специфические действия процессов. Термин "сигнал" используется также для обозначения самого события.
Говорят, что сигнал генерируется (или посылается) для процесса (потока управления), когда происходит вызвавшее его событие (например, выявлен аппаратный сбой, отработал таймер, пользователь ввел с терминала специфическую последовательность символов, другой процесс обратился к функции kill() и т.п.). Иногда по одному событию генерируются сигналы для нескольких процессов (например, для группы процессов, ассоциированных с некоторым управляющим терминалом). В момент генерации сигнала определяется, посылается ли он процессу или конкретному потоку управления в процессе. Сигналы, сгенерированные в результате действий, приписываемых отдельному потоку управления (таких, например, как возникновение аппаратной исключительной ситуации), посылаются этому потоку. Сигналы, генерация которых ассоциирована с идентификатором процесса или группы процессов, а также с асинхронным событием (к примеру, пользовательский ввод с терминала) посылаются процессу.
В каждом процессе определены действия, предпринимаемые в ответ на все предусмотренные системой сигналы. Говорят, что сигнал доставлен процессу, когда взято для выполнения действие, соответствующее данным процессу и сигналу. сигнал принят процессом, когда он выбран и возвращен одной из функций sigwait().
В интервале от генерации до доставки или принятия сигнал называется ждущим. Обычно он невидим для приложений, однако доставку сигнала потоку управления можно блокировать. Если действие, ассоциированное с заблокированным сигналом, отлично от игнорирования, он будет ждать разблокирования.
У каждого потока управления есть маска сигналов, определяющая набор блокируемых сигналов. Обычно она достается в наследство от родительского потока.
С сигналом могут быть ассоциированы действия одного из трех типов.
SIG_DFL
Подразумеваемые действия, зависящие от сигнала. Они описаны в заголовочном файле
SIG_IGN
Игнорировать сигнал. Доставка сигнала не оказывает воздействия на процесс.
указатель на функцию
Обработать сигнал, выполнив при его доставке заданную функцию. После завершения функции обработки процесс возобновляет выполнение с точки прерывания. Обычно функция обработки вызывается в соответствии со следующим C-заголовком: void func (int signo); где signo - номер доставленного сигнала.
Первоначально, до входа в функцию main(), реакция на все сигналы установлена как SIG_DFL или SIG_IGN.
Функция называется асинхронно-сигнально-безопасной (АСБ), если ее можно вызывать без каких-либо ограничений при обработке сигналов. В стандарте POSIX-2001 имеется список функций, которые должны быть либо повторно входимыми, либо непрерываемыми сигналами, что превращает их в АСБ-функции. В этот список включены 117 функций, в том числе почти все из рассматриваемых нами.
Если сигнал доставляется потоку, а реакция заключается в завершении, остановке или продолжении, весь процесс должен завершиться, остановиться или продолжиться.
Перейдем к изложению возможностей по генерации сигналов. Выше была кратко рассмотрена служебная программа kill как средство терминирования процессов извне. На самом деле она посылает заданный сигнал; то же делает и одноименная функция (пример 8.6).
#include
int kill (pid_t pid, int sig);
Листинг 8.6. Описание функции kill().
Сигнал задается аргументом sig, значение которого может быть нулевым; в этом случае действия функции kill() сводятся к проверке допустимости значения pid (нулевой результат - признак успешного завершения kill()).
Если pid > 0, это значение трактуется как идентификатор процесса. При нулевом значении pid сигнал посылается всем процессам из той же группы, что и вызывающий. Если значение pid равно -1, адресатами являются все процессы, которым вызывающий имеет право посылать сигналы. При прочих отрицательных значениях pid сигнал посылается группе процессов, чей идентификатор равен абсолютной величине pid.
Процесс имеет право послать сигнал адресату, заданному аргументом pid, если он (процесс) имеет соответствующие привилегии или его реальный или действующий идентификатор пользователя совпадает с реальным или сохраненным ПДП-идентификатором адресата.
У служебной программы kill имеется полезная опция -l, позволяющая увидеть соответствие между номерами сигналов и их мнемоническими именами. Результат выполнения команды kill -l может выглядеть так, как показано в пример 8.7.
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL10) SIGUSR111) SIGSEGV12) SIGUSR2
13) SIGPIPE14) SIGALRM15) SIGTERM17) SIGCHLD
18) SIGCONT19) SIGSTOP20) SIGTSTP21) SIGTTIN
22) SIGTTOU23) SIGURG24) SIGXCPU25) SIGXFSZ
26) SIGVTALRM27) SIGPROF28) SIGWINCH29) SIGIO
30) SIGPWR31) SIGSYS32) SIGRTMIN33) SIGRTMIN+1
34) SIGRTMIN+235) SIGRTMIN+336) SIGRTMIN+437) SIGRTMIN+5
38) SIGRTMIN+639) SIGRTMIN+740) SIGRTMIN+841) SIGRTMIN+9
42) SIGRTMIN+1043) SIGRTMIN+1144) SIGRTMIN+1245) SIGRTMIN+13
46) SIGRTMIN+1447) SIGRTMIN+1548) SIGRTMAX-1549) SIGRTMAX-14
50) SIGRTMAX-1351) SIGRTMAX-1252) SIGRTMAX-1153) SIGRTMAX-10
54) SIGRTMAX-955) SIGRTMAX-856) SIGRTMAX-757) SIGRTMAX-6
58) SIGRTMAX-559) SIGRTMAX-460) SIGRTMAX-361) SIGRTMAX-2
62) SIGRTMAX-163) SIGRTMAX
Листинг 8.7. Возможный результат выполнения команды kill -l.
Мы не будем пояснять назначение всех представленных в листинге сигналов, ограничившись кратким описанием тех, что фигурируют в стандарте POSIX-2001 как обязательные для реализации. Попутно отметим, что, согласно стандарту языка C, должны быть определены имена всего шести сигналов: SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV и SIGTERM.
SIGABRT
Сигнал аварийного завершения процесса. Подразумеваемая реакция предусматривает, помимо аварийного завершения, создание файла с образом памяти процесса.
SIGALRM
Срабатывание будильника. Подразумеваемая реакция - аварийное завершение процесса.
SIGBUS
Ошибка системной шины как следствие обращения к неопределенной области памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGCHLD
Завершение, остановка или продолжение порожденного процесса. Подразумеваемая реакция - игнорирование.
SIGCONT
Продолжение процесса, если он был остановлен. Подразумеваемая реакция - продолжение выполнения или игнорирование (если процесс не был остановлен).
SIGFPE
Некорректная арифметическая операция. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGHUP
Сигнал разъединения. Подразумеваемая реакция - аварийное завершение процесса.
SIGILL
Некорректная команда. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGINT
Сигнал прерывания, поступивший с терминала. Подразумеваемая реакция - аварийное завершение процесса.
SIGKILL
Уничтожение процесса (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - аварийное завершение процесса.
SIGPIPE
Попытка записи в канал, из которого никто не читает. Подразумеваемая реакция - аварийное завершение процесса.
SIGQUIT
Сигнал выхода, поступивший с терминала. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGSEGV
Некорректное обращение к памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGSTOP
Остановка выполнения (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - остановка процесса.
SIGTERM
Сигнал терминирования. Подразумеваемая реакция - аварийное завершение процесса.
SIGTSTP
Сигнал остановки, поступивший с терминала. Подразумеваемая реакция - остановка процесса.
SIGTTIN
Попытка чтения из фонового процесса. Подразумеваемая реакция - остановка процесса.
SIGTTOU
Попытка записи из фонового процесса. Подразумеваемая реакция - остановка процесса.
SIGUSR1, SIGUSR2
Определяемые пользователем сигналы. Подразумеваемая реакция - аварийное завершение процесса.
SIGPOLL
Опрашиваемое событие. Подразумеваемая реакция - аварийное завершение процесса.
SIGPROF
Срабатывание таймера профилирования. Подразумеваемая реакция - аварийное завершение процесса.
SIGSYS
Некорректный системный вызов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGTRAP
Попадание в точку трассировки/прерывания. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGURG
Высокоскоростное поступление данных в сокет. Подразумеваемая реакция - игнорирование.
SIGVTALRM
Срабатывание виртуального таймера. Подразумеваемая реакция - аварийное завершение процесса.
SIGXCPU
Исчерпан лимит процессорного времени. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
SIGXFSZ
Превышено ограничение на размер файлов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.
Процесс (поток управления) может послать сигнал самому себе с помощью функции raise() (пример 8.8). Для процесса вызов raise() эквивалентен kill (getpid(), sig);
#include
int raise (int sig);
Листинг 8.8. Описание функции raise().
Посылка сигнала самому себе использована в функции abort() (пример 8.9), вызывающей аварийное завершение процесса. (Заметим, что этого не произойдет, если функция обработки сигнала SIGABRT не возвращает управления. С другой стороны, abort() отменяет блокирование или игнорирование SIGABRT.)
#include
void abort (void);
Листинг 8.9. Описание функции abort().
Опросить и изменить способ обработки сигналов позволяет функция sigaction() (пример 8.10).
#include
int sigaction (int sig, const struct sigaction
*restrict act, struct sigaction
*restrict oact);
Листинг 8.10. Описание функции sigaction().
Для описания способа обработки сигнала используется структура sigaction, которая должна содержать по крайней мере следующие поля:
void (*sa_handler) (int);
/* Указатель на функцию обработки сигнала */
/* или один из макросов SIG_DFL или SIG_IGN */
sigset_t sa_mask;
/* Дополнительный набор сигналов, блокируемых */
/* на время выполнения функции обработки */
int sa_flags;
/* Флаги, влияющие на поведение сигнала */
void (*sa_sigaction) (int, siginfo_t *, void *);
/* Указатель на функцию обработки сигнала */
Приложение, соответствующее стандарту, не должно одновременно использовать поля обработчиков sa_handler и sa_sigaction.
Тип sigset_t может быть целочисленным или структурным и представлять набор сигналов (см. далее).
Тип siginfo_t должен быть структурным по крайней мере со следующими полями:
int si_signo; /* Номер сигнала */
int si_errno;
/* Значение переменной errno, ассоциированное
с данным сигналом */
int si_code;
/* Код, идентифицирующий причину сигнала */
pid_t si_pid;
/* Идентификатор процесса, пославшего сигнал */
uid_t si_uid;
/* Реальный идентификатор пользователя
процесса, пославшего сигнал */
void *si_addr;
/* Адрес, вызвавший генерацию сигнала */
int si_status;
/* Статус завершения порожденного процесса */
long si_band;
/* Событие, связанное с сигналом SIGPOLL */
В заголовочном файле
SI_USER
Сигнал послан функцией kill().
SI_QUEUE
Сигнал послан функцией sigqueue().
SI_TIMER
Сигнал сгенерирован в результате срабатывания таймера, установленного функцией timer_settime().
SI_ASYNCIO
Сигнал вызван завершением асинхронной операции ввода/вывода.
SI_MESGQ
Сигнал вызван приходом сообщения в пустую очередь сообщений.
Из кодов, специфичных для конкретных сигналов, мы упомянем лишь несколько, чтобы дать представление о степени детализации диагностики, предусмотренной стандартом POSIX-2001. (Из имени константы ясно, к какому сигналу она относится.)
ILL_ILLOPC
Некорректный код операции.
ILL_COPROC
Ошибка сопроцессора.
FPE_INTDIV
Целочисленное деление на нуль.
FPE_FLTOVF
Переполнение при выполнении операции вещественной арифметики.
FPE_FLTSUB
Индекс вне диапазона.
SEGV_MAPERR
Адрес не отображен на объект.
BUS_ADRALN
Некорректное выравнивание адреса.
BUS_ADRERR
Несуществующий физический адрес.
TRAP_BRKPT
Процесс достиг точки прерывания.
TRAP_TRACE
Срабатывание трассировки процесса.
CLD_EXITED
Завершение порожденного процесса.
CLD_STOPPED
Остановка порожденного процесса.
POLL_PRI
Поступили высокоприоритетные данные.
Вернемся непосредственно к описанию функции sigaction(). Если аргумент act отличен от NULL, он указывает на структуру, специфицирующую действия, которые будут ассоциированы с сигналом sig. По адресу oact (если он не NULL) возвращаются сведения о прежних действиях. Если значение act есть NULL, обработка сигнала остается неизменной; подобный вызов можно использовать для опроса способа обработки сигналов.
Следующие флаги в поле sa_flags влияют на поведение сигнала sig.
SA_NOCLDSTOP
Не генерировать сигнал SIGCHLD при остановке или продолжении порожденного процесса (значение аргумента sig должно равняться SIGCHLD).
SA_RESETHAND
При входе в функцию обработки сигнала sig установить подразумеваемую реакцию SIG_DFL и очистить флаг SA_SIGINFO (см. далее).
SA_SIGINFO
Если этот флаг не установлен и определена функция обработки сигнала sig, она вызывается с одним целочисленным аргументом - номером сигнала. Соответственно, в приложении следует использовать поле sa_handler структуры sigaction. При установленном флаге SA_SIGINFO функция обработки вызывается с двумя дополнительными аргументами, как void func (int sig, siginfo_t *info, void *context); второй аргумент указывает на данные, поясняющие причину генерации сигнала, а третий может быть преобразован к указателю на тип ucontext_t - контекст процесса, прерванного доставкой сигнала. В этом случае приложение должно использовать поле sa_sigaction и поля структуры типа siginfo_t. В частности, если значение si_code неположительно, сигнал был сгенерирован процессом с идентификатором si_pid и реальным идентификатором пользователя si_uid.
SA_NODEFER
По умолчанию обрабатываемый сигнал добавляется к маске сигналов процесса при входе в функцию обработки; флаг SA_NODEFER предписывает не делать этого, если только sig не фигурирует явным образом в sa_mask.
Опросить и изменить способ обработки сигналов можно и на уровне командного интерпретатора, посредством специальной встроенной команды trap:
trap [действие условие ...]
Аргумент "условие" может задаваться как EXIT (завершение командного интерпретатора) или как имя доставленного сигнала (без префикса SIG). При задании аргумента "действие" минус обозначает подразумеваемую реакцию, пустая цепочка ("") - игнорирование. Если в качестве действия задана команда, то при наступлении условия она обрабатывается как eval действие.
Команда trap без аргументов выдает на стандартный вывод список команд, ассоциированных с каждым из условий. Выдача имеет формат, пригодный для восстановления способа обработки сигналов (пример 8.11).
save_traps=$(trap)
. . .
eval "$save_traps"
Листинг 8.11. Пример сохранения и восстановления способа обработки сигналов посредством специальной встроенной команды trap.
Обеспечить выполнение утилиты logout из домашнего каталога пользователя во время завершения командного интерпретатора можно с помощью команды, показанной в пример 8.11.
trap '$HOME/logout' EXIT
Листинг 8.12. Пример использования специальной встроенной команды trap.
При перенаправлении вывода в файл приходится считаться с возможностью возникновения ошибок, специфичных для каналов. Чтобы защитить от них процедуры начальной загрузки, в ОС Lunix применяются связки из игнорирования и последующего восстановления подразумеваемой реакции на сигнал SIGPIPE (пример 8.13).
trap "" PIPE
echo "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21
trap - PIPE
Листинг 8.13. Пример использования специальной встроенной команды trap для защиты от ошибок, специфичных для каналов.
К техническим аспектам можно отнести работу с наборами сигналов, которая выполняется посредством функций, показанных в пример 8.14. Функции sigemptyset() и sigfillset() инициализируют набор, делая его, соответственно, пустым или "полным". Функция sigaddset() добавляет сигнал signo к набору set, sigdelset() удаляет сигнал, а sigismember() проверяет вхождение в набор. Обычно признаком завершения является нулевой результат, в случае ошибки возвращается -1. Только sigismember() выдает 1, если сигнал signo входит в набор set.
#include
int sigemptyset (sigset_t *set);
int sigfillset (sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset (sigset_t *set, int signo);
int sigismember (const sigset_t *set,
int signo);
Листинг 8.14. Описание функций для работы с наборами сигналов.
Функция sigprocmask() (пример 8.15) предназначена для опроса и/или изменения маски сигналов процесса, определяющей набор блокируемых сигналов.
#include
int sigprocmask (int how, const sigset_t
*restrict set, sigset_t *restrict oset);
Листинг 8.15. Описание функции sigprocmask().
Если аргумент set отличен от NULL, он указывает на набор, используемый для изменения текущей маски сигналов. Аргумент how определяет способ изменения; он может принимать одно из трех значений: SIG_BLOCK (результирующая маска получается при объединении текущей и заданной аргументом set), SIG_SETMASK (результирующая маска устанавливается равной set) и SIG_UNBLOCK (маска set вычитается из текущей).
По адресу oset (если он не NULL) возвращается прежняя маска. Если значение set есть NULL, набор блокируемых сигналов остается неизменным; подобный вызов можно использовать для опроса текущей маски сигналов процесса.
Если к моменту завершения sigprocmask() будут существовать ждущие неблокированные сигналы, по крайней мере один из них должен быть доставлен до возврата из sigprocmask().
Нельзя блокировать сигналы, не допускающие игнорирования.
Функция sigpending() (пример 8.16) позволяет выяснить набор блокированных сигналов, ожидающих доставки вызывающему процессу (потоку управления). Дождаться появления подобного сигнала можно с помощью функции sigwait() (пример 8.17).
#include
int sigpending (sigset_t *set);
Листинг 8.16. Описание функции sigpending().
#include
int sigwait (const sigset_t *restrict set,
int *restrict sig);
Листинг 8.17. Описание функции sigwait().
Функция sigwait() выбирает ждущий сигнал из заданного набора (он должен включать только блокированные сигналы), удаляет его из системного набора ждущих сигналов и помещает его номер по адресу, заданному аргументом sig. Если в момент вызова sigwait() нужного сигнала нет, процесс (поток управления) приостанавливается до появления такового.
Отметим, что стандарт POSIX-2001 не специфицирует воздействие функции sigwait() на обработку сигналов, включенных в набор set. Чтобы дождаться доставки обрабатываемого или терминирующего процесс сигнала, можно воспользоваться функцией pause() (пример 8.18).
#include
int pause (void);
Листинг 8.18. Описание функции pause().
Функция pause() может ждать доставки сигнала неопределенно долго. Возврат из pause() осуществляется после возврата из функции обработки сигнала (результат при этом равен -1). Если прием сигнала вызывает завершение процесса, возврата из функции pause(), естественно, не происходит.
Несмотря на внешнюю простоту, использование функции pause() сопряжено с рядом тонкостей. При наивном подходе сначала проверяют некоторое условие, связанное с сигналом, и, если оно не выполнено (сигнал отсутствует), вызывают pause(). К сожалению, сигнал может быть доставлен в промежутке между проверкой и вызовом pause(), что нарушает логику работы процесса и способно привести к его зависанию. Решить подобную проблему позволяет функция sigsuspend() (пример 8.19) в сочетании с рассмотренной выше функцией sigprocmask().
#include
int sigsuspend (const sigset_t *sigmask);
Листинг 8.19. Описание функции sigsuspend().
Функция sigsuspend() заменяет текущую маску сигналов вызывающего процесса на набор, заданный аргументом sigmask, а затем переходит в состояние ожидания, аналогичное функции pause(). После возврата из sigsuspend() (если таковой произойдет) восстанавливается прежняя маска сигналов.
Обычно парой функций sigprocmask() и sigsuspend() обрамляют критические интервалы. Перед входом в критический интервал посредством sigprocmask() блокируют некоторые сигналы, а на выходе вызывают sigsuspend() с маской, которую возвратила sigprocmask(), восстанавливая тем самым набор блокированных сигналов и дожидаясь их доставки.
В качестве примера использования описанных выше функций работы с сигналами рассмотрим упрощенную реализацию функции abort() (пример 8.20).
#include
#include
#include
void abort (void) {
struct sigaction sact;
sigset_t sset;
/* Вытолкнем буфера */
(void) fflush (NULL);
/* Снимем блокировку сигнала SIGABRT */
if ((sigemptyset (&sset) == 0) && (sigaddset (&sset, SIGABRT) == 0)) {
(void) sigprocmask (SIG_UNBLOCK, &sset, (sigset_t *) NULL);
}
/* Пошлем себе сигнал SIGABRT. */
/* Возможно, его перехватит функция обработки, */
/* и тогда вызывающий процесс может не завершиться */
raise (SIGABRT);
/* Установим подразумеваемую реакцию на сигнал SIGABRT */
sact.sa_handler = SIG_DFL;
sigfillset (&sact.sa_mask);
sact.sa_flags = 0;
(void) sigaction (SIGABRT, &sact, NULL);
/* Снова пошлем себе сигнал SIGABRT */
raise (SIGABRT);
/* Если сигнал снова не помог, попробуем еще одно средство завершения */
_exit (127);
}
int main (void) {
printf ("Перед вызовом abort()\n");
abort ();
printf ("После вызова abort()\n");
return 0;
}
Листинг 8.20. Упрощенная реализация функции abort() как пример использования функций работы с сигналами.
В качестве нюанса, характерного для работы с сигналами, отметим, что до первого обращения к raise() нельзя закрыть потоки (можно только вытолкнуть буфера), поскольку функция обработки сигнала SIGABRT, возможно, осуществляет вывод. Еще одним примером использования механизма сигналов может служить приведенная в пример 8.13 упрощенная реализация функции sleep(), предназначенной для "засыпания" на заданное число секунд. (Можно надеяться, что не описанные пока средства работы с временем интуитивно понятны.)
#include
#include
#include
#include
/* Функция обработки сигнала SIGALRM. */
/* Она ничего не делает, но игнорировать сигнал нельзя */
static void signal_handler (int sig) {
/* В демонстрационных целях распечатаем номер обрабатываемого сигнала */
printf ("Принят сигнал %d\n", sig);
}
/* Функция для "засыпания" на заданное число секунд */
/* Результат равен разности между заказанной и фактической */
/* продолжительностью "сна" */
unsigned int sleep (unsigned int seconds) {
time_t before, after;
unsigned int slept;
sigset_t set, oset;
struct sigaction act, oact;
if (seconds == 0) {
return 0;
}
/* Установим будильник на заданное время, */
/* но перед этим блокируем сигнал SIGALRM */
/* и зададим свою функцию обработки для него */
if ((sigemptyset (&set) < 0) || (sigaddset (&set, SIGALRM) < 0) ||
sigprocmask (SIG_BLOCK, &set, &oset)) {
return seconds;
}
act.sa_handler = signal_handler;
act.sa_flags = 0;
act.sa_mask = oset;
if (sigaction (SIGALRM, &act, &oact) < 0) {
return seconds;
}
before = time ((time_t *) NULL);
(void) alarm (seconds);
/* Как атомарное действие восстановим старую маску сигналов */
/* (в надежде, что она не блокирует SIGALRM) */
/* и станем ждать доставки обрабатываемого сигнала */
(void) sigsuspend (&oset);
/* сигнал доставлен и обработан */
after = time ((time_t *) NULL);
/* Восстановим прежний способ обработки сигнала SIGALRM */
(void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL);
/* Восстановим первоначальную маску сигналов */
(void) sigprocmask (SIG_SETMASK, &oset, (sigset_t *) NULL);
return ((slept = after - before) > seconds ? 0 : (seconds - slept));
}
int main (void) {
struct sigaction act;
/* В демонстрационных целях установим обработку прерывания с клавиатуры */
act.sa_handler = signal_handler;
(void) sigemptyset (&act.sa_mask);
act.sa_flags = 0;
(void) sigaction (SIGINT, &act, (struct sigaction *) NULL);
printf ("Заснем на 10 секунд\n");
printf ("Проснулись, не доспав %d секунд\n", sleep (10));
return (0);
}
Листинг 8.21. Упрощенная реализация функции sleep() как пример использования механизма сигналов.
Обратим внимание на применение функции sigsuspend(), которая реализует (неделимую) транзакцию снятия блокировки сигналов и перехода в режим ожидания. Отметим также, что по умолчанию при входе в функцию обработки к маске добавляется принятый сигнал для защиты от бесконечной рекурсии. Наконец, если происходит возврат из функции sigsuspend() (после возврата из функции обработки), то автоматически восстанавливается маска сигналов, существовавшая до вызова sigsuspend(). В данном случае в этой маске блокирован сигнал SIGALRM, и потому можно спокойно менять способ его обработки.
Вызвать "недосыпание" приведенной программы можно, послав ей сигнал SIGALRM (например, посредством команды kill -s SIGALRM идентификатор_процесса) или SIGINT (путем нажатия на клавиатуре терминала комбинации клавиш CTRL+C).
- 1 2 3 4 5
1.3 Очереди сообщений
Мы переходим к рассмотрению средств локального межпроцессного взаимодействия, относящихся к необязательной части стандарта POSIX-2001, именуемой "X/Open-расширение системного интерфейса" (XSI). Будут описаны очереди сообщений, семафоры и разделяемые сегменты памяти.
Остановимся сначала на понятиях и структурах, общих для всех трех упомянутых средств. Каждая очередь сообщений, набор семафоров и разделяемый сегмент однозначно идентифицируются положительным целым числом, которое обычно обозначается, соответственно, как msqid, semid и shmid и возвращается в качестве результатов функций msgget(), semget() и shmget().
При получении идентификаторов средств межпроцессного взаимодействия используется еще одна сущность - ключ, а для его генерации предназначена функция ftok() (пример 8.22). Аргумент path должен задавать маршрутное имя существующего файла, к которому вызывающий процесс может применить функцию stat(). В качестве значения аргумента id, по соображениям мобильности, рекомендуется использовать однобайтный символ. Гарантируется, что функция ftok() сгенерирует один и тот же ключ для заданной пары (файл, символ) и разные ключи для разных пар.
#include
key_t ftok (const char *path, int id);
Листинг 8.22. Описание функции ftok().
С идентификатором средства межпроцессного взаимодействия ассоциирована структура данных, содержащая информацию о допустимых и выполненных операциях. Соответствующие декларации сосредоточены в заголовочных файлах
В упомянутую структуру входит подструктура ipc_perm с данными о владельцах и режимом доступа, описанная в файле
uid_t uid;
/* Идентификатор владельца */
gid_t gid;
/* Идентификатор владеющей группы */
uid_t cuid;
/* Идентификатор пользователя,
создавшего данное средство
межпроцессного взаимодействия */
gid_t cgid;
/* Идентификатор создавшей группы */
mode_t mode;
/* Режим доступа на чтение/запись */
Управление доступом к описываемым средствам межпроцессного взаимодействия осуществляется аналогично файловому, только наряду (и наравне) с владельцами (пользователем и группой) рассматриваются те, кто эти средства создал (создатели).
Опросить статус присутствующих в данный момент в системе (т. е. активных) средств межпроцессного взаимодействия позволяет служебная программа ipcs:
ipcs [-qms] [-a | -bcopt]
По умолчанию выдается краткая информация обо всех средствах - очередях сообщений, семафорах и разделяемых сегментах памяти. Если нужно ограничиться их отдельными видами, следует воспользоваться опциями -q, -s и/или -m, соответственно.
Следующие опции управляют форматом выдачи. Задание опции -a равносильно указанию всех опций формата. Опция -b предписывает выдавать лимиты на размер (максимальное количество байт в сообщениях очереди и т.п.), -c - имена пользователя и группы создателя средства, -o - информацию об использовании (количество сообщений в очереди, их суммарный размер и т.п.), -p - информацию о процессах (идентификаторы последнего отправителя, получателя и т.п.), -t - информацию о времени (последняя управляющая операция, последняя отправка сообщения и т.п.).
Для удаления из системы активных средств межпроцессного взаимодействия предназначена служебная программа ipcrm (разумеется, подверженная контролю прав доступа). Удаляемые средства могут задаваться идентификаторами или ключами:
ipcrm [-q msgid | -Q msgkey | -s semid |
-S semkey | -m shmid | -M shmkey ] ...
На этом мы завершаем изложение общих вопросов, относящихся к средствам межпроцессного взаимодействия, и переходим к рассмотрению специфических возможностей каждого из них.
Механизм очередей сообщений позволяет процессам взаимодействовать, обмениваясь данными. Данные передаются между процессами дискретными порциями, называемыми сообщениями. Процессы выполняют над сообщениями две основные операции - прием и отправку. Процессы, отправляющие или принимающие сообщение, могут приостанавливаться, если требуемую операцию невозможно выполнить немедленно. В частности, могут быть отложены попытки отправить сообщение в заполненную до отказа очередь, получить сообщение из пустой очереди и т.п. ("операции с блокировкой"). Если же указано, что приостанавливать процесс нельзя, "операции без блокировки" либо выполняются немедленно, либо завершаются неудачей.
Прежде чем процессы смогут обмениваться сообщениями, один из них должен создать очередь. Одновременно определяются первоначальные права на выполнение операций для различных процессов, в том числе соответствующих управляющих действий над очередями.
Для работы с очередями сообщений стандарт POSIX-2001 предусматривает следующие функции (пример 8.23): msgget() (получение идентификатора очереди сообщений), msgctl() (управление очередью сообщений), msgsnd() (отправка сообщения) и msgrcv() (прием сообщения).
#include
int msgget (key_t key, int msgflg);
int msgsnd (int msqid, const void *msgp,
size_t msgsz, int msgflg);
ssize_t msgrcv (int msqid, void *msgp,
size_t msgsz, long msgtyp,
int msgflg);
int msgctl (int msqid, int cmd,
struct msqid_ds *buf);
Листинг 8.23. Описание функций для работы с очередями сообщений.
Структура msqid_ds, ассоциированная с идентификатором очереди сообщений, должна содержать по крайней мере следующие поля.
struct ipc_perm msg_perm;
/* Данные о правах доступа
к очереди сообщений */
msgqnum_t msg_qnum;
/* Текущее количество сообщений в очереди */
msglen_t msg_qbytes;
/* Максимально допустимый суммарный
размер сообщений в очереди */
pid_t msg_lspid;
/* Идентификатор процесса, отправившего
последнее сообщение */
pid_t msg_lrpid;
/* Идентификатор процесса, принявшего
последнее сообщение */
time_t msg_stime;
/* Время последней отправки */
time_t msg_rtime;
/* Время последнего приема */
time_t msg_ctime;
/* Время последнего изменения
посредством msgctl() */
Перейдем к детальному рассмотрению функций для работы с очередями сообщений.
Функция msgget() возвращает идентификатор очереди сообщений, ассоциированный с ключом key. Новая очередь, ее идентификатор и соответствующая структура msqid_ds создаются для заданного ключа, если значение аргумента key равно IPC_PRIVATE или очередь еще не ассоциирована с ключом, а в числе флагов msgflg задан IPC_CREAT.
Если необходима уверенность в том, что очередь с указанным ключом создается заново, в дополнение к флагу IPC_CREAT следует установить IPC_EXCL. Тогда попытка получить идентификатор уже существующий очереди завершится неудачей.
Структура msqid_ds для новой очереди инициализируется следующим образом.
-
Значения полей msg_perm.cuid, msg_perm.uid, msg_perm.cgid и msg_perm.gid устанавливаются равными действующим идентификаторам пользователя и группы вызывающего процесса. -
Младшие девять бит поля msg_perm.mode устанавливаются равными младшим девяти битам значения msgflg. -
Поля msg_qnum, msg_lspid, msg_lrpid, msg_stime и msg_rtime обнуляются. -
В поле msg_ctime помещается текущее время, а в поле msg_qbytes - определенный в системе лимит.
Один из тонких вопросов, связанных с созданием очереди сообщений, заключается в выборе ключа. Всем процессам, которые намереваются работать с общей очередью сообщений, для получения идентификатора msqid необходимо знать ключ очереди. Задание ключа одинаковым константным значением во всех этих программах небезопасно, поскольку может оказаться так, что тот же ключ будет случайно задействован и другими программами. Как одно из возможных решений рекомендуется использование функции ftok(), вычисляющей действительно "уникальный" ключ.
В пример 8.24 приведен простейший пример программы, где создается очередь сообщений с правами доступа, указанными в командной строке.
#include
#include
#include
/* Программа создает очередь сообщений. */
/* В командной строке задаются имя файла для ftok() */
/* и режим доступа к очереди сообщений */
#define FTOK_CHAR 'G'
int main (int argc, char *argv []) {
key_t key;
int msqid;
int mode = 0;
if (argc != 3) {
fprintf (stderr, "Использование: %s маршрутное_имя режим_доступа\n", argv [0]);
return (1);
}
if ((key = ftok (argv [1], FTOK_CHAR)) == (key_t) (-1)) {
perror ("FTOK");
return (2);
}
(void) sscanf (argv [2], "%o", (unsigned int *) &mode);
if ((msqid = msgget (key, IPC_CREAT | mode)) < 0) {
perror ("MSGGET");
return (3);
}
return 0;
}
Листинг 8.24. Пример программы, создающей очередь сообщений.
Если после выполнения этой программы воспользоваться командой ipcs -q, то результат может выглядеть так, как показано в пример 8.25.
------ Message Queues --------
key msqid owner perms used-bytes messages
0x47034bac 163840 galat 644 0 0
Листинг 8.25. Возможный результат опроса статуса очередей сообщений.
Удалить созданную очередь из системы, соответствующей стандарту POSIX-2001, можно командой ipcrm -q 163840.
Операции отправки/приема сообщений выполняют функции msgsnd() и msgrcv(); msgsnd() помещает сообщения в очередь, а msgrcv() читает и "достает" их оттуда.
В обоих случаях первый аргумент задает идентификатор очереди; второй является указателем на содержащую сообщение структуру. Сообщение состоит из двух частей: текста (последовательности байт) и так называемого типа (положительного целого числа). Тип, указанный во время отправки, используется впоследствии при выборе сообщения из очереди. Аргумент msgsz определяет длину сообщения; аргумент msgflg задает флаги.
В зависимости от значения, указанного в качестве аргумента msgtyp функции msgrcv(), из очереди выбирается то или иное сообщение. Если значение аргумента равно нулю, запрашивается первое сообщение в очереди, если больше нуля - первое сообщение типа msgtyp, а если меньше нуля - первое сообщение наименьшего из типов, не превышающих абсолютную величину аргумента msgtyp. Пусть, например, в очередь последовательно помещены сообщения с типами 5, 3 и 2. Тогда вызов msgrcv (msqid, msgp, size, 0, flags) выберет из очереди сообщение с типом 5, поскольку оно отправлено первым; вызов msgrcv (msqid, msgp, size, -4, flags) - последнее сообщение, так как 2 - это наименьший из возможных типов в указанном диапазоне; наконец, вызов msgrcv (msqid, msgp, size, 3, flags) - сообщение с типом 3.
Во многих приложениях взаимодействующим посредством очереди сообщений процессам требуется синхронизировать свое выполнение. Например, процесс-получатель, пытавшийся прочитать сообщение и обнаруживший, что очередь пуста (либо сообщение указанного типа отсутствует), должен иметь возможность подождать, пока процесс-отправитель не поместит в очередь требуемое сообщение. Аналогичным образом, процесс, желающий отправить сообщение в очередь, в которой нет достаточного для него места, может ожидать его освобождения в результате чтения сообщений другими процессами. Процесс, вызвавший подобного рода "операцию с блокировкой", приостанавливается до тех пор, пока либо станет возможным выполнение операции, либо будет ликвидирована очередь. С другой стороны, имеются приложения, где подобные ситуации должны приводить к немедленному (и неудачному) завершению вызова функции.
Если не указано противное, функции msgsnd() и msgrcv() выполняют операции с блокировкой, например: msgsnd (msqid, msgp, size, 0); msgrcv (msqid, msgp, size, type, 0). Чтобы выполнить операцию без блокировки, необходимо установить флаг IPC_NOWAIT: msgsnd (msqid, msgp, size, IPC_NOWAIT); msgrcv (msqid, msgp, size, type, IPC_NOWAIT).
Аргумент msgp указывает на значение структурного типа, в котором представлены тип и тело сообщения (пример 8.26).
struct msgbuf {
long mtype; /* Тип сообщения */
char mtext [1]; /* Текст сообщения */
};
Листинг 8.26. Описание структурного типа для представления сообщений.
Для хранения реальных сообщений в прикладной программе следует определить аналогичную структуру, указав желаемый размер сообщения, например, так, как это сделано в пример 8.27.
#define MAXSZTMSG 8192
struct mymsgbuf {
long mtype; /* Тип сообщения */
char mtext [MAXSZTMSG]; /* Текст сообщения */
};
struct mymsgbuf msgbuf;
Листинг 8.27. Описание структуры для хранения сообщений.
В качестве аргумента msgsz обычно указывается размер текстового буфера, например: sizeof (msgbuf.text).
Если не указано противное, в случае, когда длина выбранного сообщения больше, чем msgsz, вызов msgrcv() завершается неудачей. Если же установить флаг MSG_NOERROR, длинное сообщение обрезается до msgsz байт. Отброшенная часть пропадает, а вызывающий процесс не получает никакого уведомления о том, что сообщение обрезано.
При успешном завершении msgsnd() возвращает 0, а msgrcv() - значение, равное числу реально полученных байт; при неудаче возвращается -1.
Процессы, обладающие достаточными правами доступа, посредством функции msgctl() могут получать информацию о состоянии очереди, изменять ряд характеристик, удалять очередь.
Управляющее действие определяется значением аргумента cmd. Допустимых значений три: IPC_STAT - получить информацию о состоянии очереди, IPC_SET - переустановить характеристики очереди, IPC_RMID - удалить очередь.
Команды IPC_STAT и IPC_SET для хранения информации об очереди используют имеющуюся в прикладной программе структуру типа msqid_ds, указатель на которую содержит аргумент buf: IPC_STAT копирует в нее ассоциированную с очередью структуру данных, а IPC_SET, наоборот, в соответствии с ней обновляет ассоциированную структуру. Команда IPC_SET позволяет переустановить значения идентификаторов владельца (msg_perm.uid) и владеющей группы (msg_perm.gid), режима доступа (msg_perm.mode), максимально допустимый суммарный размер сообщений в очереди (msg_qbytes). Увеличить значение msg_qbytes может только процесс, обладающий соответствующими привилегиями. В пример 8.28 приведена программа, изменяющая максимально допустимый суммарный размер сообщений в очереди. Предполагается, что очередь сообщений уже создана, а ее идентификатор известен. Читателю предлагается выполнить эту программу с разными значениями максимально допустимого суммарного размера (как меньше, так и больше текущего), действуя от имени обычного и привилегированного пользователя.
#include
#include
int main (int argc, char *argv []) {
int msqid;
struct msqid_ds msqid_ds;
if (argc != 3) {
fprintf (stderr, "Использование: %s идентификатор_очереди максимальный_размер\n", argv [0]);
return (1);
}
(void) sscanf (argv [1], "%d", &msqid);
/* Получим исходное значение структуры данных */
if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) {
perror ("IPC_STAT-1");
return (2);
}
printf ("Максимальный размер очереди до изменения: %ld\n", msqid_ds.msg_qbytes);
(void) sscanf (argv [2], "%d", (int *) &msqid_ds.msg_qbytes);
/* Попробуем внести изменения */
if (msgctl (msqid, IPC_SET, &msqid_ds) == -1) {
perror ("IPC_SET");
}
/* Получим новое значение структуры данных */
if (msgctl (msqid, IPC_STAT, &msqid_ds) == -1) {
perror ("IPC_STAT-2");
return (3);
}
printf ("Максимальный размер очереди после изменения: %ld\n", msqid_ds.msg_qbytes);
return 0;
}
Листинг 8.28. Пример программы управления очередями сообщений.
Две программы, показанные в листингах пример 8.29 и пример 8.30, демонстрируют полный цикл работы с очередями сообщений - от создания до удаления. Программа из пример 8.29 представляет собой родительский процесс, читающий строки со стандартного ввода и отправляющий их в виде сообщений процессу-потомку (пример 8.30). Последний принимает сообщения и выдает их тела на стандартный вывод. Предполагается, что программа этого процесса находится в файле msq_child текущего каталога.
#include
#include
#include
#include
#include
#include
/* Программа копирует строки со стандартного ввода на стандартный вывод, */
/* "прокачивая" их через очередь сообщений */
#define FTOK_FILE "/home/galat"
#define FTOK_CHAR "G"
#define MSGQ_MODE 0644
#define MY_PROMPT "Вводите строки\n"
#define MY_MSG "Вы ввели: "
int main (void) {
key_t key;
int msqid;
struct mymsgbuf {
long mtype;
char mtext [LINE_MAX];
} line_buf, msgbuf;
switch (fork ()) {
case -1:
perror ("FORK");
return (1);
case 0:
/* Чтение из очереди и выдачу на стандартный вывод */
/* реализуем в порожденном процессе. */
(void) execl ("./msq_child", "msq_child", FTOK_FILE, FTOK_CHAR, (char *) 0);
perror ("EXEC");
return (2); /* execl() завершился неудачей */
}
/* Чтение со стандартного ввода и запись в очередь */
/* возложим на родительский процесс */
/* Выработаем ключ для очереди сообщений */
if ((key = ftok (FTOK_FILE, FTOK_CHAR [0])) == (key_t) (-1)) {
perror ("FTOK");
return (3);
}
/* Получим идентификатор очереди сообщений */
if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) {
perror ("MSGGET");
return (4);
}
/* Приступим к отправке сообщений в очередь */
msgbuf.mtype = line_buf.mtype = 1;
strncpy (msgbuf.mtext, MY_PROMPT, sizeof (msgbuf.mtext));
if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) {
perror ("MSGSND-1");
return (5);
}
strncpy (msgbuf.mtext, MY_MSG, sizeof (msgbuf.mtext));
while (fgets (line_buf.mtext, sizeof (line_buf.mtext), stdin) != NULL) {
if (msgsnd (msqid, (void *) &msgbuf, strlen (msgbuf.mtext) + 1, 0) != 0) {
perror ("MSGSND-2");
break;
}
if (msgsnd (msqid, (void *) &line_buf, strlen (line_buf.mtext) + 1, 0) != 0) {
perror ("MSGSND-3");
break;
}
}
/* Удалим очередь */
if (msgctl (msqid, IPC_RMID, NULL) == -1) {
perror ("MSGCTL-IPC_RMID");
return (6);
}
return (0);
}
Листинг 8.29. Передающая часть программы работы с очередями сообщений.
#include
#include
#include
/* Программа получает сообщения из очереди */
/* и копирует их тела на стандартный вывод */
#define MSGQ_MODE 0644
int main (int argc, char *argv []) {
key_t key;
int msqid;
struct mymsgbuf {
long mtype;
char mtext [LINE_MAX];
} msgbuf;
if (argc != 3) {
fprintf (stderr, "Использование: %s имя_файла цепочка_символов\n", argv [0]);
return (1);
}
/* Выработаем ключ для очереди сообщений */
if ((key = ftok (argv [1], *argv [2])) == (key_t) (-1)) {
perror ("CHILD FTOK");
return (2);
}
/* Получим идентификатор очереди сообщений */
if ((msqid = msgget (key, IPC_CREAT | MSGQ_MODE)) < 0) {
perror ("CHILD MSGGET");
return (3);
}
/* Цикл приема сообщений и выдачи строк */
while (msgrcv (msqid, (void *) &msgbuf, sizeof (msgbuf.mtext), 0, 0) > 0) {
if (fputs (msgbuf.mtext, stdout) == EOF) {
break;
}
}
return 0;
}
Листинг 8.30. Приемная часть программы работы с очередями сообщений.
Обратим внимание на способ выработки согласованного ключа, а также на то, что, вообще говоря, неизвестно, какой из процессов - родительский или порожденный - создаст очередь, а какой получит уже ассоциированный с ключом идентификатор (вызовы msgget() в обоих процессах одинаковы), но на корректность работы программы это не влияет.
- 1 2 3 4 5
1.4 Семафоры
Согласно определению стандарта POSIX-2001, семафор - это минимальный примитив синхронизации, служащий основой для более сложных механизмов синхронизации, определенных в прикладной программе.
У семафора есть значение, которое представляется целым числом в диапазоне от 0 до 32767.
Семафоры создаются (функцией semget()) и обрабатываются (функцией semop()) наборами (массивами), причем операции над наборами с точки зрения приложений являются атомарными. В рамках групповых операций для любого семафора из набора можно сделать следующее: увеличить значение, уменьшить значение, дождаться обнуления.
Процессы, обладающие соответствующими правами, также могут выполнять различные управляющие действия над семафорами. Для этого служит функция semctl().
Описание перечисленных функций представлено в пример 8.31.
#include
int semget (key_t key, int nsems, int semflg);
int semop (int semid, struct sembuf *sops,
size_t nsops);
int semctl (int semid, int semnum,
int cmd, ...);
Листинг 8.31. Описание функций для работы с семафорами.
Структура semid_ds, ассоциированная с идентификатором набора семафоров, должна содержать по крайней мере следующие поля.
struct ipc_perm sem_perm;
/* Данные о правах доступа к
набору семафоров */
unsigned short sem_nsems;
/* Число семафоров в наборе */
time_t sem_otime;
/* Время последней операции semop() */
time_t sem_ctime;
/* Время последнего изменения
посредством semctl() */
Отдельные семафоры из набора представляются безымянной структурой, состоящей по крайней мере из следующих полей.
unsigned short semval;
/* Значение семафора */
pid_t sempid;
/* Идентификатор процесса, выполнившего
последнюю операцию над семафором */
unsigned short semncnt;
/* Число процессов, ожидающих увеличения
текущего значения семафора */
unsigned short semzcnt;
/* Число процессов, ожидающих обнуления
значения семафора */
Функция semget() аналогична msgget(); аргумент nsems задает число семафоров в наборе. Структура semid_ds инициализируется так же, как msqid_ds. Безымянные структуры, соответствующие отдельным семафорам, функцией semget() не инициализируются.
Операции, выполняемые посредством функции semop(), задаются массивом sops с числом элементов nsops, состоящим из структур типа sembuf , каждая из которых содержит по крайней мере следующие поля.
unsigned short sem_num;
/* Номер семафора в наборе (нумерация с нуля) */
short sem_op;
/* Запрашиваемая операция над семафором */
short sem_flg;
/* Флаги операции */
Операция над семафором определяется значением поля sem_op: положительное значение предписывает увеличить значение семафора на указанную величину, отрицательное - уменьшить, нулевое - сравнить с нулем. Вторая операция не может быть успешно выполнена, если в результате значение семафора становится отрицательным, а третья - если значение семафора ненулевое.
Выполнение массива операций с точки зрения пользовательского процесса является неделимым действием. Это значит, во-первых, что если операции выполняются, то только все вместе и, во-вторых, никакой другой процесс не может получить доступ к промежуточному состоянию набора семафоров, когда часть операций из массива уже выполнилась, а другая еще не успела. Операционная система, разумеется, выполняет операции из массива по очереди, причем порядок не оговаривается. Если очередная операция не может быть выполнена, то эффект предыдущих аннулируется, а вызов функции semop() приостанавливается (операция с блокировкой) или немедленно завершается неудачей (операция без блокировки). Подчеркнем, что в случае неудачного завершения вызова semop() значения всех семафоров в наборе останутся неизменными. Приведенный в пример 8.32 массив операций задает уменьшение (с блокировкой) семафора 1 при условии, что значение семафора 0 равно нулю.
sembuf [0].sem_num = 1;
sembuf [0].sem_flg = 0;
sembuf [0].sem_op = -2;
sembuf [1].sem_num = 0;
sembuf [1].sem_flg = IPC_NOWAIT;
sembuf [1].sem_op = 0;
Листинг 8.32. Пример задания массива операций над семафорами.
Обращаясь к функции semctl(), процессы могут получать информацию о состоянии набора семафоров, изменить ряд его характеристик, удалить набор.
Аргументы semid (идентификатор набора семафоров) и semnum (номер семафора в наборе) определяют объект, над которым выполняется управляющее действие, задаваемое значением аргумента cmd. Если объектом является набор, значение semnum игнорируется.
Для некоторых действий задействован четвертый аргумент (пример 8.33).
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
Листинг 8.33. Описание четвертого (дополнительного) аргумента функции semctl().
Среди допустимых действий - GETVAL (получить значение семафора и выдать его в качестве результата) и SETVAL (установить значение семафора равным arg.val). Имеются и аналогичные групповые действия - GETALL (прочитать значения всех семафоров набора и поместить их в массив arg.array) и SETALL (установить значения всех семафоров набора равными значениям элементов массива). Предусмотрены действия, позволяющие выяснить идентификатор процесса, выполнившего последнюю операцию над семафором (GETPID), а также число процессов, ожидающих увеличения/обнуления (GETNCNT/GETZCNT) значения семафора (информация о процессах выдается в качестве результата, пример 8.34).
val = semctl (semid, semnum, GETVAL);
arg.val = ...;
if (semctl (semid, semnum, SETVAL, arg) == -1) ...;
arg.array = (unsigned short *) malloc (nsems * sizeof (unsigned short));
err = semctl (semid, 0, GETALL, arg);
for (i = 0; i < nsems; i++) arg.array [i] = ...;
err = semctl (semid, 0, SETALL, arg);
lpid = semctl (semid, semnum, GETPID);
ncnt = semctl (semid, semnum, GETNCNT);
zcnt = semctl (semid, semnum, GETZCNT);
Листинг 8.34. Примеры управляющих действий над семафорами.
Наконец, для семафоров, как и для очередей сообщений, определены управляющие команды IPC_STAT (получить информацию о состоянии набора семафоров), IPC_SET (переустановить характеристики), IPC_RMID (удалить набор семафоров), представленные в пример 8.35.
arg.buf = (struct semid_ds *) malloc (sizeof (struct semid_ds);
err = semctl (semid, 0, IPC_STAT, arg);
arg.buf->sem_perm.mode = 0644;
err = semctl (semid, 0, IPC_SET, arg);
Листинг 8.35. Дополнительные примеры управляющих действий над семафорами.
В качестве примера использования семафоров рассмотрим известную задачу об обедающих философах. За круглым столом сидит несколько философов. В каждый момент времени каждый из них либо беседует, либо ест. Для еды одновременно требуется две вилки. Поэтому, прежде чем в очередной раз перейти от беседы к приему пищи, философу надо дождаться, пока освободятся обе вилки - слева и справа от него, и взять их в руки. Немного поев, философ кладет вилки на стол и вновь присоединяется к беседе. Требуется разработать программную модель обеда философов. Главное в этой задаче - корректная дисциплина захвата и освобождения вилок. В самом деле, если, например, каждый из философов одновременно с другими возьмется за вилку, лежащую слева от него, и будет ждать освобождения правой, обед не завершится никогда.
Предлагаемое решение состоит из двух программ. Первая (пример 8.36) реализует процесс-монитор, который порождает набор семафоров (по одному семафору на каждую вилку), устанавливает начальные значения семафоров (занятой вилке будет соответствовать значение 0, свободной - 1), запускает несколько процессов, представляющих философов, указывая место за столом (в качестве одного из аргументов передается число от 1 до QPH), ожидает, пока все процессы завершатся (все философы съедят свой обед), и удаляет набор семафоров. Предполагается (для нужд функции ftok()), что исходный текст программы находится в файле phdin.c (точнее, что такой файл существует).
#include
#include
#include
#include
/* Программа-монитор обеда философов */
#define QPH 5
#define ARG_SIZE 20
int main (void) {
int key; /* Ключ набора семафоров */
int semid; /* Идентификатор набора семафоров */
int no; /* Номер философа и/или вилки */
char ssemid [ARG_SIZE], sno [ARG_SIZE], sqph [ARG_SIZE];
/* Создание и инициализация набора семафоров */
/* (по семафору на вилку) */
key = ftok ("phdin.c", 'C');
if ((semid = semget (key, QPH, 0600 | IPC_CREAT)) < 0) {
perror ("SEMGET");
return (1);
}
for (no = 0; no < QPH; no++) {
if (semctl (semid, no, SETVAL, 1) < 0) {
perror ("SETVAL");
return (2);
}
}
sprintf (ssemid, "%d", semid);
sprintf (sqph, "%d", QPH);
/* Все - к столу */
for (no = 1; no <= QPH; no++) {
switch (fork ()) {
case -1:
perror ("FORK");
return (3);
case 0:
sprintf (sno, "%d", no);
execl ("./phil", "phil", ssemid, sqph, sno, (char *) 0);
perror ("EXEC");
return (4);
}
}
/* Ожидание завершения обеда */
for (no = 1; no <= QPH; no++) {
(void) wait (NULL);
}
/* Удаление набора семафоров */
if (semctl (semid, 0, IPC_RMID) < 0) {
perror ("SEMCTL");
return (5);
}
return 0;
}
Листинг 8.36. Процесс-монитор для обеда философов.
Вторая программа (пример 8.37) описывает обед каждого философа. Философ какое-то время беседует (случайное значение trnd), затем пытается взять вилки слева и справа от себя, когда ему это удается, некоторое время ест (случайное значение ernd), после чего освобождает вилки. Так продолжается до тех пор, пока не будет съеден весь обед. Предполагается, что выполнимый файл программы называется phil.
#include
#include
#include
#include
/* Процесс обеда одного философа */
#define ernd (rand () % 3 + 1)
#define trnd (rand () % 5 + 1)
#define FO 15
int main (int argc, char *argv []) {
int semid; /* Идентификатор набора семафоров */
int qph; /* Число философов */
int no; /* Номер философа */
int t; /* Время очередного отрезка еды или беседы */
int fo; /* Время до конца обеда */
struct sembuf sembuf [2];
if (argc != 4) {
fprintf (stderr, "Использование: %s идентификатор_набора_семафоров число_философов номер_философа \n", argv [0]);
return (1);
}
fo = FO;
sscanf (argv [1], "%d", &semid);
sscanf (argv [2], "%d", &qph);
sscanf (argv [3], "%d", &no);
/* Выбор вилок */
sembuf [0].sem_num = no - 1; /* Левая */
sembuf [0].sem_flg = 0;
sembuf [1].sem_num = no % qph; /* Правая */
sembuf [1].sem_flg = 0;
while (fo > 0) { /* Обед */
/* Философ говорит */
printf ("Философ %d беседует\n", no);
t = trnd; sleep (t); fo -= t;
/* Пытается взять вилки */
sembuf [0].sem_op = -1;
sembuf [1].sem_op = -1;
if (semop (semid, sembuf, 2) < 0) {
perror ("SEMOP");
return (1);
}
/* Ест */
printf ("Философ %d ест\n", no);
t = ernd; sleep (t); fo -= t;
/* Отдает вилки */
sembuf [0].sem_op = 1;
sembuf [1].sem_op = 1;
if (semop (semid, sembuf, 2) < 0) {
perror ("SEMOP");
return (2);
}
}
printf ("Философ %d закончил обед\n", no);
return 0;
}
Листинг 8.37. Программа, описывающая обед одного философа.
Отметим, что возможность выполнения групповых операций над семафорами предельно упростила решение, сделав его прямолинейным, по большому счету нечестным, но зато очевидным образом гарантирующим отсутствие тупиков.
В пример 8.38 приведен второй вариант решения задачи, предложенный С.В. Самборским. В нем реализованы четыре стратегии захвата вилок, которые сравниваются по результатам моделирования поведения философов в течение нескольких минут. Все стратегии гарантируют отсутствие тупиков, но только две из них, соответствующие опциям -a и -p, заведомо не позволят ни одному философу умереть от голода из-за невозможности получить обе вилки сразу. (Это свойство "стратегий -a и -p" является следствием упорядоченности ресурсов.)
/* Обедающие философы. Запуск:
mudrecProc [-a | -p | -I -V] [-t число_секунд] имя_философа ...
Опции:
-t число_секунд - сколько секунд моделируется
Стратегии захвата вилок:
-a - сначала захватывается вилка с меньшим номером;
-I - некорректная (но эффективная) интеллигентная стратегия: во время
ожидания уже захваченная вилка кладется;
-p - сначала захватывается нечетная вилка;
-V - использован групповой захват семафоров.
Пример запуска: mudrecProc -p -t 600 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
*/
static char rcsid[] __attribute__((unused)) = \
"$Id: mudrecProc.c,v 1.7 2003/11/11 13:14:07 sambor Exp $";
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)>(b)?(b):(a))
struct mudrec {
long num;
char *name;
int left_fork, right_fork;
int eat_time, wait_time, think_time, max_wait_time;
int count;
};
int Stop; /* Семафор для синхронизации выхода */
/* Различные дескрипторы */
int protokol [2] = {-1, -1};
#define pFdIn (protokol [1])
#define pFdOut (protokol [0])
int semFork; /* Вилки */
int from_fil; /* Очередь для возврата результатов */
/* Разные алгоритмы захвата вилок */
static void get_forks_simple (struct mudrec *this);
static void get_forks_parity (struct mudrec *this);
static void get_forks_maybe_infinit_time (struct mudrec *this);
static void get_forks_use_groups (struct mudrec *this);
/* Используемый метод захвата вилок */
void (*get_forks) (struct mudrec *this) = get_forks_simple;
/* Возвращение вилок */
static void put_forks (struct mudrec *this);
/*
* Философы
*/
void filosof (struct mudrec this) {
char buffer [LINE_MAX];
int bytes;
if (fork ()) return;
srandom (getpid ()); /* Очень важно для процессов, иначе получим одно и то же! */
random (); random (); random (); random (); random ();
random (); random (); random (); random (); random ();
/* Пока семафор Stop не поднят */
while (!semctl (Stop, 0, GETVAL)) {
/* Пора подкрепиться */
{
int wait_time, tm;
sprintf (buffer, "%s: хочет есть\n", this.name);
bytes = write (pFdIn, buffer, strlen (buffer));
tm = time (0);
(*get_forks) (&this);
wait_time = time (0) - tm; /* Сколько времени получали вилки */
this.wait_time += wait_time;
this.max_wait_time = max (wait_time, this.max_wait_time);
sprintf (buffer, "%s: ждал вилок %d сек\n", this.name, wait_time);
bytes = write (pFdIn, buffer, strlen (buffer));
}
/* Может, обед уже закончился? */
if (semctl (Stop, 0, GETVAL)) {
put_forks (&this);
break;
}
/* Едим */
{
int eat_time = random () % 20 + 1;
sleep (eat_time);
this.eat_time += eat_time;
this.count++;
sprintf (buffer,"%s: ел %d сек\n", this.name, eat_time);
bytes = write (pFdIn, buffer, strlen (buffer));
}
/* Отдаем вилки */
put_forks (&this);
if (semctl (Stop, 0, GETVAL)) break;
/* Размышляем */
{
int think_time = random () % 10 + 1;
sleep (think_time);
this.think_time += think_time;
}
}
sprintf (buffer,"%s: уходит\n", this.name);
bytes = write (pFdIn, buffer, strlen (buffer));
msgsnd (from_fil, &this, sizeof (this), 0); /* Отослали статистику своего обеда */
_exit (0); /* ВАЖНО (_): Нам не нужны преждевременные вызовы cleanup_ipc */
}
/* Кладем вилки одну за другой */
static void put_forks (struct mudrec *this) {
struct sembuf tmp_buf;
tmp_buf.sem_flg = 0;
tmp_buf.sem_op = 1;
tmp_buf.sem_num = this->left_fork - 1;
semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = 0;
tmp_buf.sem_op = 1;
tmp_buf.sem_num = this->right_fork - 1;
semop (semFork, &tmp_buf, 1);
}
/* Берем вилки по очереди в порядке номеров */
static void get_forks_simple (struct mudrec *this) {
struct sembuf tmp_buf;
int first = min (this->left_fork, this->right_fork);
int last = max (this->left_fork, this->right_fork);
tmp_buf.sem_flg = SEM_UNDO;
tmp_buf.sem_op = -1;
tmp_buf.sem_num = first - 1;
semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO;
tmp_buf.sem_op = -1;
tmp_buf.sem_num = last - 1;
semop (semFork, &tmp_buf, 1);
}
/* Берем сначала нечетную вилку (если обе нечетные - то с большим номером) */
static void get_forks_parity (struct mudrec *this) {
struct sembuf tmp_buf;
int left = this->left_fork, right = this->right_fork;
int first = max ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000;
int last = min ((left & 1) * 1000 + left, (right & 1) * 1000 + right) % 1000;
tmp_buf.sem_flg = SEM_UNDO;
tmp_buf.sem_op = -1;
tmp_buf.sem_num = first - 1;
semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO;
tmp_buf.sem_op = -1;
tmp_buf.sem_num = last - 1;
semop (semFork, &tmp_buf, 1);
}
/* Берем вилки по очереди, в произвольном порядке.
* Но если вторая вилка не берется сразу, то кладем первую.
* То есть философ не расходует вилочное время впустую.
*/
static void get_forks_maybe_infinit_time (struct mudrec *this) {
struct sembuf tmp_buf;
int left = this->left_fork, right = this->right_fork;
for (;;) {
tmp_buf.sem_flg = SEM_UNDO; /* Первую вилку берем с ожиданием */
tmp_buf.sem_op = -1;
tmp_buf.sem_num = left - 1;
semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Вторую - без ожидания */
tmp_buf.sem_op = -1;
tmp_buf.sem_num = right - 1;
if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */
tmp_buf.sem_flg = 0; /* Неуспех: возвращаем первую вилку */
tmp_buf.sem_op = 1;
tmp_buf.sem_num = left - 1;
semop(semFork,&tmp_buf,1);
tmp_buf.sem_flg = SEM_UNDO; /* Отдав первую, ждем вторую */
tmp_buf.sem_op = -1;
tmp_buf.sem_num = right - 1;
semop (semFork, &tmp_buf, 1);
tmp_buf.sem_flg = SEM_UNDO | IPC_NOWAIT; /* Берем первую вилку без ожидания */
tmp_buf.sem_op = -1;
tmp_buf.sem_num = left - 1;
if (0 == semop (semFork, &tmp_buf, 1)) return; /* Успех */
tmp_buf.sem_flg = 0; /* Неуспех: отдаем вторую вилку, */
tmp_buf.sem_op = 1; /* чтобы ждать первую */
tmp_buf.sem_num = right - 1;
semop (semFork, &tmp_buf, 1);
}
}
/* Хватаем обе вилки сразу, используя групповые операции */
static void get_forks_use_groups (struct mudrec *this) {
struct sembuf tmp_buf [2];
tmp_buf[0].sem_flg = SEM_UNDO;
tmp_buf[0].sem_op = -1;
tmp_buf[0].sem_num = this->left_fork - 1;
tmp_buf[1].sem_flg = SEM_UNDO;
tmp_buf[1].sem_op = -1;
tmp_buf[1].sem_num = this->right_fork - 1;
semop (semFork, tmp_buf, 2);
}
/*
* Мелкие служебные функции.
*/
static void stop (int dummy) {
struct sembuf tmp_buf;
tmp_buf.sem_flg = 0;
tmp_buf.sem_op = 1;
tmp_buf.sem_num = 0;
semop (Stop, &tmp_buf, 1);
}
void cleanup_ipc (void) {
/*
* Уничтожение семафоров.
*/
semctl (semFork, 1, IPC_RMID);
semctl (Stop, 1, IPC_RMID);
/* То же с очередью */
msgctl (from_fil, IPC_RMID, NULL);
}
static void usage (char name []) {
fprintf (stderr,"Использование: %s [-a | -p | -I| -V] [-t число_секунд] имя_философа ...\n", name);
exit (1);
}
/*
* Точка входа демонстрационной программы.
*/
int main (int argc, char *argv[]) {
char buffer [LINE_MAX], *p;
int i, n, c;
int open_room_time = 300;
union semun tmp_arg;
int nMudr;
struct sigaction sact;
while ((c = getopt (argc, argv, "apIVt:")) != -1) {
switch (c) {
case 'a': get_forks = get_forks_simple; break;
case 'p': get_forks = get_forks_parity; break;
case 'I': get_forks = get_forks_maybe_infinit_time; break;
case 'V': get_forks = get_forks_use_groups; break;
case 't': open_room_time = strtol (optarg, &p, 0);
if (optarg [0] == 0 || *p != 0) usage (argv [0]);
break;
default: usage (argv [0]);
}
}
nMudr = argc - optind;
if (nMudr < 2) usage (argv [0]); /* Меньше двух философов неинтересно ... */
/*
* Создание канала для протокола обработки событий
*/
pipe (protokol);
/*
* Создадим семафоры для охраны вилок
*/
semFork = semget (ftok (argv [0], 2), nMudr, IPC_CREAT | 0777);
tmp_arg.val = 1;
for (i=1; i <= nMudr; i++)
semctl (semFork, i - 1, SETVAL, tmp_arg); /* Начальное значение 1 */
/* Прежде чем впускать философов, обеспечим окончание обеда */
Stop = semget (ftok (argv [0], 3), 1, IPC_CREAT | 0777);
tmp_arg.val = 0;
semctl (Stop, 0, SETVAL, tmp_arg); /* Начальное значение 0 */
/* Очередь для возврата результатов */
from_fil = msgget (ftok (argv [0], 4), IPC_CREAT | 0777);
atexit (cleanup_ipc); /* Запланировали уничтожение семафоров */
/* и других средств межпроцессного взаимодействия */
/*
* Философы входят в столовую
*/
for (i = 0; i < nMudr; i++, optind++) {
struct mudrec next;
memset (&next, 0, sizeof (next));
next.num = i + 1; /* Номер */
next.name = argv [optind]; /* Имя */
/* Указали, какими вилками пользоваться */
next.left_fork = i + 1;
next.right_fork = i + 2;
if (i == nMudr - 1)
next.right_fork = 1; /* Последний пользуется вилкой первого */
filosof (next);
}
/* Зададим реакцию на сигналы и установим будильник на конец обеда */
sact.sa_handler = stop;
(void) sigemptyset (&sact.sa_mask);
sact.sa_flags = 0;
(void) sigaction (SIGINT, &sact, (struct sigaction *) NULL);
(void) sigaction (SIGALRM, &sact, (struct sigaction *) NULL);
alarm (open_room_time);
/*
* Выдача сообщений на стандартный вывод и выход после окончания обеда.
*/
close (pFdIn); /* Сами должны закрыть, иначе из цикла не выйдем! */
for (;;) {
n = read (pFdOut, buffer, LINE_MAX);
if ((n == 0) || ((n == -1) && (errno != EINTR))) break;
for (i = 0; i < n; i++) putchar (buffer [i]);
}
close (pFdOut);
/* Распечатали сводную информацию */
{
int full_eating_time = 0;
int full_waiting_time = 0;
int full_thinking_time = 0;
for (i = 1; i <= nMudr; i++) {
struct mudrec this;
/* Получили статистику обеда */
msgrcv (from_fil, &this, sizeof (this), i, 0); /* За счет i получаем */
/* строго по порядку */
full_eating_time += this.eat_time;
full_waiting_time += this.wait_time;
full_thinking_time += this.think_time;
if (this.count > 0) {
float count = this.count;
float think_time = this.think_time / count;
float eat_time = this.eat_time / count;
float wait_time = this.wait_time / count;
printf ("%s: ел %d раз в среднем: думал=%.1f ел=%.1f ждал=%.1f (максимум %d)\n",
this.name, this.count, think_time, eat_time, wait_time, this.max_wait_time);
}
else
printf("%s: не поел\n", this.name);
}
{
float total_time = (full_eating_time + full_waiting_time
+ full_thinking_time) / (float)nMudr;
printf (" Среднее число одновременно едящих = %.3f\n Среднее число одновременно ждущих = %.3f\n",
full_eating_time / total_time, full_waiting_time / total_time);
}
}
/* Сообщим об окончании работы */
printf ("Конец обеда\n");
return 0;
}
Листинг 8.38. Второй вариант решения задачи об обедающих философах.
Получит ли в конце концов философ вилки при групповых операциях (опция -V), зависит от реализации. Может случиться так, что хотя бы одна из них в каждый момент времени будет в руках у одного из соседей. То же верно и для "интеллигентной" стратегии (опция -I). Тем не менее, результаты моделирования показывают, что на практике две последние стратегии эффективнее в смысле минимизации времени ожидания вилок.
Отметим небольшие терминологические различия в двух приведенных вариантах решения задачи об обедающих философах. Во втором варианте явно выделены начальные и конечные моделируемые события - вход философов в столовую и выход из нее (в первом варианте они просто сидят за столом).
С методической точки зрения второй вариант интересен тем, что в нем использованы все рассмотренные нами средства межпроцессного взаимодействия - каналы, сигналы, очереди сообщений и, конечно, семафоры. (Тонкость: флаг SEM_UNDO обеспечивает корректировку значения семафора при завершении процесса.)
В пример 8.39 приведена статистика поведения пяти философов для всех четырех стратегий при времени моделирования 100 секунд. Эти результаты говорят в пользу групповых операций над семафорами.
-a:
A: ел 2 раза в среднем: думал=3.5 ел=11.5 ждал=36.5 (максимум 73)
B: ел 3 раза в среднем: думал=5.7 ел=7.7 ждал=20.0 (максимум 41)
C: ел 3 раза в среднем: думал=5.7 ел=11.3 ждал=17.0 (максимум 33)
D: ел 3 раза в среднем: думал=1.7 ел=16.7 ждал=15.7 (максимум 19)
E: ел 1 раз в среднем: думал=10.0 ел=20.0 ждал=73.0 (максимум 41)
Среднее число одновременно едящих = 1.471
Среднее число одновременно ждущих = 2.980
-p:
A: ел 3 раза в среднем: думал=3.7 ел=15.3 ждал=16.0 (максимум 34)
B: ел 4 раза в среднем: думал=5.0 ел=13.8 ждал=8.2 (максимум 15)
C: ел 3 раза в среднем: думал=6.7 ел=3.7 ждал=25.7 (максимум 27)
D: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=13.8 (максимум 28)
E: ел 3 раза в среднем: думал=5.3 ел=15.3 ждал=16.7 (максимум 29)
Среднее число одновременно едящих = 1.761
Среднее число одновременно ждущих = 2.413
-I:
A: ел 5 раз в среднем: думал=4.2 ел=9.4 ждал=6.6 (максимум 15)
B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=17.0 (максимум 31)
C: ел 4 раза в среднем: думал=6.8 ел=7.0 ждал=12.2 (максимум 45)
D: ел 3 раза в среднем: думал=4.3 ел=16.0 ждал=13.0 (максимум 16)
E: ел 4 раза в среднем: думал=5.8 ел=8.5 ждал=10.8 (максимум 22)
Среднее число одновременно едящих = 1.858
Среднее число одновременно ждущих = 2.125
-V:
A: ел 5 раз в среднем: думал=5.6 ел=5.6 ждал=8.8 (максимум 17)
B: ел 3 раза в среднем: думал=6.3 ел=10.3 ждал=16.7 (максимум 20)
C: ел 4 раза в среднем: думал=4.8 ел=11.0 ждал=9.8 (максимум 18)
D: ел 4 раза в среднем: думал=5.2 ел=12.0 ждал=8.8 (максимум 15)
E: ел 4 раза в среднем: думал=5.2 ел=10.5 ждал=10.2 (максимум 20)
Среднее число одновременно едящих = 1.892
Среднее число одновременно ждущих = 2.049
Листинг 8.39. Результаты моделирования поведения философов.
- 1 2 3 4 5