Главная страница
Навигация по странице:

  • Лекция 8. Процедурные методы синхронизации в операционной системе Windows

  • Объекты синхронизации, функции ожидания.

  • Управление объектами ядра.

  • Пример задачи об ограниченном буфере.

  • Лекция 9. Тупики и защита от них

  • Условия возникновения тупика.

  • Моделирование блокировок.

  • Стратегии борьбы с блокировками.

  • Экзаменационные вопросы по разделу III

  • Востокин С.В.. Операционные системы. Экзаменационные вопросы по разделам дисциплины. Учебник предназначен для студентов специальности Фундамен тальная информатика и информационные технологии


    Скачать 2.09 Mb.
    НазваниеЭкзаменационные вопросы по разделам дисциплины. Учебник предназначен для студентов специальности Фундамен тальная информатика и информационные технологии
    Дата24.03.2022
    Размер2.09 Mb.
    Формат файлаpdf
    Имя файлаВостокин С.В.. Операционные системы.pdf
    ТипЭкзаменационные вопросы
    #412378
    страница5 из 8
    1   2   3   4   5   6   7   8
    Агрессивная оптимизация. Реализация синхронизации с ис- пользованием разделяемых переменных и примитива test and set lock имеет максимальное быстродействие. Однако является достаточно сложной в силу ряда эффектов.
    Эффект агрессивной оптимизации возникает, когда компиля- тор на основе предположения о неизменяемости переменной вне текущего потока сохраняет ее значение в регистре, тем самым нарушает логику работы программы. Например, в коде, показанном ниже, используется флаг g_fFinished для ожидания завершения опе- рации в потоке с функцией CalcFunc. volatile BOOL g_fFinished = FALSE;
    /*volatile – загружаем значение из памяти при каждом обращении к переменной (отключает оптимизацию кода компилятором).*/ int main( ) {
    CreateThread (…, CalcFunc,…); while (g_fFinished = = FALSE);
    }
    DWORD WINAPI CalcFunc (PVOID) {
    … g_fFinished = TRUE;
    }
    Если не использовать ключевое слово volatile, то возникнет риск того, что значения флага загрузятся в регистр процессора перед вычислением while и в дальнейшем проверяться будет значение из регистра. То есть ждущий процесс никогда не увидит окончания вы- числений.
    Голодание. При реализации спин-блокировки возможна ситу- ация, когда поток длительное время опрашивает условие входа в критическую секцию. Периодически это условие оказывается ис-

    63 тинным. Тем не менее, поток не может войти в свою критическую секцию потому, что во время истинного значения условия входа по- ток не получает время процессора. Проблема решается введением задержек перед повторными попытками проверки условия входа.
    Например, пусть два потока выполняют идентичный код и в началь- ный момент времени остановились на строке (1).
    (1) while(1){
    (2) while(InterlockedExchange(&x,1));
    (3)
    /*критическая секция*/
    (4)
    InterlockedExchange(&x,0)
    (5) }
    Когда первому потоку будет отведен квант времени, он в цик- ле выполняет строки (1)-(5). Так как строка (3) выполняется значи- тельно дольше, чем остальные строки, первый поток по истечении кванта остановится на ней. Далее квант времени выделяется второму потоку. Однако он в течение отводимого ему кванта сможет только выполнять цикл (2). Ситуация повторяется при каждом следующем обслуживании: второй поток не сможет войти в свою критическую секцию.
    Ложное разделение. Еще один эффект связан с понижением быстродействия программы при неправильном объявлении пере- менных. Например, в объявлении, показанном ниже, две глобальные переменные объявлены вместе. Одна их них используется исключи- тельно в функции потока Thread1, а другая в функции потока
    Thread2. volatile int x = 0;
    // используется в Thread1( ) volatile int y = 0;
    // используется в Thread2( )

    64
    Рис. 7.1. Переменные X и Y находятся в одной линии кэша, к ним невозможен одновременный доступ из разных процессоров
    Потоки кажутся независимыми, однако это не так. Если поток
    Thread1 выполняет операцию с переменной Х, поток Thread2 вы- нужден приостанавливать операцию с переменной Y и наоборот, из- за чего снижается быстродействие программы (рис. 7.1). Это проис- ходит потому, что для ускорения работы с памятью каждый из про- цессоров использует локальный кэш, однако загрузка значений в кэш производится не побайтно, а блоком. Обычно это участок памя- ти, выровненный по 32-байтной границе. Он называется кэш-линия.
    Если две переменные попадают в одну кэш-линию, то эффект от ис- пользования кэш-памяти пропадает. Поэтому следует выравнивать переменные по границам кэш-линий или вводить фиктивные пере- менные, чтобы гарантировать, что переменные не попадут в одну кэш-линию.
    Правильные объявления будут выглядеть следующим образом.
    #define CACHE_LINE 32
    #define CACHE_ALIGN __declspec(align(CACHE_LINE))
    // используется в Thread1( ) volatile CACHE_ALIGN int x = 0;
    // используется в Thread2( ) volatile CACHE_ALIGN int y = 0;
    X
    Y
    ЦП 1
    ЦП 2
    X
    Y
    ОЗУ
    Локальные кэши процес- соров
    X
    Y
    X

    65
    Для того, чтобы избежать проблем, сопутствующих синхрони- зации с разделяемыми переменными при сохранении высокой ско- рости, в Windows реализована группа функций для организации критической секции. Функция InitializeCriticalSection используется для настройки структуры данных, служащей для управления крити- ческой секцией перед первым входом. Функция DeleteCriticalSection служит для перевода структуры управления критической секцией в исходное состояние. Функции EnterCriticalSection вместе с
    TryEnterCriticalSection реализуют протокол входа в критическую секцию. Функция LeaveCriticalSection реализует протокол выхода из критической секции.
    Дополнительно имеются функции
    InitializeCriticalSectionAndSpinCount и SetCriticalSectionSpinCount для настройки числа попыток входа в критическую секцию перед переходом в режим ядра
    (или возврата в случае
    TryEnterCriticalSection).
    Достоинством функций является высокое быстродействие.
    Однако в них нельзя указать таймаут (аварийный выход через опре- деленное время), их нельзя применять для организации взаимодей- ствия между процессами, только для взаимодействия между потока- ми одного процесса. От этих недостатков свободны процедурные методы синхронизации.
    Лекция 8. Процедурные методы синхронизации
    в операционной системе Windows
    Объекты синхронизации, функции ожидания. Управление объекта- ми ядра. События. Семафоры. Мьютексы. Пример задачи об ограни- ченном буфере.
    Объекты синхронизации, функции ожидания. Процедурные методы синхронизации в Windows используются по отношению к объектам, реализованным в ядре операционной системы. Для такой синхронизации применяются только объекты ядра, которые могут находиться в сигнальном (свободном) и несигнальном (занятом) со- стоянии. К ним относятся рассмотренные ранее объекты: задания,

    66 процессы, потоки. Эти объекты переходят в сигнальное состояние при завершении исполнения. Имеется группа объектов, используе- мых специально для синхронизации потоков и процессов, – это со- бытия, мьютексы, семафоры и таймеры. Отдельную группу образу- ют объекты, предназначенные для синхронизации ввода-вывода.
    С точки зрения программиста все перечисленные объекты имеют общее свойство: их описатели можно использовать в функ- циях ожидания WaitForSingleObject, WaitForMultipleObject и неко- торых других. Если объект находится в несигнальном состоянии, то вызов функции ожидания с описателем объекта блокируется. Де- скриптор потока, который вызвал функцию, помещается в очередь этого объекта ядра, а сам поток переходит в состояние ожидания.
    Когда объект ядра переходит в сигнальное состояние, выполняются действия, специфичные для данного типа объекта ядра. Например, может быть разблокирован один или все потоки, ожидающие на данном объекте ядра.
    В функцию WaitForSingleObject передаются два параметра: описатель объекта и значение таймаута. Таймаут определяет пре- дельное время нахождения потока в блокированном состоянии. В функцию WaitForMultipleObjects передается массив описателей объ- ектов ядра. Для этого указывается адрес начала массива и количе- ство описателей в нем. Также указывается параметр, определяющий семантику ожидания на группе описателей: можно ждать перехода всех объектов ядра в сигнальное состояние или какого-то одного объекта. Указывается также таймаут.
    При возврате из функции ожидания обычно требуется опреде- лить причину завершения функции. В случае ошибки функция воз- вращает значение WAIT_FAILED. Для уточнения состояния ошибки далее используют GetLastError и другие функции. В случае заверше- ния по таймауту возвращается значение WAIT_TIMEOUT. Если причиной возврата является переход в сигнальное состояние, воз- вращается значение WAIT_OBJECT_0.
    Если выполняется ожидание на функции WaitForMultipleObjects, то, чтобы узнать, какой именно объект перешел в сигнальное состо- яние, нужно проверить, что значение, возвращенное функцией, не равно WAIT_FAILED и WAIT_TIMEOUT и вычесть из него кон- станту WAIT_OBJECT_0. В результате мы получим индекс описате-

    67 ля объекта, перешедшего в сигнальное состояние, в массиве описа- телей, переданном в функцию WaitForMultipleObject.
    Управление объектами ядра. Рассмотрим операции, относя- щиеся как к объектам, использующимся в функциях ожидания, так и к другим объектам ядра в операционной системе Windows.
    Для создания объектов ядра используются индивидуальные для каждого объекта функции, имеющие префикс Create. Например, мьютекс может быть создан вызовом
    HANDLE mutex=CreateMutex(NULL, FALSE, NULL).
    Обычно первым параметром всех функций, создающих объек- ты ядра, является структура атрибутов безопасности, а последним – имя объекта.
    Закрытие описателя любого объекта ядра выполняется вызо- вом функции CloseHandle.
    Чтобы понять принцип работы функции, рассмотрим более детально, что представляет собой описатель объекта ядра (рис.8.1).
    Для каждого процесса в ядре операционной системы создается таблица описателей. Запись в таблице содержит некоторые атрибуты описателя и указатель на объект ядра. На один и тот же объект ядра могут указывать несколько описателей – возможно из таблиц описа- телей разных процессов. В любом объекте ядра имеется специаль- ный атрибут для подсчета ссылок на объект. Значение описателя, используемое в адресном пространстве процесса, – это смещение на запись в таблице описателей процесса.
    Рис. 8.1.Описатель объекта ядра в Windows
    2 ссылки
    Описатель –
    HANDLE
    Счетчик ссылок
    Объект ядра
    Таблица объектов ядра процесса

    68
    При закрытии описателя происходит модификация атрибута, указывающего на то, что запись теперь недействительна. Далее про- исходит декремент счетчика ссылок у объекта. Если значение счет- чика ссылок достигает нуля, происходит удаление объекта из памяти ядра операционной системы.
    Важное преимущество объектов ядра – возможность их ис- пользования для синхронизации потоков, принадлежащих разным процессам. Для этой цели нужно уметь передавать описатели объек- тов между процессами. Их можно передавать путем наследования, именования и дублирования.
    При наследовании вначале необходимо модифицировать атри- бут наследования в таблице описателей процесса. Это можно сде- лать непосредственно при создании описателя, как показано ниже.
    SECURITY_ATTRIBUTE sa; sa.nlength = sizeof(sa); sa.lpSecurityDescriptor=NULL; /*NULL – защита по умол- чанию; определяет, кто может пользоваться объектом
    (при NULL – создатель процесса и администраторы) */ sa.bInheritHandle=TRUE; /*наследовать описатель*/
    HANDLE hMutex = CreateMutex (&sa, FALSE, NULL);
    Можно воспользоваться функциями GetHandleInformation и
    SetHandleInformation для изменения атрибутов описателя уже после создания объекта ядра.
    Наследование описателей происходит следующим образом
    (рис. 8.2). При создании процесса функцией CreateProcess для него создается новая таблица описателей. В эту таблицу копируются все записи родительского процесса, имеющие атрибут наследования.
    Записи помещаются по тем же смещениям, по которым размещались исходные записи родительского процесса. Для каждой скопирован- ной записи производится инкремент счетчика ссылок у связанного с ней объекта ядра.
    Дочерний процесс может использовать ссылки на унаследо- ванные объекты ядра. Для передачи значений унаследованных опи- сателей в дочерний процесс обычно используется командная строка или переменные окружения. Если дочерний процесс уже создан, то нельзя воспользоваться механизмом наследования.

    69
    Рис. 8.2. Схема наследования описателя в Windows
    При создании объекта ядра может быть указано его имя:
    HANDLE mutex = CreateMutex(NULL, FALSE, “SomeMutex”);.
    В данном случае любой процесс может получить доступ к
    объекту ядра по имени, если такой доступ разрешен настройками безопасности. При повторном вызове функции Create проводится проверка: имеется ли объект ядра с данным именем и типом. Если объект с таким именем и типом уже существует, то функция не со- здает новый объект, а возвращает описатель существующего объек- та (при этом остальные параметры в вызове игнорируются). При необходимости можно проверить, создан ли объект ядра или полу- чен описатель ранее созданного объекта: if(GetLastError()==ERROR_ALREADY_EXISTS){
    /*если создан ранее*/
    }.
    Механизм ссылки на объекты ядра подходит для всех случаев взаимодействия, когда можно определить соглашения на имена со- здаваемых объектов. Этот механизм также часто используется для контроля количества запущенных экземпляров приложения.
    Универсальным способом передачи описателей объектов ядра между процессами является дублирование описателей. Дублирова- ние выполняется функцией DuplicateHandle:
    2 ссылки
    2.Увеличение счетчика ссы- лок
    1.Копирование записи с сохранением смещения
    Handle1
    Handle2 =
    Handle1

    70
    BOOL DuplicateHandle(
    HANDLE hSourceProcessHandle,/*описатель исходного процесса*/
    HANDLE hSourceHandle,
    /*дублируемый описатель процесса-источника*/
    HANDLE hTargetProcessHandle, /*процесс-приемник*/
    PHANDLE phTargetHandle,
    /*описатель в процессе- приемнике*/
    DWORD dwDesiredAccess,
    /*настройка атрибутов дублируемого описателя*/
    DWORD dwQptions.
    В дублировании принимают участие (в общем случае) три про- цесса: процесс, выполняющий дублирование, процесс – источник и процесс – приемник. Если источником или приемником является управляющий процесс, то вместо соответствующего описателя поме- щается вызов функции GetCurrentProcess. Особенностью дублирова- ния является то, что необходимо использовать механизм межпро- цессного взаимодействия для передачи продублированного описателя в процесс – приемник. Функция DuplicateHandle его сформирует, но нужно также поставить в известность сам процесс – приемник о том, что в его таблицу описателей добавлен новый описатель.
    Теперь рассмотрим объекты синхронизации и специфические для каждого объекта функции процедурной синхронизации.
    События используются при реализации кооперативной син- хронизации, когда один поток ждет поступления данных от другого.
    При наступлении события объект переходит в сигнальное состояние.
    Если событие не наступило – находится в несигнальном состоянии.
    В зависимости от того, каким образом осуществляется перевод со- бытия в несигнальное состояние, существуют два типа событий: со- бытие со сбросом вручную и событие с автоматическим сбросом.
    Оба типа объектов создаются функцией CreateEvent:
    HANDLE CreateEvent(
    PSECURITY_ATTRIBUTES sa,/*атрибуты безопасности*/
    BOOL fManualReset,/*тип объекта TRUE – со сбросом вручную*/
    BOOL fInitialState,/*начальное состояние события*/
    LPCTSTR name);/*имя события*/.

    71
    Для управления событием используются следующие функции:
    SetEvent – устанавливает событие в сигнальное состояние; ResetEvent – устанавливает событие в несигнальное состояние; PulseEvent – уста- навливает в сигнальное состояние и сбрасывает в несигнальное.
    Функция PulseEvent выполняет разблокирование потоков, если таковые имеются в момент ее вызова. События с автоматическим сбросом целесообразно использовать, если происходит периодиче- ское ожидание завершения события. В данном случае вместо ком- бинации вызовов WaitForSingleObject/ResetEvent достаточно ис- пользовать один вызов WaitForSingleObject.
    Семафоры – универсальные объекты процедурной синхрони- зации, предложены Э. Дейкстра. Семафоры выполняют роль счет- чика числа доступных ресурсов. С семафорами определены две опе- рации. Для возврата ресурсов в пул доступных ресурсов использует- ся операция увеличения счетчика (up-операция или V-операция).
    Для захвата ресурсов используется операция уменьшения счетчика
    (down-операция или P-операция). Если счетчик ресурсов принимает значение ноль, то поток блокируется до тех пор, пока ресурс не бу- дет возвращен в пул другим потоком с использованием up или V- операции. В любой момент счетчик не может быть больше макси- мального числа ресурсов и меньше нуля.
    Семафор создается при помощи функции CreateSemaphore:
    HANDLE CreateSemaphore(
    PSECURITY_ATTRIBUTES sa, /*атрибуты безопасности*/
    LONG lInitialCount,/*начальное значение счетчика
    (ресурсов)*/
    LONG lMaxCount,/*предельное значение счетчика
    (ресурсов)*/
    LPCTSTR name);/*имя семафора*/.
    Инкремент счетчика семафора выполняется при помощи вы- зова функции BOOL ReleaseSemaphore (HANDLE, LONG Re- leaseCount,
    LPLONG lpPreviousCount). В ней, в отличие от абстракт- ных операций up или V, можно указать, насколько следует увели- чить счетчик (параметр ReleaseCount), и узнать его предыдущее зна- чение (параметр lpPreviousCount). Декремент счетчика выполняется

    72 при помощи любой функции ожидания, примененной к семафору, например WaitForSingleObject.
    Мьютекс (MUTEX –MUTualEXclusion, взаимное исключение)
    – бинарный семафор, специально предназначенный для решения задачи взаимного исключения, защиты критической секции. Для со- здания мьютекса используется функция CreateMutex:
    HANDLE CreateMutex(
    PSECURITY_ATTRIBUTES sa,/*атрибуты безопасности*/
    BOOL fInitialOwner,/*признак, является ли поток, создающий мьютекс его владельцем*/
    LPCTSTR name);/*имя мьютекса*/.
    Операция освобождения мьютекса (up или V операция) вы- полняется вызовом функции BOOL ReleaseMutex (HANDLE). Опе- рация захвата мьютекса выполняется при помощи любой функции ожидания, например WaitForSingleObject.
    Мьютекс можно рассматривать как бинарный семафор. Также мьютекс похож на критическую секцию. От семафора мьютекс от- личается тем, что он имеет атрибут владения (как критическая сек- ция). Отличие от критических секций состоит в том, что при помо- щи мьютекса можно синхронизировать потоки в разных процессах и указывать таймаут.
    Если поток владеет мьютексом, то вызов функции ожидания с этим мьютексом завершится успешно, при этом увеличится внут- ренний счетчик рекурсий. Функция ReleaseMutex выполняет декре- мент счетчика рекурсий и освобождает мьютекс, если счетчик до- стигает значения ноль. Также ведет себя и критическая секция.
    Если попытаться освободить мьютекс из протока, который им не владеет, то возникнет ошибка и функция GetLastError вернет зна- чение ERROR_NOT_OWNED.
    Если поток, владеющий мьютексом, завершается, то мьютекс переходит в несигнальное состояние и может быть использован дру- гими потоками. Вызов функции ожидания с таким мьютексом будет выполнен, но функция
    GetLastError вернет ошибку
    WAIT_ABANDONED.
    Пример задачи об ограниченном буфере. Проиллюстрируем применение семафоров и мьютексов для решения одной из класси-

    73 ческих задач синхронизации – задаче об ограниченном буфере, так- же известной как проблема производителя и потребителя.
    Два процесса совместно используют буфер ограниченного размера. Один из них (производитель) помещает данные в этот бу- фер, а другой (потребитель) – считывает их оттуда. Требуется обес- печить ожидание процесса-производителя – когда буфер заполнен и ожидание процесса-потребителя – когда буфер пуст. В других слу- чаях процессы могут добавлять (производитель) и извлекать (потре- битель) данные из буфера.
    Приведем программу на псевдокоде с условными операциями для семафоров, мьютексов и буфера, иллюстрирующую решение проблемы для одного производителя и одного потребителя. semaphore mutex = 1;// защита критического
    // ресурса – буфера semaphore empty = 100; //число пустых сегментов буфера semaphore full = 0; // число полных сегментов буфера
    // код процесса – производителя void producer(){ int item; while(1){ item=produce_item();// создать данные,
    //помещаемые в буфер down(&empty); // уменьшить счетчик пустых
    // сегментов буфера down(&mutex);// вход в критическую секцию insert_item(item);// поместить в буфер
    // новый элемент up(&mutex); //выход из критической секции up(&full); //увеличить счетчик
    // полных сегментов буфера
    }
    }
    // код процесса – потребителя void consumer(void){ int item; while(1){ down(&full); // уменьшить счетчик
    // полных сегментов буфера

    74 down(&mutex);// вход в критическую секцию item = remove_item(;// извлечь элемент
    // из буфера up(&mutex); //выход из критической секции up(&empty); //увеличить счетчик
    // пустых сегментов буфера consume_item(item);// обработать элемент
    }
    }
    Заметим, что семафоры в приведенном примере используются по-разному. Семафор mutex служит для реализации взаимного ис- ключения, то есть конкурентной синхронизации. В реальной про- грамме для операционной системы Windows его необходимо реали- зовывать при помощи мьютекса или критической секции. Семафоры full и empty используются для задания определенной последователь- ности событий при кооперативной синхронизации. В реализации для
    Windows уместно использовать семафоры. Таким образом, проблема об ограниченном буфере – пример гибридной проблемы, где встре- чается как кооперативная, так и конкурентная синхронизация.
    Лекция 9. Тупики и защита от них
    Определение тупика. Условие возникновения тупика. Моделирова- ние блокировок. Стратегии борьбы с блокировками: игнорирование проблемы, обнаружение и восстановление, динамическое избега- ние тупиковых ситуаций, предотвращение тупиков при помощи устранения одного из условий их возникновения.
    Определение тупика. Процедурные методы синхронизации более удобны в применении, чем методы синхронизации на основе разделяемых переменных. Однако при некорректном их использова- нии возможно возникновение еще одного типа ошибок синхрониза- ции, известного как проблема тупиков (deadlock).
    Группа процессов находится в тупиковой ситуации, если каж- дый процесс из группы ожидает событие, которое может вызвать только другой процесс из этой же группы.
    Условия возникновения тупика. В большинстве случаев со- бытием, которого ждет каждый процесс, является освобождение ре- сурса. В 1971 году Коффман с соавторами доказали, что для возник-

    75 новения ситуации блокировки в системе процессов и ресурсов должны выполняться четыре условия:
    1. Условие взаимного исключения. Каждый ресурс в любой момент отдан ровно одному процессу или доступен.
    2. Условие удержания и ожидания. Процессы, удерживающие полученные ранее ресурсы, могут запрашивать новые.
    3. Условие отсутствия принудительной выгрузки. У процесса нельзя забрать ранее полученный ресурс. Процесс, владеющий ре- сурсом, должен сам освободить его.
    4. Условие циклического ожидания. Должна существовать круговая последовательность из двух и более процессов, каждый из которых ждет доступа к ресурсу, удерживаемому следующим чле- ном последовательности.
    Для возникновения блокировки должны выполняться все че- тыре условия. Если хотя бы одно условие отсутствует, блокировка невозможна.
    Моделирование блокировок. В 1972 году Холт предложил метод моделирования условий возникновения тупика, используя ориентированные графы. Графические обозначения диаграмм Холта
    (рис. 9.1). Графы имеют два типа узлов. Кругом обозначают про- цесс, квадратом – ресурс. Ребро, направленное от ресурса (квадрат) к процессу (круг) обозначает, что ресурс был ранее запрошен про- цессом, получен и в данный момент используется. Ребро, направ- ленное от процесса к ресурсу, означает, что процесс в данный мо- мент блокирован и находится в состоянии ожидания доступа к дан- ному ресурсу.
    Рис. 9.1. Условные обозначения на диаграммах Холта
    Процесс
    Владение ресурсом
    Ресурс
    P
    R
    Запрос ресурса
    P
    R

    76
    Простейший тупик в системе из двух процессов и двух ресур- сов представлен графом Холта (рис.9.2).
    Рис. 9.2. Простейший тупик
    Рассмотрим, каким образом может возникнуть показанная на рис.9.2 ситуация. Пусть имеются функции потоков T1 и T2, код ко- торых показан ниже. В качестве ресурсов выступают мьютексы M1 и M2. В начальный момент нет запросов и захваченных ресурсов.
    DWORD T1(PVOID){
    DWORD T2(PVOID){
    (1)WaitforSingleObject(M1,-1);
    (2)
    WaitforSingleObject(M2,-1);
    (3)
    WaitforSingleObject(M1,-1);
    (4)WaitforSingleObject(M2,-1);
    (5)--------------тупик----------------------------
    //критическая секция
    //критическая секция
    ReleaseMutex(M1);
    ReleaseMutex(M1);
    ReleaseMutex(M2);
    ReleaseMutex(M2);
    }
    }
    В момент времени (1) поток T1 выполняет захват мьютекса
    M1. Так как мьютекс свободен, происходит успешный захват. Это показано ребром M1

    T1. Далее в момент (2) планировщик может передать управление потоку T2, который выполняет захват свобод- ного мьютекса M2. На графе (рис.9.2) появляется ребро M2

    T2.
    Продолжая вычисления, поток T2 выполняет запрос мьютекса M1, но так как мьютекс M1 уже заблокирован, поток переходит в состо- яние «ожидание» на мьютексе M1. На графе это показано ребром
    T2

    M1. В момент (4) управление передается потоку T1, который выполняет попытку захватить мьютекс M2, уже принадлежащий по- току T2, и тоже переходит в состояние ожидания. Цикл на графе
    M1
    M2
    T1
    T2

    77
    Холта, обозначающий состояние тупика, замыкается ребром
    T1

    M2.
    Рассматривая данный простейший сценарий возникновения тупика, можно сделать следующие выводы. Во-первых, тупики воз- никают не при каждом исполнении. Например, если бы на шаге (2) планировщик не выполнил переключение контекста на поток T2, а дал возможность потоку T1 выполнить захват мьютекса M2, тупик бы не возник. Во-вторых, аккуратным планированием можно избе- жать блокировок. То есть планировщик, обладая некоторой инфор- мацией о необходимых потокам ресурсах, мог бы исключить пере- ключение контекста, вызвавшее блокировку. В-третьих, блокировки можно обнаружить. При переходе процесса в состояние ожидания при запросе ресурса можно выполнить анализ графа ресурсов и про- цессов на наличие цикла, то есть тупика.
    Стратегии борьбы с блокировками. Рассмотрим стратегии предотвращения блокировок, вытекающие их моделей Холта и
    Коффмана.
    «Страусовый алгоритм». Допущение, что блокировки в си- стеме можно игнорировать. Усилия, затраченные на преодоление блокировок, могут себя не оправдать. Например, область памяти, в которой исполняется ядро операционной системы, ограничена. Так- же имеют предел таблицы процессов, таблицы открытых файлов и таблицы других системных ресурсов. Допустим, в операционной системе можно открыть n файлов, и каждый из N запущенных про- цессов открыл по n/N файлов. Далее, если каждый из процессов бу- дет периодически повторять попытки открыть еще по одному файлу, возникнет тупик. Большая часть операционных систем, включая
    UNIX и Windows, игнорируют эту проблему. Полное ее решение включало бы неприемлемые с инженерной точки зрения ограниче- ния на использование ресурсов процессами.
    Обнаружение и восстановление. Данная стратегия состоит в том, чтобы дать возможность произойти блокировке, обнаружить ее и предпринять действия для ее исправления. Существует несколько способов восстановления из тупика.
    Принудительная выгрузка заключается в том, чтобы взять ре- сурс у одного процесса, отдать другому процессу и затем возвратить назад. Такая возможность зависит от свойств ресурса. Например, в

    78 случае необходимости можно временно прервать процесс печати и предоставить принтер другому процессу.
    При восстановлении через откат процесс периодически сохра- няет свое состояние, создавая контрольные точки. Это означает, что его состояние записывается в файл и впоследствии процесс может быть возобновлен из этого файла. Чтобы выйти из тупика процесс, занимающий необходимый ресурс, откатывает к тому моменту вре- мени, перед которым он получил данный ресурс. Освободившийся ресурс отдается другому процессу, попавшему в тупик.
    Простейшим способом выхода из блокировки является уни- чтожение одного или нескольких процессов, находящихся в цикле взаимоблокировки. В некоторых случаях имеются процессы без по- бочных эффектов, которые можно перезапустить, не нарушая работу системы. Например, процедуру компиляции всегда можно повто- рить заново.
    Рассмотрим простейший случай обнаружения тупика, когда каждый тип ресурсов системы представлен единственным ресурсом.
    Например, такая система могла бы иметь один сканер, одно устрой- ство записи дисков, один принтер и так далее.
    Обнаружение тупика в случае единственности ресурса каждо- го типа состоит из двух этапов: в текущем состоянии необходимо построить граф Холта, далее найти на нем цикл по какому-либо ал- горитму.
    Рассмотрим пример системы из семи процессов, состояние ко- торых описывает таблица.
    Процесс Захват ресурса Ждет ресурс
    A
    R
    S
    B
    -
    T
    C
    -
    S
    D
    U
    S, T
    E
    T
    V
    F
    W
    S
    G
    V
    U

    79
    Рис. 9.3. Граф Холта, показывающий состояние тупика процессов D,V и G
    Граф Холта, построенный по таблице, показан на рис.9.3. По визуальной модели состояния системы процессов видно, что про- цессы D, E, G (вместе с ними и процесс B) находятся в тупике. Про- цессы А, C и F могут завершить исполнение и высвободить занима- емые ресурсы, поскольку любому из них можно предоставить ре- сурс S.
    Для анализа графа большей размерности может применяться следующий алгоритм. Используем список L для хранения посещен- ных узлов и пометки для пройденных ребер. Алгоритм применяется для каждого узла графа.
    Шаг 1. Начальные условия: имеется текущий узел, список L пустой, ребра не маркированы.
    Шаг 2. Текущий узел добавляем в конец списка L. Если теку- щий узел присутствует в списке два раза, то граф содержит цикл
    (записанный в L). Алгоритм завершается.
    Шаг 3. Если из текущего узла выходит немаркированное реб- ро, переходим к шагу 4, иначе переходим к шагу 5.
    R
    S
    T
    V
    U
    W
    A
    B
    C
    D
    E
    F
    G

    80
    Шаг 4. Выбираем любое немаркированное ребро, маркиру- ем его, по нему переходим к новому текущему узлу. Переходим к шагу 2.
    Шаг 5. Удаляем последний узел из списка. Предпоследний узел объявляем текущим. Возвращаемся к шагу 3. Если перед шагом
    5 в списке был только начальный узел, то граф не содержит циклов.
    Алгоритм завершается.
    Теперь рассмотрим общий случай обнаружения тупика, если имеется один или несколько типов ресурсов представленных не од- ним, а сразу несколькими ресурсами.
    В системе имеется m типов ресурсов. E = (e
    1
    , e
    2
    , …, e
    m
    ) – век- тор существующих ресурсов, в котором e
    j
    - количество ресурсов типа
    j (1

    j

    m). A = (a
    1
    , a
    2
    , …, a
    m
    ) – вектор доступных ресурсов. В си- стеме имеется n процессов. Матрица текущего распределения ресур- сов С имеет вид:
    C=
    





    





    nm
    n
    m
    m
    c
    c
    c
    c
    c
    c
    c
    1 2
    21 1
    12 11
    , где
    ij
    c
    – количество ресурсов типа
    j, полученных процессом i.
    Матрица запросов ресурсов R имеет вид:
    R=
    





    





    nm
    n
    m
    m
    r
    r
    r
    r
    r
    r
    r
    1 2
    21 1
    12 11
    , где r
    ij
    - количество ресурсов типа j, запрашиваемых процессом i.
    С учетом условия взаимного исключения для ресурсов выпол- няется соотношение
    j
    n
    i
    j
    ij
    e
    a
    c




    1

    81
    Алгоритм обнаружения тупиков состоит из следующих шагов.
    Все процессы не маркированы.
    Шаг 1. В матрице R ищем строку, соответствующую немарки- рованному процессу, которая меньше либо равна А (r
    j

    a
    j
    ).
    Шаг 2. Если такая строка i найдена, то маркируем соответ- ствующий процесс, прибавляем строку номером i матрицы C к век- тору А, возвращаемся к шагу 1.
    Шаг 3. Если немаркированных процессов нет, то алгоритм за- вершается.
    Если после завершения алгоритма остаются немаркированные процессы, то они находятся в тупике.
    Пример. Требуется определить, находится ли система процесс- ресурс в тупике.
    E=(4, 2, 3, 1) A=(2, 1, 0, 0)











    0120 2001 0010
    C











    2100 1010 2001
    R
    Убедимся в выполнении условия взаимного исключения для ресурсов: сложим строки матрицы C и вектор A, получим вектор E.
    1 2
    3 2 2 2 0 3 -
    4 2 2 1 2 -
    4 2 3 1 1-
    A
    ( , , , )
    маркируем
    й процесс
    A
    ( , , , )
    маркируем
    й процесс
    A
    ( , , , )
    маркируем
    й процесс



    Выполняя последовательность шагов алгоритма, маркируем все процессы. Следовательно нет процессов, находящихся в тупике.
    Избегание взаимных блокировок. В первом примере показано, что, выбирая определенный порядок планирования среди несколь- ких возможных, планировщик избегает тупика. Стратегию поведе- ния планировщика в случае двух процессов и произвольного коли- чества ресурсов (в каждом типе имеется 1 ресурс) представляет диа- грамма траектории ресурсов (рис. 9.4).

    82
    Рис. 9.4. Диаграмма траектории ресурсов
    Диаграмма представляет собой первый квадрант координат- ной плоскости. Оси являются временными, представляя события процесса А и процесса B. Для определенности свяжем отсчеты вре- мени процесса А со следующими событиями: 1 – захват принтера,
    2 – захват плоттера, 3 – освобождение принтера, 4 – освобождение плоттера. На временной оси процесса B отмечаются следующие со- бытия: 1 – захват плоттера, 2 – захват принтера, 3 – освобождение плоттера, 4 – освобождение принтера.
    С учетом введенных обозначений точка на диаграмме траек- тории ресурсов представляет состояние системы. Она может дви- гаться только вверх и вправо. В некоторых областях диаграммы си- стема не может находиться по правилу исключения для ресурса. Об- ласти называются, соответственно, области исключения ресурса
    (рис. 9.4).
    Если во время планирования точка, обозначающая состояние системы, попадет в область Х, то со временем в системе возникнет блокировка, так как точка не может двигаться вниз и влево. Любое состояние в области серого цвета является безопасным состоянием.
    А
    B
    1 2
    3 4
    1 2
    3 4
    Области ис- ключения ре- сурсов
    Тупик

    83
    В таком состоянии планировщик путем аккуратного планирования ресурсов может избежать возникновения тупика (рис. 9.4).
    Алгоритм Банкира для одного типа ресурсов. Анализ диа- граммы траектории ресурсов показал, что цель алгоритма планиро- вания – проверить приводит ли удовлетворение запроса на ресурс к выходу из безопасного состояния системы.
    Пусть имеется один ресурс некоторого типа. Воспользуемся аналогией работы планировщика с поведением банкира, кредитую- щего клиентов. Запишем состояние системы в виде таблицы распре- деления ресурсов, в которой A, B, C – имена клиентов банка (про- цессы); второй столбец – выданные клиенту кредиты (ресурсы); тре- тий столбец – число кредитов, необходимых клиентам. Внизу под- писано число свободных кредитов у банкира.
    Вначале проверим, является ли текущее состояние безопас- ным. То есть сможем ли мы предложить стратегию обслуживания клиентов, удовлетворяющую все запросы на кредиты. Для этого бу- дем давать ссуду только тем клиентам, запросы которых в кредитах мы можем покрыть полностью при текущем количестве свободных кредитов у банка. Ниже показана такая стратегия обслуживания.
    A
    3 9
    A
    3 9
    A
    3 9
    A
    3 9
    A
    0
    -
    B
    2 4
    B
    4 4
    B
    0
    -
    B
    0
    -
    B
    0
    -
    C
    2 7
    C
    2 7
    C
    2 7
    C
    0
    -
    C
    0
    -
    Свободно 3
    Свободно 1
    Свободно 5
    Свободно 7
    Свободно 10
    Теперь допустим, что в рассмотренном безопасном состоянии клиент A выставил запрос на один кредит. Рассмотрим гипотетиче- ское состояние, возникающее при удовлетворении такого запроса.
    A
    3 9
    +1
    A
    4 9
    A
    4 9
    A
    8 9
    B
    2 4
    B
    2 4
    B
    0
    -
    B
    0
    -
    C
    2 7
    C
    2 7
    C
    2 7
    C
    2 7
    Свободно 3
    Свободно 2
    Свободно 4
    Свободно 0
    Небезопасно
    Небезопасно
    Небезопасно

    84
    Мы видим, что возникает состояние, в котором не хватает одного кредита для удовлетворения запроса как клиента А (показа- но выше), так и клиента C (определяется аналогично). Оказавшись в таком состоянии, банкир признает банкротство, так как вынуж- ден отказать всем не обслуженным клиентам. То есть система ока- зывается в тупике, следовательно запрос процессом A одного ре- сурса приводит к выходу из безопасного состояния и должен быть отклонен.
    Предотвращение при помощи устранения одного из условий
    возникновения тупика. Если удастся предложить стратегию работы с ресурсами, в которой исключено хотя бы одно из условий Хоффма- на, тупик не возникнет.
    1. Атака взаимного исключения. Пример использования оче- реди печати показывает, что можно избежать захвата принтера.
    2. Атака условия удержания и ожидания. Если все ресурсы за- прашиваются одновременно, то тупиков не возникнет. Например, можно использовать функцию WaitForMultipleObjects.
    3. Атака условия отсутствия принудительной выгрузки. Реали- зуется при обработке транзакций. Каждый процесс готов освободить захваченные ресурсы по запросу менеджера транзакций.
    4. Атака условия циклического ожидания. Если все ресурсы пронумерованы и каждый процесс имеет право захватывать ресурс только с большим порядковым номером, чем номера уже захвачен- ных ресурсов, то тупика не возникнет.
    Экзаменационные вопросы по разделу III
    1. Абстракция процесса, управление процессами в многозадачной операционной системе. Определение процесса. Диаграмма со- стояния, контекст, дескриптор процесса. Квантование и приори- тетное планирование. Нити (потоки исполнения).
    2. Функциональные возможности многозадачности в ОС Windows.
    Способы использования многозадачности в приложениях.
    3. Планировщик ОС Windows. Класс и уровень приоритета. Пере- ключение контекста. Потоки, не являющиеся готовыми. Дина- мический приоритет.

    85 4. Эффект инверсии приоритетов. Пример возникновения инвер- сии. Способы преодоления.
    5. Мультипроцессорная обработка в ОС Windows. Термины, вызо- вы API, их назначение.
    6. Состояние состязания. Пример возникновения и способ преодо- ления.
    7. Средства синхронизации в режиме пользователя в ОС Windows.
    Функции, реализующие атомарные операции, объект «критиче- ская секция».
    8. Задача о критической секции. Алгоритм Питерсона для двух процессов. Условия задачи. Объяснение принципа работы алго- ритма.
    9. Предотвращение агрессивной оптимизации кода с использова- нием модификатора volatile. Эффект голодания, пример возник- новения.
    10. Эффект ложного разделения переменных. Пример влияния кэш- линий на скорость исполнения многопоточных программ.
    11. Управление объектами ядра в ОС Windows. Описатель объекта.
    Таблица описателей объектов процесса. Создание, наследование, именование, дублирование описателей.
    12. Средства синхронизации в режиме ядра в ОС Windows. Собы- тия, семафоры, мьютексы.
    13. Эффект взаимоблокировки или возникновения тупика. Опреде- ление, условия возникновения, моделирование графами Холта.
    14. Стратегия «обнаружение-устранение» для борьбы с взаимобло- кировками. Применение графов Холта и матриц распределения ресурсов.
    15. Стратегия избегания блокировок. Диаграмма траектории ресур- сов. Алгоритм банкира для одного вида ресурсов.
    16. Предотвращение блокировок путем исключения условий их воз- никновения.

    86
    1   2   3   4   5   6   7   8


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