Билеты (Graur) (1114773), страница 14
Текст из файла (страница 14)
typedef int semaphore; /* тип данных «семафор» */
semaphore mutex = 1; /* контроль за доступом к «rc» (разделямый ресурс) */
semaphore db = 1; /* контроль за доступом к базе данных */
int rc = 0; /* кол-во процессов читающих или пишущих */
void reader (void)
{
while (TRUE) /* бесконечный цикл */
{
down(&mutex); /* получить эксклюзивный доступ к «rc»*/
rc = rc + 1; /* еще одним читателем больше */
if (rc == 1) down(&db); /* если это первый читатель, нужно заблокировать эксклюзивный доступ к базе */
up(&mutex); /*освободить ресурс rc */
read_data_base(); /* доступ к данным */
down(&mutex); /*получить эксклюзивный доступ к «rc»*/
rc = rc - 1: /* теперь одним читателем меньше */
if (rc == 0) up(&db); /*если это был последний читатель, разблокировать эксклюзивный доступ к базе данных */
up(&mutex); /*освободить разделяемый ресурс rc */
use_data_read(); /* некритическая секция */
}
}
void writer (void)
{
while(TRUE) /* бесконечный цикл */
{
think_up_data(); /* некритическая секция */
down(&db); /* получить эксклюзивный доступ к данным*/
write_data_base(); /* записать данные */
up(&db); /* отдать эксклюзивный доступ */
}
}
В этом примере, первый процесс, обратившийся к базе данных по чтению, осуществляет операцию down() над семафором db, тем самым блокируя эксклюзивный доступ к базе, который нужен для записи. Число процессов, осуществляющих чтение в данный момент, определяется переменной rc (обратите внимание! Так как переменная rc является разделяемым ресурсом – ее изменяют все процессы, обращающиеся к базе данных по чтению – то доступ к ней охраняется семафором mutex). Когда читающий процесс заканчивает свою работу, он уменьшает значение rc на единицу. Если он является последним читателем, он также совершает операцию up над семафором db, тем самым разрешая заблокированному писателю, если таковой имелся, получить эксклюзивный доступ к базе для записи.
Надо заметить, что приведенный алгоритм дает преимущество при доступе к базе данных процессам-читателям, так как процесс, ожидающий доступа по записи, будет ждать до тех пор, пока все читающие процессы не окончат работу, и если в это время появляется новый читающий процесс, он тоже беспрепятственно получит доступ. Это может привести к неприятной ситуации в том случае, если в фазе, когда ресурс доступен по чтению, и имеется ожидающий процесс-писатель, будут появляться новые и новые читающие процессы. К примеру, представим себе, что новый читающий процесс появляется каждую секунду и чтение длится в среднем 2 секунды. Количество читающих процессов в этом случае будет приблизительно константным, и процесс-писатель никогда не получит доступ к данным.
Чтобы этого избежать, можно модифицировать алгоритм таким образом, чтобы в случае, если имеется хотя бы один ожидающий процесс-писатель, новые процессы-читатели не получали доступа к ресурсу, а ожидали, когда процесс-писатель обновит данные. В этой ситуации процесс-писатель должен будет ожидать окончания работы с ресурсом только тех читателей, которые получили доступ раньше, чем он его запросил. Однако, обратная сторона данного решения в том, что оно несколько снижает производительность процессов-читателей, так как вынуждает их ждать в тот момент, когда ресурс не занят в эксклюзивном режиме.
БИЛЕТ 35
Задача о «спящем парикмахере»
Еще одна классическая задача на синхронизацию процессов – это так называемая «задача о спящем парикмахере» [2]. Рассмотрим парикмахерскую, в которой работает один парикмахер, имеется одно кресло для стрижки и несколько кресел в приемной для посетителей, ожидающих своей очереди. Если в парикмахерской нет посетителей, парикмахер засыпает прямо на своем рабочем месте. Появившийся посетитель должен его разбудить, в результате чего парикмахер приступает к работе. Если в процессе стрижки появляются новые посетители, они должны либо подождать своей очереди, либо покинуть парикмахерскую, если в приемной нет свободного кресла для ожидания. Задача состоит в том, чтобы корректно запрограммировать поведение парикмахера и посетителей.
Одно из возможных решений этой задачи представлено ниже. Процедура barber() описывает поведение парикмахера (она включает в себя бесконечный цикл – ожидание клиентов и стрижку). Процедура customer() описывает поведение посетителя. Несмотря на кажущуюся простоту задачи, понадобится целых 3 семафора: customers – подсчитывает количество посетителей, ожидающих в очереди, barbers – обозначает количество свободных парикмахеров (в случае одного парикмахера его значение либо 0, либо 1) и mutex – используется для синхронизации доступа к разделяемой переменной waiting. Переменная waiting, как и семафор customers, содержит количество посетителей, ожидающих в очереди, она используется в программе для того, чтобы иметь возможность проверить, имеется ли свободное кресло для ожидания, и при этом не заблокировать процесс, если кресла не окажется. Заметим, что как и в предыдущем примере, эта переменная является разделяемым ресурсом, и доступ к ней охраняется семафором mutex. Это необходимо, так как для обычной переменной, в отличие от семафора, чтение и последующее изменение не являются неделимой операцией.
#define CHAIRS 5
typedef int semaphore; /* тип данных «семафор» */
semaphore customers = 0; /* посетители, ожидающие в очереди */
semaphore barbers = 0; /* парикмахеры, ожидающие посетителей */
semaphore mutex = 1; /* контроль за доступом к переменной waiting */
int waiting = 0;
void barber()
{
while (true) {
down(customers); /* если customers == 0, т.е. посетителей нет, то заблокируемся до появления посетителя */
down(mutex); /* получаем доступ к waiting */
waiting = wating – 1; /* уменьшаем кол-во ожидающих клиентов */
up(barbers); /* парикмахер готов к работе */
up(mutex); /* освобождаем ресурс waiting */
cut_hair(); /* процесс стрижки */
}
void customer()
{
down(mutex); /* получаем доступ к waiting */
if (waiting < CHAIRS) /* есть место для ожидания */
{
waiting = waiting + 1; /* увеличиваем кол-во ожидающих клиентов */
up(customers); /* если парикмахер спит, это его разбудит */
up(mutex); /* освобождаем ресурс waiting */
down(barbers); /* если парикмахер занят, переходим в состояние ожидания, иначе – занимаем парикмахера*/
get_haircut(); /* процесс стрижки */
}
else
{
up(mutex); /* нет свободного кресла для ожидания – придется уйти */
}
}
БИЛЕТ 36 Сигналы. Примеры программирования.
Сигналы.
Сигналы представляют собой средство уведомления процесса о наступлении некоторого события в системе. Инициатором посылки сигнала может выступать как другой процесс, так и сама ОС. Сигналы, посылаемые ОС, уведомляют о наступлении некоторых строго предопределенных ситуаций (как, например, завершение порожденного процесса, прерывание процесса нажатием комбинации Ctrl-C, попытка выполнить недопустимую машинную инструкцию, попытка недопустимой записи в канал и т.п.), при этом каждой такой ситуации сопоставлен свой сигнал. Кроме того, зарезервирован один или несколько номеров сигналов, семантика которых определяется пользовательскими процессами по своему усмотрению (например, процессы могут посылать друг другу сигналы с целью синхронизации).
Количество различных сигналов в современных версиях UNIX около 30, каждый из них имеет уникальное имя и номер. Описания представлены в файле <signal.h>. В таблице приведено несколько примеров сигналов:
Числовое значение | Константа | Значение сигнала |
2 | SIGINT | Прерывание выполнения по нажатию Ctrl-C |
3 | SIGQUIT | Аварийное завершение работы |
9 | SIGKILL | Уничтожение процесса |
14 | SIGALRM | Прерывание от программного таймера |
18 | SIGCHLD | Завершился процесс-потомок |
Сигналы являются механизмом асинхронного взаимодействия, т.е. момент прихода сигнала процессу заранее неизвестен. Однако процесс может предвидеть возможность получения того или иного сигнала и установить определенную реакцию на его приход. В этом плане сигналы можно рассматривать как программный аналог аппаратных прерываний.
При получении сигнала процессом возможны три варианта реакции на полученный сигнал:
-
Процесс реагирует на сигнал стандартным образом, установленным по умолчанию (для большинства сигналов действие по умолчанию – это завершение процесса).
-
Процесс может установить специальную обработку сигнала, в этом случае по приходу сигнала вызывается функция-обработчик, определенная процессом (при этом говорят, что сигнал перехватывается)
-
Процесс может проигнорировать сигнал.
Для каждого сигнала процесс может устанавливать свой вариант реакции, например, некоторые сигналы он может игнорировать, некоторые перехватывать, а на остальные установить реакцию по умолчанию. При этом в процессе своей работы процесс может изменять вариант реакции на тот или иной сигнал. Однако, необходимо отметить, что некоторые сигналы невозможно ни перехватить, ни игнорировать. Они используются ядром ОС для управления работой процессов (например, SIGKILL, SIGSTOP).
Если в процесс одновременно доставляется несколько различных сигналов, то порядок их обработки не определен. Если же обработки ждут несколько экземпляров одного и того же сигнала, то ответ на вопрос, сколько экземпляров будет доставлено в процесс – все или один – зависит от конкретной реализации ОС.
Отдельного рассмотрения заслуживает ситуация, когда сигнал приходит в момент выполнения системного вызова. Обработка такой ситуации в разных версиях UNIX реализована по-разному, например, обработка сигнала может быть отложена до завершения системного вызова; либо системный вызов автоматически перезапускается после его прерывания сигналом; либо системный вызов вернет –1, а в переменной errno будет установлено значение EINTR
Для отправки сигнала существует системный вызов kill():
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int sig)
Первым параметром вызова служит идентификатор процесса, которому посылается сигнал (в частности, процесс может послать сигнал самому себе). Существует также возможность одновременно послать сигнал нескольким процессам, например, если значение этого параметра есть 0, сигнал будет передан всем процессам, которые принадлежат той же группе, что и процесс, посылающий сигнал, за исключением процессов с идентификаторами 0 и 1.
Во втором параметре передается номер посылаемого сигнала. Если этот параметр равен 0, то будет выполнена проверка корректности обращения к kill() (в частности, существование процесса с идентификатором pid), но никакой сигнал в действительности посылаться не будет.
Если процесс-отправитель не обладает правами привилегированного пользователя, то он может отправить сигнал только тем процессам, у которых реальный или эффективный идентификатор владельца процесса совпадает с реальным или эффективным идентификатором владельца процесса-отправителя.
Для определения реакции на получение того или иного сигнала в процессе служит системный вызов signal():
#include <signal.h>
void (*signal ( int sig, void (*disp) (int))) (int)
где аргумент sig — номер сигнала, для которого устанавливается реакция, а disp — либо определенная пользователем функция-обработчик сигнала, либо одна из констант: SIG_DFL и SIG_IGN. Первая из них указывает, что необходимо установить для данного сигнала обработку по умолчанию, т.е. стандартную реакцию системы, а вторая — что данный сигнал необходимо игнорировать. При успешном завершении функция возвращает указатель на предыдущий обработчик данного сигнала (он может использоваться процессом, например, для восстановления прежней реакции на сигнал).
Как видно из прототипа вызова signal(), определенная пользователем функция-обработчик сигнала должна принимать один целочисленный аргумент (в нем будет передан номер обрабатываемого сигнала), и не возвращать никаких значений.
Отметим одну особенность реализации сигналов в ранних версиях UNIX: каждый раз при получении сигнала его диспозиция (т.е. действие при получении сигнала) сбрасывается на действие по умолчанию, т.о. если процесс желает многократно обрабатывать сигнал своим собственным обработчиком, он должен каждый раз при обработке сигнала заново устанавливать реакцию на него (см. II)