Добавлен: 29.10.2018
Просмотров: 47969
Скачиваний: 190
1086
Глава 12. Разработка операционных систем
процессор ARM? Для этого нам пришлось бы добавить третий условный оператор
в листинг 12.3 б для процессора ARM. При том, как это было сделано, нужно только
добавить строку
#define WORD_LENGTH 32
в файл
config.h
для процессора ARM.
Этот пример иллюстрирует обсуждавшийся ранее принцип ортогональности. Участ-
ки системы, зависящие от типа центрального процессора, должны компилироваться
с использованием условной компиляции в зависимости от значения константы CPU,
а для участков системы, зависимых от размера слова, должен использоваться макрос
WORD_LENGTH. Те же соображения справедливы и для многих других параметров.
Косвенность
Иногда говорят, что нет такой проблемы в кибернетике, которую нельзя решить на
другом уровне косвенности . Хотя это определенное преувеличение, во фразе имеется
и доля истины. Рассмотрим несколько примеров. В системах на основе процессора x86
при нажатии клавиши аппаратура формирует прерывание и помещает в регистр
устройства не символ ASCII, а скан-код клавиши. Более того, когда позднее клавиша
отпущена, генерируется второе прерывание, также с номером клавиши. Такая косвен-
ность предоставляет операционной системе возможность использовать номер клавиши
в качестве индекса в таблице, чтобы получить по его значению символ ASCII. Этот
способ облегчает обработку разных клавиатур, существующих в различных странах.
Наличие информации как о нажатии, так и об отпускании клавиш позволяет исполь-
зовать любую клавишу в качестве регистра, так как операционной системе известно,
в каком порядке нажимались и отпускались клавиши.
Косвенность используется также при выводе данных. Программы могут выводить на
экран символы ASCII, но эти символы могут интерпретироваться как индексы в та-
блице, содержащей текущий отображаемый шрифт. Элемент таблицы содержит рас-
тровое изображение символа. Такая форма косвенности позволяет отделить символы
от шрифта.
Еще одним примером косвенности служит использование старших номеров устройств
в UNIX. В ядре содержатся две таблицы: одна для блочных устройств и одна для сим-
вольных, — индексированные старшим номером устройства. Когда процесс открывает
специальный файл, например
/dev/hd0
, система извлекает из i-узла информацию о типе
устройства (блочное или символьное), а также старший и младший номера устройств
и, используя их в качестве индексов, находит в таблице драйверов соответствующий
драйвер. Такой вид косвенности облегчает реконфигурацию системы, так как программы
имеют дело с символьными именами устройств, а не с фактическими именами драйверов.
Еще один пример косвенности встречается в системах передачи сообщений, указыва-
ющих в качестве адресата не процесс, а почтовый ящик. Таким образом достигается
существенная гибкость (например, секретарша может принимать почту своего шефа).
В определенном смысле использование макросов, например,
#define PROC_TABLE_SIZE 256
также представляет собой одну из форм косвенности, поскольку программист может
написать программу, не зная фактической величины таблицы. Считается хорошей
12.3. Реализация
1087
практикой давать символьные имена всем константам (иногда кроме –1, 0 и 1) и по-
мещать их в заголовки с соответствующими комментариями.
Повторное использование
Часто возникает возможность использовать повторно ту же самую программу в не-
сколько ином контексте. Это позволяет уменьшить размер двоичных файлов, а кроме
того означает, что такую программу потребуется отлаживать всего один раз. Напри-
мер, предположим, что для учета свободных блоков на диске используются битовые
массивы. Дисковыми блоками можно управлять при помощи процедур alloc и free.
Как минимум, эти процедуры должны работать с любым диском. Но мы можем пойти
дальше в этих рассуждениях. Те же самые процедуры могут применяться для управле-
ния блоками памяти, блоками кэша файловой системы и i-узлами. В самом деле, они
могут использоваться для распределения и освобождения ресурсов, которые могут
быть линейно пронумерованы.
Реентерабельность
Реентерабельность — свойство программы, позволяющее нескольким процессам вы-
полнять эту программу одновременно. На мультипроцессорах всегда имеется опасность
того, что один из процессоров начнет выполнение процедуры, уже выполняющейся
другим процессором. В этом случае два (или более) потока на различных центральных
процессорах будут одновременно выполнять одну и ту же программу. Если в этой про-
грамме существуют области, для которых такая ситуация нежелательна, доступ к этим
(критическим) областям должен быть защищен при помощи мьютексов.
В однопроцессорных системах эта проблема также существует. В частности, большая
часть операционных систем работает с разрешенными прерываниями. Если преры-
вания запрещать, то многие сигналы, подаваемые устройствами ввода-вывода, будут
потеряны и система станет ненадежной. В то время, когда операционная система вы-
полняет некоторую процедуру, может произойти прерывание, и вполне возможно, что
обработчик прерываний также начнет выполнение этой же процедуры. Если на момент
прерывания структуры данных в прерванной процедуре находились в противоречивом
состоянии (то есть прерванная процедура начала изменять эти данные, но не успела
закончить), обработчик прерываний либо будет работать некорректно, либо не сможет
работать вообще.
Такая ситуация может произойти, например, в том случае, если прерываемой про-
цедурой является сам планировщик. Предположим, что некий процесс использовал
свой квант и операционная система переместила его в конец очереди. Во время работы
со списком происходит прерывание, в результате которого другой процесс переходит
в состояние готовности и запускает планировщик. Если в этот момент очередь будет на-
ходиться в противоречивом состоянии, операционная система, скорее всего, не сможет
продолжать работу. Поэтому даже в однопроцессорной системе лучше всего большую
часть системы делать реентерабельной, критические структуры данных защищать
мьютексами, а прерывания в некоторых случаях вообще запрещать.
Метод грубой силы
Применение простых решений под названием метода грубой силы с годами приобрело
негативный оттенок, однако простота решения часто оказывается преимуществом.
1088
Глава 12. Разработка операционных систем
В каждой операционной системе есть множество процедур, которые редко вызываются
или оперируют таким небольшим количеством данных, что оптимизировать их нет
смысла. Например, в системе часто приходится искать какой-либо элемент в таблице
или массиве. Метод грубой силы в данном случае заключается в том, чтобы оставить
таблицу в том виде, в каком она существует, никак не упорядочивая элементы, и про-
изводить поиск в ней линейно от начала к концу. Если число элементов в таблице не-
велико (например, не более 100), выигрыш от сортировки таблицы или применения
хэширования будет невелик, но программа станет гораздо сложнее и, следовательно,
вероятность содержания в ней ошибок резко возрастет. Сортировка или хэширование
таблицы монтирования (отслеживающей смонтированные файловые системы в систе-
ме UNIX) — далеко не самая лучшая затея.
Разумеется, для функций, находящихся в критических участках системы, например
в процедуре, занимающейся переключением контекста, следует предпринять все меры
для их ускорения, возможно, даже писать их (боже упаси!) на ассемблере. Но боль-
шая часть системы не находится на критическом участке. Так, ко многим системным
вызовам обращаются редко. Если системный вызов fork выполняется раз в 10 с, а его
выполнение занимает 10 мс, то даже если удастся невозможное — оптимизация, после
которой выполнение системного вызова fork будет занимать 0 мс, общий выигрыш
составит всего 0,1 %. Если после оптимизации код станет больше и будет содержать
больше ошибок, то лучше оптимизацией не заниматься.
Проверка на ошибки прежде всего
Многие системные вызовы могут завершиться безуспешно по нескольким причинам:
файл, который нужно открыть, может принадлежать кому-то другому; создание про-
цесса может не удаться, так как таблица процессов переполнена; сигнал не может быть
послан, потому что процесса-получателя не существует. Операционная система должна
скрупулезно проверить возможность наличия самых разных ошибок, прежде чем вы-
полнять системный вызов.
Для выполнения многих системных вызовов требуется получение ресурсов, например
элементов таблицы процессов, элементов таблицы i-узлов или дескрипторов файлов.
Прежде чем захватывать ресурсы, полезно проверить, можно ли выполнить этот си-
стемный вызов. Это означает, что всю проверку следует поместить в начало процедуры,
выполняющей системный вызов. Каждая проверка должна иметь следующий вид:
if (error_condition) return(ERROR_CODE);
Если системному вызову удается пробраться сквозь все тесты, то становится ясно, что
он завершится успешно. В этот момент ему можно выделять ресурсы.
Если проверки будут перемежаться обращением к ресурсам, то при неудачном ре-
зультате очередного теста системному вызову придется возвращать все полученные
ресурсы. Если программа написана не совсем корректно и какой-либо ресурс не будет
возвращен, операционная система сразу не зависнет. Например, один из элементов та-
блицы процессов может оказаться навечно (до перезагрузки) недоступным. В этом нет
ничего страшного. Но со временем эта ситуация может повториться несколько раз, при
этом количество недоступных ресурсов будет накапливаться. Наконец, большая часть
элементов таблицы процессов может стать недоступной, что приведет к зависанию
системы, причем исправление этой ошибки, скорее всего, окажется крайне сложным,
так как воспроизвести эту ситуацию будет непросто.
12.4. Производительность
1089
Многие системы страдают подобными «заболеваниями» в форме утечки памяти. До-
вольно часто программы обращаются к процедуре malloc, чтобы получить память, но
забывают позднее обратиться к функции free, чтобы освободить ее. Вся память системы
постепенно исчезает, пока система не зависнет.
Энглер и его коллеги (Engler et al., 2000) предложили метод проверки на наличие по-
добных ошибок во время компиляции. Авторы этой книги выяснили, что программи-
стам известно множество условий, за соблюдением которых им приходится следить
при написании программы и которые не проверяются компилятором. Так, например,
если вы заблокировали мьютекс, то все пути выполнения этой программы начиная
с этого места должны содержать разблокировку мьютекса и не должны содержать
повторной блокировки того же мьютекса. Авторы книги разработали метод, позволя-
ющий программисту поручить компилятору автоматически следить за соблюдением
подобных правил. Программист также может указать в программе, что выделенная
процессу память должна быть освобождена независимо от того, по какой ветви будет
продолжаться выполнение программы, а также поручить компилятору следить за вы-
полнением множества других условий.
12.4. Производительность
При прочих равных условиях быстрая операционная система лучше медленной. Од-
нако быстрая, но ненадежная операционная система хуже надежной, но медленной.
Поскольку сложные оптимизирующие методы часто приводят к появлению в системе
новых ошибок, не следует злоупотреблять оптимизацией. И все же существуют обла-
сти, в которых производительность является критичной и оптимизация стоит затра-
чиваемых усилий. В следующих разделах мы рассмотрим несколько методов, которые
могут применяться для повышения производительности там, где это нужно.
12.4.1. Почему операционные системы такие медленные?
Прежде чем перейти к разговору о методах оптимизации, имеет смысл отметить, что
в медлительности работы многих операционных систем во многом виноваты сами
операционные системы. Например, старые операционные системы, такие как MS DOS
и UNIX Version 7, загружались за несколько секунд. Для загрузки современных версий
систем UNIX и Windows 8 требуется несколько минут, несмотря на то что они загру-
жаются на аппаратуре, работающей в тысячу раз быстрее. Причина состоит в том, что
новые системы выполняют гораздо больше действий, нужно это или нет. Например,
Plug-and-Play облегчает установку новых аппаратных устройств, но платой за это
является то, что при каждой загрузке операционная система должна исследовать все
аппаратное обеспечение, чтобы определить, не появилось ли что-либо новое. Скани-
рование шин занимает время.
Альтернативный (и, по мнению автора, лучший) подход заключается в том, чтобы
совсем выбросить Plug-and-Play, а на экране установить значок
Установка новой аппа-
ратуры
. Установив новое аппаратное устройство, пользователь просто щелкает мышью
на этом значке, запуская процедуру сканирования шин, вместо того чтобы разрешать
операционной системе делать это при каждой загрузке. Разработчики операционных
систем, безусловно, знали о наличии такой возможности. Однако они отказались от
1090
Глава 12. Разработка операционных систем
нее, в основном потому, что считали пользователей недостаточно умными, чтобы пра-
вильно выполнить требуемые действия (правда, высказано это было в более мягких
выражениях). Это всего лишь один пример, но можно привести множество других
примеров того, как желание сделать систему «дружественной по отношению к поль-
зователю» (или «защищенной от дурака», в зависимости от ваших лингвистических
предпочтений) значительно снижало производительность системы.
Вероятно, больше всего могут добиться разработчики систем в деле увеличения произ-
водительности, если существенно повысят избирательность при добавлении к системе
новых функций. Вопрос, который должен при этом ставиться, не «понравится ли это
пользователям?», а «стоит ли добавление этой функции той неизбежной платы, заклю-
чающейся в увеличении программы, снижении скорости, увеличении сложности и сни-
жении надежности?» Следует включать новую функцию, только если преимущества
со всей очевидностью перевешивают недостатки. Некоторые программисты склонны
предполагать, что размеры программы будут равны нулю, а скорость ее работы будет
бесконечной. Как показывают эксперименты, эта точка зрения является несколько
оптимистичной.
Другой фактор, также играющий роль в данном вопросе, заключается в рыночной
стратегии производителей программного обеспечения. К тому времени, когда версия 4
или 5 некоего программного продукта попадает на рынок, все нужные новые функции,
включенные в эту версию, у потребителей, вероятно, уже будут. Чтобы не снижать уро-
вень продаж, многие производители продолжают выпускать новые версии с большим
количеством функций. Добавление новых функций просто ради добавления новых
функций может помочь увеличению продаж, но редко способствует увеличению про-
изводительности.
12.4.2. Что следует оптимизировать?
Общее правило гласит, что первая версия системы должна быть как можно проще.
Оптимизировать следует только те части системы, которые, очевидно, будут пред-
ставлять собой проблему, поэтому их оптимизация является неизбежной. Одним из
таких примеров является наличие блочного кэша для файловой системы. Как только
операционная система отлажена до работоспособного состояния, следует произвести
тщательные измерения, чтобы понять, на что действительно тратится время. Опираясь
на эти числа, следует заниматься оптимизацией в тех областях, в которых это будет
наиболее полезно.
Вот правдивая история о том, как оптимизация принесла больше вреда, чем пользы.
Один из студентов автора (имя студента мы здесь называть не будем) написал про-
грамму mkfs для системы MINIX. Эта программа создает пустую файловую систему
на только что отформатированном диске. На оптимизацию этой программы студент
затратил около шести месяцев. Когда он попытался запустить эту программу, оказалось,
что она не работает, после чего потребовалось еще шесть дополнительных месяцев на
ее отладку. На жестком диске эту программу, как правило, запускают всего один раз,
при установке системы. Она также только раз запускается для каждого гибкого дис-
ка — после его форматирования. Каждый запуск программы занимает около 2 секунд.
Даже если бы работа неоптимизированной версии занимала минуту, все равно затраты
времени на оптимизацию столь редко используемой программы являлись бы непроиз-
водительным расходованием ресурсов.