Главная страница

Околения компьютеров


Скачать 1.67 Mb.
НазваниеОколения компьютеров
Дата18.04.2022
Размер1.67 Mb.
Формат файлаpdf
Имя файлаOS_Graur.pdf
ТипДокументы
#481658
страница10 из 20
1   ...   6   7   8   9   10   11   12   13   ...   20
down() над семафором db, тем самым блокируя эксклюзивный доступ к базе, который нужен для записи. Число процессов, осуществляющих чтение в данный момент, определяется переменной rc (обратите внимание! Так как переменная rc является разделяемым ресурсом – ее изменяют все процессы, обращающиеся к базе данных по чтению – то доступ к ней охраняется семафором mutex). Когда читающий процесс заканчивает свою работу, он уменьшает значение rc на единицу. Если он является последним читателем, он также совершает операцию up над семафором db, тем самым разрешая заблокированному писателю, если таковой имелся, получить эксклюзивный доступ к базе для записи.
Надо заметить, что приведенный алгоритм дает преимущество при доступе к базе данных процессам-читателям, так как процесс, ожидающий доступа по записи, будет ждать до тех пор, пока все читающие процессы не окончат работу, и если в это время появляется новый читающий процесс, он тоже беспрепятственно получит доступ. Это может привести к неприятной ситуации в том случае, если в фазе, когда ресурс доступен по чтению, и имеется ожидающий процесс-писатель, будут появляться новые и новые читающие процессы. К примеру, представим себе, что новый читающий процесс появляется каждую секунду и чтение длится в среднем 2 секунды. Количество читающих процессов в этом случае будет приблизительно константным, и процесс-писатель никогда не получит доступ к данным.
Чтобы этого избежать, можно модифицировать алгоритм таким образом, чтобы в случае, если имеется хотя бы один ожидающий процесс-писатель, новые процессы- читатели не получали доступа к ресурсу, а ожидали, когда процесс-писатель обновит данные. В этой ситуации процесс-писатель должен будет ожидать окончания работы с ресурсом только тех читателей, которые получили доступ раньше, чем он его запросил. Однако, обратная сторона данного решения в том, что оно несколько снижает производительность процессов-читателей, так как вынуждает их ждать в тот момент, когда ресурс не занят в эксклюзивном режиме.

БИЛЕТ 29 Сигналы. Примеры программирования.
Сигналы.
Сигналы представляют собой средство уведомления процесса о наступлении некоторого события в системе. Инициатором посылки сигнала может выступать как другой процесс, так и сама ОС. Сигналы, посылаемые ОС, уведомляют о наступлении некоторых строго предопределенных ситуаций (как, например, завершение порожденного процесса, прерывание процесса нажатием комбинации
Ctrl-C, попытка выполнить недопустимую машинную инструкцию, попытка недопустимой записи в канал и т.п.), при этом каждой такой ситуации сопоставлен свой сигнал. Кроме того, зарезервирован один или несколько номеров сигналов, семантика которых определяется пользовательскими процессами по своему усмотрению (например, процессы могут посылать друг другу сигналы с целью синхронизации).
Количество различных сигналов в современных версиях UNIX около 30, каждый из них имеет уникальное имя и номер. Описания представлены в файле .
В таблице приведено несколько примеров сигналов:
Числовое значение
Константа Значение сигнала
2 SIGINT
Прерывание выполнения по нажатию Ctrl-C
3 SIGQUIT
Аварийное завершение работы
9 SIGKILL
Уничтожение процесса
14 SIGALRM
Прерывание от программного таймера
18 SIGCHLD
Завершился процесс-потомок
Сигналы являются механизмом асинхронного взаимодействия, т.е. момент прихода сигнала процессу заранее неизвестен. Однако процесс может предвидеть возможность получения того или иного сигнала и установить определенную реакцию на его приход. В этом плане сигналы можно рассматривать как программный аналог аппаратных прерываний.
При получении сигнала процессом возможны три варианта реакции на полученный сигнал:
- Процесс реагирует на сигнал стандартным образом, установленным по умолчанию (для большинства сигналов действие по умолчанию – это завершение процесса).
- Процесс может установить специальную обработку сигнала, в этом случае по приходу сигнала вызывается функция-обработчик, определенная процессом (при этом говорят, что сигнал перехватывается)
- Процесс может проигнорировать сигнал.
Для каждого сигнала процесс может устанавливать свой вариант реакции, например, некоторые сигналы он может игнорировать, некоторые перехватывать, а на остальные установить реакцию по умолчанию. При этом в процессе своей работы процесс может изменять вариант реакции на тот или иной сигнал. Однако, необходимо отметить, что некоторые сигналы невозможно ни перехватить, ни игнорировать. Они используются ядром ОС для управления работой процессов
(например, SIGKILL, SIGSTOP).
Если в процесс одновременно доставляется несколько различных сигналов, то порядок их обработки не определен. Если же обработки ждут несколько
экземпляров одного и того же сигнала, то ответ на вопрос, сколько экземпляров будет доставлено в процесс – все или один – зависит от конкретной реализации ОС.
Отдельного рассмотрения заслуживает ситуация, когда сигнал приходит в момент выполнения системного вызова. Обработка такой ситуации в разных версиях UNIX реализована по-разному, например, обработка сигнала может быть отложена до завершения системного вызова; либо системный вызов автоматически перезапускается после его прерывания сигналом; либо системный вызов вернет –1, а в переменной errno будет установлено значение EINTR
Для отправки сигнала существует системный вызов kill():
#include
#INCLUDE
INT KILL (PID_T PID, INT SIG)
Первым параметром вызова служит идентификатор процесса, которому посылается сигнал (в частности, процесс может послать сигнал самому себе). Существует также возможность одновременно послать сигнал нескольким процессам, например, если значение этого параметра есть 0, сигнал будет передан всем процессам, которые принадлежат той же группе, что и процесс, посылающий сигнал, за исключением процессов с идентификаторами 0 и 1.
Во втором параметре передается номер посылаемого сигнала. Если этот параметр равен 0, то будет выполнена проверка корректности обращения к kill() (в частности, существование процесса с идентификатором pid), но никакой сигнал в действительности посылаться не будет.
Если процесс-отправитель не обладает правами привилегированного пользователя, то он может отправить сигнал только тем процессам, у которых реальный или эффективный идентификатор владельца процесса совпадает с реальным или эффективным идентификатором владельца процесса-отправителя.
Для определения реакции на получение того или иного сигнала в процессе служит системный вызов signal():
#INCLUDE
void (*signal ( int sig, void (*disp) (int))) (int) где аргумент sig — номер сигнала, для которого устанавливается реакция, а disp
— либо определенная пользователем функция-обработчик сигнала, либо одна из констант: SIG_DFL и SIG_IGN. Первая из них указывает, что необходимо установить для данного сигнала обработку по умолчанию, т.е. стандартную реакцию системы, а вторая — что данный сигнал необходимо игнорировать. При успешном завершении функция возвращает указатель на предыдущий обработчик данного сигнала (он может использоваться процессом, например, для восстановления прежней реакции на сигнал).
Как видно из прототипа вызова signal(), определенная пользователем функция- обработчик сигнала должна принимать один целочисленный аргумент (в нем будет передан номер обрабатываемого сигнала), и не возвращать никаких значений.
Отметим одну особенность реализации сигналов в ранних версиях UNIX: каждый раз при получении сигнала его диспозиция (т.е. действие при получении сигнала) сбрасывается на действие по умолчанию, т.о. если процесс желает многократно обрабатывать сигнал своим собственным обработчиком, он должен каждый раз при обработке сигнала заново устанавливать реакцию на него (см. 1)

В заключении отметим, что механизм сигналов является достаточно ресурсоемким, ибо отправка сигнала представляет собой системный вызов, а доставка сигнала - прерывание выполнения процесса-получателя. Вызов функции-обработчика и возврат требует операций со стеком. Сигналы также несут весьма ограниченную информацию.
1. Обработка сигнала.
В данном примере при получении сигнала SIGINT четырежды вызывается специальный обработчик, а в пятый раз происходит обработка по умолчанию.
#include
#include
#include int count = 0; void SigHndlr (int s) /* обработчик сигнала */
{ printf("\n I got SIGINT %d time(s) \n",
++ count); if (count == 5) signal (SIGINT, SIG_DFL);
/* ставим обработчик сигнала по умолчанию */ else signal (SIGINT, SigHndlr);
/* восстанавливаем обработчик сигнала */
} int main(int argc, char **argv)
{ signal (SIGINT, SigHndlr); /* установка реакции на сигнал */ while (1); /*”тело программы” */ return 0;
}
2. Программа “Будильник”.
Программа “Будильник”. Существуют задачи, в которых необходимо прервать выполнение процесса по истечении некоторого количества времени. Средствами
ОС “заводится” будильник, который будет поторапливать ввести некоторое имя.
Системный вызов alarm():
#include unsigned int alarm(unsigned int seconds); инициализирует отложенное появление сигнала SIGALRM - процесс запрашивает ядро отправить ему самому сигнал по прошествии определенного времени.
#include

#include
#include void alrm(int s) /*обработчик сигнала SIG_ALRM */
{ printf(“\n жду имя \n”); alarm(5); /* заводим будильник */ signal(SIGALRM, alrm); /* переустанавливаем реакцию на сигнал */
} int main(int argc, char **argv)
{ char s[80]; signal(SIGALRM, alrm);
/* установка обработчика alrm на приход сигнала
SIG_ALRM */ alarm(5); /* заводим будильник */ printf(“Введите имя \n”); for (;;)
{ printf(“имя:”); if (gets(s) != NULL) break; /* ожидаем ввода имени */
}; printf(“OK! \n”); return 0;
}
В начале программы мы устанавливаем реакцию на сигнал SIGALRM - функцию
alrm(), далее мы заводим будильник, запрашиваем “Введите имя” и ожидаем ввода строки символов. Если ввод строки задерживается, то будет вызвана функция alrm(), которая напомнит, что программа “ждет имя”, опять заведет будильник и поставит себя на обработку сигнала SIGALRM еще раз. И так будет до тех пор, пока не будет введена строка. Здесь имеется один нюанс: если в момент выполнения системного вызова возникает событие, связанное с сигналом, то система прерывает выполнение системного вызова и возвращает код ответа, равный «-1».
3. Двухпроцессный вариант программы “Будильник”.
#include
#include
#include
#include void alr(int s)
{ printf(“\n Быстрее!!! \n”);
signal(SIGALRM, alr);
/* переустановка обработчика alr на приход сигнала
SIGALRM */
} int main(int argc, char **argv)
{ char s[80]; int pid; signal(SIGALRM, alr);
/* установка обработчика alr на приход сигнала SIGALRM
*/ if (pid = fork()) { for (;;)
{ sleep(5);
/*приостанавливаем процесс на 5 секунд */ kill(pid, SIGALRM);
/*отправляем сигнал SIGALRM процессу- сыну */
}
} else
{ printf(“Введите имя \n”); for (;;)
{ printf(“имя:”); if (gets(s) != NULL) break; /*ожидаем ввода имени*/
} printf(“OK!\n”); kill(getppid(), SIGKILL);
/* убиваем зациклившегося отца */
} return 0;
}
В данном случае программа реализуется в двух процессах. Как и в предыдущем примере, имеется функция реакции на сигнал alr(), которая выводит на экран сообщение и переустанавливает функцию реакции на сигнал опять же на себя. В основной программе мы также указываем alr() как реакцию на SIGALRM. После этого мы запускаем сыновний процесс, и отцовский процесс (бесконечный цикл)
“засыпает” на 5 единиц времени, после чего сыновнему процессу будет отправлен сигнал SIGALRM. Все, что ниже цикла, будет выполняться в процессе-сыне: мы ожидаем ввода строки, если ввод осуществлен, то происходит уничтожение отца
(SIGKILL).

БИЛЕТ 30 неименованные каналы
Программные каналы
Одним из простейших средств взаимодействия процессов в операционной системе
UNIX является механизм каналов. Неименованный канал есть некая сущность, в которую можно помещать и извлекать данные, для чего служат два файловых дескриптора, ассоциированных с каналом: один для записи в канал, другой — для чтения. Для создания канала служит системный вызов pipe():
#include
INT PIPE(INT *FD)
Данный системный вызов выделяет в оперативной памяти некоторое ограниченное пространство и возвращает через параметр fd массив из двух файловых дескрипторов: один для записи в канал — fd[1], другой для чтения — fd[0].
Эти дескрипторы являются дескрипторами открытых файлов, с которыми можно работать, используя такие системные вызовы как read(), write(), dup() и так далее. Однако следует четко понимать различия между обычным файлом и каналом.
Основные отличительные свойства канала следующие:
- В отличие от файла, к неименованному каналу невозможен доступ по имени, т.е. единственная возможность использовать канал – это те файловые дескрипторы, которые с ним ассоциированы
- Канал не существует вне процесса, т.е. для существования канала необходим процесс, который его создаст и в котором он будет существовать, для файла это не так.
- Канал реализует модель последовательного доступа к данным (FIFO), т.е. данные из канала можно прочитать только в той же последовательности, в каком они были записаны. Это означает, что для файловых дескрипторов, ассоциированных с каналом, не определена операция lseek() (при попытке обратиться к этому вызову произойдет ошибка).
Кроме того, существует ряд отличий в поведении операций чтения и записи в канал, а именно:
При чтении из канала:
 если прочитано меньше байтов, чем находится в канале, оставшиеся сохраняются в канале;
 если делается попытка прочитать больше данных, чем имеется в канале, и при этом существуют открытые дескрипторы записи, ассоциированные с каналом, будет прочитано (т.е. изъято из канала) доступное количество данных, после чего читающий процесс блокируется до тех пор, пока в канале не появится достаточное количество данных для завершения операции чтения.
 процесс может избежать такого блокирования, изменив для канала режим блокировки с использованием системного вызова fcntl(). В неблокирующем режиме в ситуации, описанной выше, будет прочитано доступное количество данных, и управление будет сразу возвращено процессу.

 При закрытии записывающей стороны канала, в него помещается символ
EOF. После этого процесс, осуществляющий чтение, может выбрать из канала все оставшиеся данные и признак конца файла, благодаря которому блокирования при чтении в этом случае не происходит.
При записи в канал:
 если процесс пытается записать большее число байтов, чем помещается в канал (но не превышающее предельный размер канала) записывается возможное количество данных, после чего процесс, осуществляющий запись, блокируется до тех пор, пока в канале не появится достаточное количество места для завершения операции записи;
 процесс может избежать такого блокирования, изменив для канала режим блокировки с использованием системного вызова fcntl(). В неблокирующем режиме в ситуации, описанной выше, будет записано возможное количество данных, и управление будет сразу возвращено процессу.
 если же процесс пытается записать в канал порцию данных, превышающую предельный размер канала, то будет записано доступное количество данных, после чего процесс заблокируется до появления в канале свободного места любого размера (пусть даже и всего 1 байт), затем процесс разблокируется, вновь производит запись на доступное место в канале, и если данные для записи еще не исчерпаны, вновь блокируется до появления свободного места и т.д., пока не будут записаны все данные, после чего происходит возврат из вызова write()
 если процесс пытается осуществить запись в канал, с которым не ассоциирован ни один дескриптор чтения, то он получает сигнал SIGPIPE
(тем самым ОС уведомляет его о недопустимости такой операции).
В стандартной ситуации (при отсутствии переполнения) система гарантирует атомарность операции записи, т. е. при одновременной записи нескольких процессов в канал их данные не перемешиваются.
4. Использование канала.
Пример использования канала в рамках одного процесса – копирование строк.
Фактически осуществляется посылка данных самому себе.
#include
#include int main(int argc, char **argv)
{ char *s = ”chanel”; char buf[80]; int pipes[2]; pipe(pipes); write(pipes[1], s, strlen(s) + 1); read(pipes[0], buf, strlen(s) + 1); close(pipes[0]); close(pipes[1]); printf(“%s\n”, buf); return 0;
}

Процесс
pipes[1] pipes[0] write() read()
Рис. 7 Обмен через канал в рамках одного процесса.
Чаще всего, однако, канал используется для обмена данными между несколькими процессами. При организации такого обмена используется тот факт, что при порождении сыновнего процесса посредством системного вызова fork() наследуется таблица файловых дескрипторов процесса-отца, т.е. все файловые дескрипторы, доступные процессу-отцу, будут доступны и процессу-сыну. Таким образом, если перед порождением потомка был создан канал, файловые дескрипторы для доступа к каналу будут унаследованы и сыном. В итоге обоим процессам оказываются доступны дескрипторы, связанные с каналом, и они могут использовать канал для обмена данными (см. Рис. 8 и 5).
Процесс-отец
pipe();
fd[0]
Процесс-сын
fork()
канал
чтение
запись
fd[1]
fd[0]
fd[1]
1   ...   6   7   8   9   10   11   12   13   ...   20


написать администратору сайта