Выполнение запросов от многих участников системы к нескольким устройствам

И. С. Гусев

ВЫПОЛНЕНИЕ ЗАПРОСОВ ОТ МНОГИХ УЧАСТНИКОВ СИСТЕМЫ К НЕСКОЛЬКИМ УСТРОЙСТВАМ

Предлагается подход к построению блоковой подсистемы, удовлетворяющей требованиям мягкого реального времени, таким как минимальное гарантированное время работы и минимальное время нахождения в блокировках.

Для операционных систем все чаще возникает требование работы в режиме реального времени, а также поддержки выполнения распределенных вычислений. Обычно происходит разделение операционных систем на системы реального и распределенного времени.

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

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

Связь задержки и блокировки ресурсов

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

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

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

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

Если предполагается, что работа с ресурсов будет происходить длительное время, то используется пас-

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

Работа с устройствами хранения информации

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

Рис. 1. Схема доступа к устройствам хранения информации

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

Обработка блоковых запросов

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

1. Блокирующее чтение блока с устройства. Выполнение потока приостанавливается, пока не произойдет считывание соответствующего блока. Используется для считывания данных.

2. Чтение блока из кэша. Происходит поиск блока в кэше. В случае успеха возвращаются данные. Если блока в кэше нет, то возвращается ошибка. Применяется для проверки необходимости использования опережающего чтения.

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

4. Запись блока на устройство с использованием отложенной записи. Блок помещается в кэш грязных блоков, которые необходимо впоследствии записать на устройство. Поток продолжает работать дальше, ожидание записи не происходит. Используется для ускорения записи.

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

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

7. Очистка кэша устройства. Происходит удаление из кэша блоков устройства. Даже если блок грязный, то все равно происходит удаление без предварительной записи. Используется для очистки кэша в случае смены носителя (например, смены дискет в дисководе).

Для организации оптимальной работы все множество блоков делится на четыре подмножества.

1. Подмножество кэшируемых блоков. Это блоки, которые могут быть в любой момент вытеснены из кэша.

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

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

4. Подмножество блоков запросов к устройству. Блоки, которые доступны только драйверу устройства. Все потоки, пытающиеся работать с ними, приостанавливаются до выполнения запроса чтения или записи.

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

Все блоки находятся в АВЛ-дереве

Кэшируемые блоки, Грязные блоки,

выстроенные в N выстроенные в

очередь по стратегии очередь по стратегии

LRU Л LRU Л

Свободные блоки ч Блоки с запросами к

устройству

Рис. 2. Структура кэша

Для кэшируемых блоков и грязных блоков применяют двойное индексирование. Кэшируемые и грязные блоки объединяются в двухсвязные списки, которые постоянно перестраиваются в соответствии со стратегией LRU [2], при которой последний использованный блок помещается в начало списка, а чем дольше не использовался блок, тем ниже в списке он находится. Эта стратегия позволяет оставлять в памяти часто используемые блоки и вытеснять редко используемые. Однако поиск нужного блока по этому списку происходит довольно медленно (за n операций).

Для ускорения поиска во многих операционных системах (например, в Unix системах [3]) применяются кэш-функции [1], которые при размере кэш-таблицы, соизмеримой с размером множества поиска, позволяют в среднем значительно сократить время поиска (свести его практически до константы). В наихудшем случае трудоемкость все равно остается линейной, а для перестроения кэш-функции для нового размера кэша также требуется линейная трудоемкость, что совершенно неприемлемо для реального времени. Для гарантированного поиска предлагается использовать АВЛ-дерево [1]. Все блоки - кэшируемые и грязные - объединяются в одно АВЛ-дерево. Поиск и изменение дерева проходят за логарифмическое время относительно числа блоков, что позволяет гарантировать время поиска.

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

- для множеств кэшируемых и грязных блоков;

- для свободных блоков;

- для блоков запросов.

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

Благодаря применению АВЛ-дерева поиск происходит за время, меньшее, чем квант процессорного времени, в связи с чем стало возможно использовать блокировки с активным ожиданием, так как они имеют наименьшие накладные расходы, а при достаточно малом времени блокирования не оказывают влияния на исполнение потоков реального времени. Использование же пассивного ожидания ресурсов приводит к неоправданно частому переключению контекстов потоков. Далее рассматриваются основные алгоритмы и оцениваются их трудоемкости.

Блокирующее чтение блока с устройства.

1. Поиск блока в кэше.

2. Если блок найден, то выход с возвратом данных.

3. Просмотр в блоках запросов.

4. Если блок не найден, получение свободного блока, формирование запроса на чтение и помещение его в блоки запросов.

5. Разблокирование кэша.

6. Ожидание выполнения запроса.

7. Если нет других ожидающих - перенос блока в кэшируемые.

8. Выход с возвратом данных.

Трудоемкость алгоритма О(п) и 1,4 ^ п .

Запись блока на устройство с использованием

отложенной записи.

1. Поиск блока в кэше.

2. Если блок найден и он грязный, то модификация данных и выход, иначе удаление из кэша.

3. Если блок найден в списке запросов, то ожидание выполнения запроса, иначе получение свободного блока.

4. Если грязных блоков больше, чем допустимо, то вытеснение самого давнего блока (с записью его на устройство).

5. Формирование блока и помещение его в список грязных.

6. Выход.

Трудоемкость алгоритма О(п) и 1,4logп .

Сброс блока кэша на устройство.

1. Поиск блока в кэше.

2. Если блок найден и он грязный, то перенос его в список блоков запросов.

3. Поиск блока в списке запросов.

4. Если блок найден в списке запросов, то ожидание выполнения запроса.

5. Выход.

Трудоемкость алгоритма О(п) и 1,4logп .

Формирование и исполнение очереди запросов.

1. Если в списке запросов количество блоков меньше установленного, то добавление блоков запросов и списка грязных блоков.

2. Сортировка блоков по возрастанию номеров.

3. Передача запросов в драйвер.

4. Пробуждение потоков ожидающих запросов.

5. Если есть блоки запросов или грязные блоки, то переход на шаг 1.

6. Засыпание до появления запросов.

7. Переход на шаг 1.

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

ЛИТЕРАТУРА

1. Кнут Д. Искусство программирования для ЭВМ. Т. 1. Основные алгоритмы. М.: Мир, 1976. 736 с.

2. ЦикритзисД., Бернстайн Ф. Операционные системы. М.: Мир, 1977. 333 с.

3. Maurice J. Bach. The design of the UNIX operating system: Prentice-Hall, 1986.

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