<< Пред.           стр. 5 (из 8)           След. >>

Список литературы по разделу

 
  Поскольку UNIX всегда была многозадачной системой, ее алгоритм планирования с самого начала развития системы разрабатывался так, чтобы обеспечить хорошую реакцию в ин­терактивных процессах. У этого алгоритма два уровня. Низкоуровневый алгоритм выбирает следующий процесс из набора процессов в памяти и готовых к работе. Высокоуровневый алгоритм перемещает процессы из памяти на диск и обратно, что предоставляет всем процессам возможность попасть в память и быть запущенными.
  У каждой версии UNIX свой слегка отличающийся низкоуровневый алгоритм планирования, но у большинства этих алгоритмов есть много общих черт, кото­рые мы здесь и опишем. В низкоуровневом алгоритме используется несколько очередей. С каждой очередью связан диапазон непересекающихся значений прио­ритетов. Процессы, выполняющиеся в режиме пользователя (верхняя часть айс­берга), имеют положительные значения приоритетов. У процессов, выполняющих­ся в режиме ядра (обращающихся к системным вызовам), значения приоритетов отрицательные. Отрицательные значения приоритетов считаются наивысшими, а положительные – наоборот, минимальными. В очере­дях располагаются только процессы, находящиеся в памяти и готовые к работе.
  Когда запускается низкоуровневый планировщик, он ищет очередь, начиная с самого высокого приоритета (то есть с наименьшего отрицательного значения), пока не находит очередь, в которой есть хотя бы один процесс. После этого в этой очереди выбирается и запускается первый процесс. Ему разрешается работать в те­чение некоего максимального кванта времени, как правило, 100 мс, или пока он не заблокируется. Если процесс использует весь свой квант времени, он помещается обратно, в конец очереди, а алгоритм планирования запускается снова. Таким об­разом, процессы, входящие в одну группу приоритетов, совместно используют цен­тральный процессор в порядке циклической очереди.
  Раз в секунду приоритет каждого процесса пересчитывается по формуле, со­стоящей из суммы трех компонентов. Первой составляющей этой формулы является параметр использования центрального процессора, представляющий собой среднее значение тиков (прерываний) таймера в секунду, которые процесс работал в тече­ние последних нескольких секунд. При каждом тике таймера счет­чик использования центрального процессора в таблице процессов увеличивается на единицу. Этот счетчик в конце концов добавляется к приоритету процесса, увеличивая тем самым числовое значение его приоритета (что соответствует бо­лее низкому приоритету), в результате чего процесс попадает в менее приоритет­ную очередь. Однако в UNIX процесс не находится в конце очереди бесконечно долго, и величина параметра использования центрального процессора со временем уменьша­ется. В различных версиях UNIX это уменьшение выполняется по-разному. Один из способов состоит в том, что к этому параметру прибавляется полученное число тиков, после чего сумма делится на два. Такой алгоритм учитывает самое последнее значение числа тиков с весовым коэффициентом 1/2, предшествующее ему – с весовым коэффициентом 1/4 и т. д. Алгоритм взвешивания очень быстр, так как состоит из всего одной операции сложения и одного сдвига, но также применяются и другие схемы взвешивания.
  Второй составляющей формулы является так называемый параметр nice. Его значение по умолчанию рав­но 0, но допустимый диапазон значений, как правило, составляет от -20 до +20. Процесс может установить значение nice в диапазоне от 0 до 20 с помощью систем­ного вызова. Только системный администратор может запросить обслуживание с более высоким приоритетом (то есть значения nice от -20 до -1).
  Третьей составляющей формулы является параметр base (база). Когда процесс эмулирует прерывание для выполнения системного вызова в ядре, процесс, вероятно, должен быть блокирован, пока системный вызов не будет вы­полнен и не вернется в режим пользователя. Например, процесс может обратить­ся к системному вызову, ожидая, пока один из его дочерних процессов не закончит работу. Он может также ожидать ввода с терминала или завершения дис­ковой операции ввода-вывода и т. д. Когда процесс блокируется, он удаляется из структуры очереди, пока этот процесс снова не будет готов работать. Однако когда происходит событие, которого ждал процесс, он снова помещает­ся в очередь с отрицательным значением. Выбор очереди определяется событием, которого ждал процесс. Например дисковый ввод-вывод может быть событием с наивысшим приоритетом, так что процесс, только что прочитавший или запи­савший блок диска, вероятно, получит центральный процессор в течение 100 мс. Отрицательные значения приоритета для дискового ввода-вывода, терминаль­ного ввода-вывода и некоторых других операций жестко прошиты в операционной системе и могут быть изменены только путем перекомпиляции самой системы. Эти значения (отрицательные) и представлены параметром base. Их величина достаточно отличается от нуля, чтобы перезапущенный процесс навер­няка попадал в другую очередь.
  В основе этой схемы лежит идея как можно более быстрого удаления процес­сов из ядра. Например, если процесс пытается читать дисковый файл, необходимость ждать секунду между обращениями к системным вызовам read замедлит его работу во много раз. Значительно лучше позволить ему немедленно продолжить работу сразу после выполнения запроса, так чтобы он мог быстро обратиться к следую­щему системному вызову. Если процесс был заблокирован ожиданием ввода с тер­минала, то, очевидно, это интерактивный процесс, и ему должен быть предостав­лен наивысший приоритет, как только он перейдет в состояние готовности, чтобы гарантировать хорошее качество обслуживания интерактивных процессов. Таким образом, процессы, ограниченные производительностью процессора (то есть на­ходящиеся в положительных очередях), в основном обслуживаются после того, как будут обслужены все процессы, ограниченные вводом-выводом (когда все эти про­цессы окажутся заблокированы в ожидании ввода-вывода).
  Планирование в опера­ционной системе Linux представляет собой одну из немногих областей, в которых использует алгоритм, отличный от применяющегося в UNIX. Так как потоки в системе Linux реализованы в ядре, то планирование в ней основано на потоках, а не на процессах. В операционной системе Linux алгоритмом планирования раз­личаются три класса потоков:
  1. Потоки реального времени, обслуживаемые по алгоритму FIFO.
  2. Потоки реального времени, обслуживаемые в порядке цикли-ческой очереди.
  3. Потоки разделения времени.
  Потоки реального времени, обслуживаемые по алгоритму FIFO, имеют наивыс­шие приоритеты и не могут прерываться другими потоками, за исключением та­кого же потока реального времени FIFO, перешедшего в состояние готовности. Потоки реального времени, обслуживаемые в порядке циклической очереди, пред­ставляют собой то же самое, что и потоки реального времени FIFO, но с тем отли­чием, что они могут прерываться таймером. Находящиеся в состоянии готовности потоки реального времени, обслуживаемые в порядке циклической очереди, вы­полняются в течение определенного кванта времени, после чего поток помещает­ся в конец своей очереди. Ни один из этих классов на самом деле не является клас­сом реального времени. Здесь нельзя задать предельный срок выполнения задания и предоставить гарантий его выполнения. Эти классы просто имеют более высо­кий приоритет, чем у потоков стандартного класса разделения времени.
  У каждого потока есть приоритет планирования. Значение по умолчанию рав­но 20, но оно может быть изменено при помощи специального системного вызова, вычитающего некоторое значение в диапа­зоне от -20 до +19 из 20. Поэтому возможные значения приоритетов попадают в промежуток от 1 до 40. Цель алгоритма планирования состоит в том, чтобы обеспечить грубое пропорциональ­ное соответствие качества обслуживания приоритету (то есть чем выше приори­тет, тем меньше должно быть время отклика и тем большая доля процессорного времени достанется процессу).
  Помимо приоритета с каждым процессом связан квант времени, то есть коли­чество тиков таймера, в течение которых процесс может выполняться. По умолча­нию каждый тик равен 10 мс. Планировщик использует сочетание значений приоритета и кванта для плани-рования по определенному алгоритму.
 
 7.3. Управление памятью в UNIX
 
 7.3.1. Основные понятия
 
  У каждого процесса в системе UNIX есть адресное пространство, состоящее из трех сегментов: текста (программы), данных и стека. Текстовый (программный) сегмент содержит машинные команды, образующие исполняемый код программы. Он создается компилятором и ассемблером при трансляции программы, написанной на языке высокого уровня, в машинный код. Как правило, тексто­вый сегмент разрешен только для чтения. Текстовый сегмент не изменяется ни в размерах, ни по своему содержанию.
  Сегмент данных содержит переменные, строки, массивы и другие данные про­граммы. Он состоит из двух частей: инициализированных данных и неинициали­зированных данных. По историческим причинам вторая часть называется BSS (Bulk Storage System – запоминающее устройство большой емкости или массовое запоминающее устройств). Инициализированная часть сегмента данных содержит переменные и константы компилятора, значения которых должны быть заданы при запуске программы. Например, на языке С можно объявить символьную строку и в то же время за­дать ее значение, то есть проинициализировать ее. Когда программа запускается, она предполагает, что в этой строке уже содержится некий осмысленный текст. Чтобы реализовать это, компилятор назначает строке определенное место в адрес­ном пространстве и гарантирует, что в момент запуска программы по этому адресу будет располагаться соответствующая строка. С точки зрения операционной сис­темы, инициализированные данные не отличаются от текста программы – тот и другой сегменты содержат сформированные компилятором последовательности битов, загружаемые в память при запуске программы.
  Неинициализированные данные необходимы лишь с точки зрения оптимиза­ции. Когда начальное значение глобальной переменной явно не указано, то, соглас­но семантике языка С, ее значение устанавливается равным 0. На практике боль­шинство глобальных переменных не инициализируются, и, таким образом, их начальное значение равно 0. Это можно реализовать следующим образом: создать целый сегмент исполняемого двоичного файла, точно равного по размеру числу байтов данных, и проинициализировать весь этот сегмент нулями. Однако с целью экономии места на диске этого не делается. Файл содержит только те переменные, начальные значения которых явно заданы. Вместо неинициализи­рованных переменных компилятор помещает в исполняемый файл просто одно слово, содержащее размер области неинициализированных данных в байтах. При запуске программы операционная система считывает это слово, выделяет нужное число байтов и обнуляет их.
  В отличие от текстового сегмента, который не может изменяться, сегмент дан­ных может модифицироваться. Программы изменяют свои переменные постоян­но. Более того, многим программам требуется выделение дополнительной памяти динамически, во время выполнения. Чтобы реализовать это, операционная систе­ма UNIX разрешает сегменту данных расти при динамическом выделении памяти программам и уменьшаться при освобождении памяти программами. Програм­ма может установить размер своего сегмента данных с помощью системного вызо­ва brk. Таким образом, чтобы получить больше памяти, программа может увеличить размер своего сегмента данных. Этим системным вызовом пользуется библиотеч­ная процедура, используемая для выделения памяти.
  Третий сегмент – это сегмент стека. На большинстве вычислительных машин он начи­нается около старших адресов виртуального адресного пространства и растет вниз к 0. Если указатель стека оказывается ниже нижней границы сегмента стека, как правило, происходит аппаратное прерывание, при котором операционная сис­тема понижает границу сегмента стека на одну страницу памяти. Программы не управляют явно размером сегмента стека. Когда программа запускается, ее стек не пуст. Напротив, он содержит все пере­менные окружения (оболочки), а также командную строку, введенную в оболочке при вызове этой программы. Таким образом, программа может узнать параметры, с которыми она была запущена.
  Когда два пользователя запускают одну и ту же программу, например тексто­вый редактор, в памяти можно хранить две копии программы редактора. Однако такой подход является неэффективным. Вместо этого большинством систем UNIX поддерживаются текстовые сегменты совместного использования. Отображение выполняется аппаратным обеспечением виртуальной памяти.
  Сегменты данных и стека никогда не бывают общими, кроме как после выпол­нения системного вызова fork, и то только те страницы, которые не модифициру­ются любым из процессов. Если размер любого из сегментов должен быть увели­чен, то отсутствие свободного места в соседних страницах памяти не является проблемой, так как соседние виртуальные страницы памяти не обязаны отобра­жаться на соседние физические страницы.
  На некоторых вычислительных машинах аппаратное обеспечение поддерживает раздельные адресные пространства для команд и для данных. Если такая возможность есть, система UNIX может ею воспользоваться. Например, на компьютере с 32-разряд­ными адресами при возможности использования раздельных адресных пространств можно получить 4 Гбайт адресного пространства для команд и еще 4 Гбайт адрес­ного пространства для данных. Передача управления по адресу 0 будет восприни­маться как передача управления по адресу 0 в текстовом пространстве, тогда как при обращении к данным по адресу 0 будет использоваться адрес 0 в пространстве данных. Таким образом, это свойство удваивает доступное адресное пространство.
  Многими версиями UNIX поддерживается отображение файлов на адресное пространство памяти. Это свойство позволяет отображать файл на часть адресно­го пространства процесса, так чтобы можно было читать из файла и писать в файл, как если бы это был массив, хранящийся в памяти. Отображение файла на адрес­ное пространство памяти делает произвольный доступ к нему существенно более легким, нежели при использовании системных вызовов, таких как read и write. Совместный доступ к библиотекам предоставляется именно при помощи этого механизма.
  Дополнительное преимущество отображения файла на память заключается в том, что два или более процессов могут одновременно отобразить на свое адрес­ное пространство один и тот же файл. Запись в этот файл одним из процессов мгновенно становится видимой всем остальным. Таким образом, отображение на адресное пространство памяти временного файла (который будет удален после завершения работы процессов) представляет собой механизм реализации общей памяти для нескольких процессов, причем у такого механизма будет высокая про­пускная способность. В предельном случае два или более процессов могут отобра­зить на память файл, покрывающий все адресное пространство, получая, таким образом, форму совместного использования памяти – нечто среднее между процес­сами и потоками. В этом случае, как и у потоков, все адресное пространство ис­пользуется совместно, но каждый процесс может управлять собственными файла­ми и сигналами, что отличает этот вариант от потоков.
 
 7.3.2. Реализация управления памятью в UNIX
 
  До версии 3BSD большинство систем UNIX основывались на свопинге (подкач­ке), работавшем следующим образом. Когда загружалось больше процессов, чем могло поместиться в памяти, некоторые из них выгружались на диск. Выгружае­мый процесс всегда выгружался на диск целиком (исключение представляли толь­ко совместно используемые текстовые сегменты). Таким образом, процесс мог быть либо в памяти, либо на диске.
  Перемещением данных между памятью и диском управлял верхний уровень двух­уровневого планировщика, называвшийся свопером (swapper). Выгрузка данных из памяти на диск инициировалась, когда у ядра кончалась свободная память из-за одного из следующих событий:
  1. Системному вызову fork требовалась память для дочернего процесса.
  2. Системный вызов brk собирался расширить сегмент данных.
  3. Разросшемуся стеку требовалась дополнительная память.
  Кроме того, когда наступало время запустить процесс, уже достаточно долго находящийся на диске, часто бывало необходимо удалить из памяти другой про­цесс, чтобы освободить место для запускаемого процесса. Выбирая про­цесс, который необходимо было удалить из памяти, свопер сначала рассматривал блокированные (например, ожи­данием ввода с терминала) процессы. Лучше удалить из памяти процесс, который не может работать, чем работоспособный процесс. Если такие процессы находи­лись, из них выбирался процесс с наивысшим значением суммы приоритета и вре­мени пребывания в памяти. Таким образом, подвергались выгрузки процессы, потребившие большое количество процессорного времени или находящиеся в памяти уже достаточно долгое время. Если блокированных процессов не было, тогда на основе тех же критериев выбирался готовый процесс.
  Каждые несколько секунд свопер исследовал список выгруженных процессов, проверяя, не готов ли какой-либо из этих процессов к работе. Если процессы в со­стоянии готовности обнаруживались, из них выбирался процесс, дольше всех нахо­дящийся на диске. Затем свопер проверял, будет ли это легкий свопинг или тяже­лый. Легким свопингом считался тот, для которого не требовалось дополнительное высвобождение памяти. При этом нужно было всего лишь загрузить выгружен­ный на диск процесс. Тяжелым свопингом назывался свопинг, при котором для загрузки в память выгруженного на диск процесса из нее требовалось удалить один или несколько других процессов. Затем весь этот алгоритм повторялся до тех пор, пока не выполнялось одно из следующих двух условий: на диске не оставалось процессов, готовых к работе, или в памяти не оставалось места для новых процессов. Чтобы не терять боль­шую часть производительности системы на свопинг, ни один процесс не выгру­жался на диск, если он пробыл в памяти менее 2 с. Свободное место в памяти и на устройстве перекачки учитывалось при помо­щи связного списка свободных пространств. Когда требовалось свободное про­странство в памяти или на диске, из списка выбиралось первое подходящее сво­бодное пространство. После этого в список возвращался остаток от свободного пространства.
  Начиная с версии 3BSD к системе была добавлена страничная подкачка, чтобы предоставить возможность работать с программами больших размеров. Практически во всех версиях системы UNIX теперь есть страничная подкачка по требованию, появившаяся впервые в версии 3BSD. Ниже описывается реализация этого механизма в версии 4BSD. Такая реализация во многом соответствует и версии System V, которая основана на 4BSD.
  Идея, лежащая в основе страничной подкачки в системе 4BSD, состоит в том, что процессу для работы не нужно целиком находиться в памяти. Все, что в действи­тельности требуется, – это структура пользователя и таблицы страниц. Если они загружены, то процесс считается находящимся в памяти и может быть запущен планировщиком. Страницы с сегментами текста, данных и стека загружаются в па­мять динамически, по мере обращения к ним. Если пользовательской структуры и таблицы страниц нет в памяти, то процесс не может быть запущен, пока свопер не загрузит их.
  Страничная подкачка реализуется частично ядром и частично новым про­цессом, называемым страничным демоном. Как и все демоны, страничный демон периодически запускается и смотрит, есть ли для него работа. Если он обнаруживает, что количество страниц в списке свободных стра­ниц слишком мало, страничный демон инициирует действия по освобождению дополнительных страниц.
  Память в 4BSD делится на три части. Первые две части, ядро операционной системы и карта памяти, фиксированы в фи­зической памяти (то есть никогда не выгружаются). Остальная память машины делится на страничные блоки, каждый из которых может содержать либо страницу текста, данных или стека, либо находиться в списке свободных страниц.
  Карта памяти содержит информацию о содержимом страничных блоков. Для каждого страничного блока в карте памяти есть запись фиксированной длины. При килобайтных страничных блоках и 16-байтовых записях карты памяти на нее расходуется менее 2 % от общего объема памяти. Первые два поля записи карты памяти используются только тогда, когда соответствующий страничный блок находится в списке свободных страниц. В этом случае они сшивают свобод­ные страницы в двусвязный список. Следующие три записи используются, когда страничный блок содержит информацию. У каждой страницы в памяти есть фик­сированное место хранения на диске, в которое она помещается, когда выгружает­ся из памяти. Еще три поля содержат ссылку на запись в таблице процессов, тип хранящегося в странице сегмента и смещение в сегменте процесса. Последнее поле содержит некоторые флаги, нужные для алгоритма страничной подкачки. При запуске процесс может вызвать страничное прерывание, если одной или нескольких его страниц не окажется в памяти. При страничном прерывании опе­рационная система берет первый страничный блок из списка свободных страниц, удаляет его из списка и считывает в него требуемую страницу. Если список сво­бодных страниц пуст, выполнение процесса приостанавливается до тех пор, пока страничный демон не освободит страничный блок.
  Алгоритм замещения страниц выполняется страничным демоном. Раз в 250 мс он сравнивает количество свободных страничных блоков с систем­ным параметром lotsfree (равным, как правило, 1/4 объема памяти). Если число свободных страничных блоков меньше, чем значение этого параметра, страничный демон начинает переносить страницы из памяти на диск, пока количество свобод­ных страничных блоков не станет равно lotsfree. Если же количество свободных страничных блоков больше или равно lotsfree, тогда страничный демон ничего не предпринимает. Если в машине много памяти и мало активных процессов, страничный демон практически все время бездействует.
  Страничный демон использует модифицированную версию алгоритма часов. Это глобальный алгоритм, то есть при удалении страницы он не учитывает, чья это страница. Таким образом, количество страниц, выделяемых каждому процес­су, меняется со временем. Основной алгоритм часов работает, сканируя в цикле страничные блоки (как если бы они лежали на окружности циферблата часов). На первом проходе, когда стрелка часов указывает на страничный блок, сбрасывается его бит использова­ния. На втором проходе у каждого страничного блока, к которому не было доступа с момента первого прохода, бит использования останется сброшенным, и этот стра­ничный блок будет помещен в список свободных страниц. Страничный блок в списке свободных страниц сохра­няет свое содержание, что позволяет восстановить страницу, если она потребует­ся прежде, чем будет перезаписана.
  Изначально в версии UNIX 4BSD использовался основной алгоритм часов, но затем он был заменен более эффективным алгоритмом часов с двумя стрелками. В этом алгоритме страничный демон под­держивает два указателя на карту памяти. При работе он сначала очищает бит использования передней стрелкой, а затем проверяет этот бит задней стрелкой. После чего перемещает обе стрелки. Если две стрелки находятся близко друг от друга, то только у очень активно используемых страниц появляется шанс, что к ним будет обращение между проходами двух стрелок. Если же стрелки разнесены на 359 градусов (то есть задняя стрелка находится слегка впереди передней), то по сути снова получается исходный алгоритм часов. При каждом запуске страничного демона стрелки проходят не полный оборот, а столько, сколько необходимо, что­бы количество страниц в списке свободных страниц было не менее lotsfree.
  Если операционная система обнаруживает, что частота подкачки страниц слишком высока, а количество свободных страниц все время ниже lotsfree, свопер начинает удалять из памяти один или несколько процессов, чтобы остановить состязание за свободные страничные блоки. Свопинг в системе 4BSD осуществляется по следующему алгоритму. Сначала свопер проверяет, есть ли процесс, который бездействовал в течение 20 и более секунд. Если такие процессы есть, из них выбирается бездей­ствовавший в течение максимального срока и выгружается на диск. Если таких процессов нет, изучаются четыре самых больших процесса, из которых выбирает­ся тот, который находился в памяти дольше всех, и выгружается на диск. При не­обходимости этот алгоритм повторяется до тех пор, пока не будет высвобождено достаточное количество памяти.
  Каждые несколько секунд свопер проверяет, есть ли на диске готовые процес­сы, которые следует загрузить в память. Каждому процессу на диске присваива­ется значение, зависящее от времени его пребывания в выгруженном состоянии, размера, значения, использовавшегося при обращении к системному вызову nice (если такое обращение было), и от того, как долго этот процесс бездействовал, прежде чем был выгружен на диск. Эта функция обычно взвешивается так, чтобы загружать в память процесс, дольше всех находящийся в выгруженном состоянии, если толь­ко он не крайне большой. Теория утверждает, что загружать большие процессы дорого, поэтому их не следует перемещать с диска в память и обратно слишком часто. Загрузка про­цесса производится только при условии наличия достаточного количества свобод­ных страниц, чтобы, когда случится неизбежное страничное прерывание, для него нашлись свободные страничные блоки. Свопер загружает в память только струк­туру пользователя и таблицы страниц. Страницы с текстом, данными и стеком подгружаются при помощи обычной страничной подкачки.
  У каждого сегмента каждого активного процесса есть место на диске, где он располагается, когда его страницы удаляются из памяти. Сегменты данных и сте­ка сохраняются на временном устройстве, но текст программы подгружается из самого исполняемого двоичного файла. Для текста программы временная копия не используется.
  Страничная подкачка в версии System V во многом схожа с применяемой в системе 4BSD, но тем не менее между этими версиями операционной системы есть интересные различия. Во-первых, в System V вместо алгоритма часов с двумя стрелками использует­ся оригинальный алгоритм часов с одной стрелкой. Более того, вместо того чтобы помещать страницу в список свободных страниц на втором проходе, страница по­мещается туда только в случае, если она не использовалась в течение нескольких последовательных проходов. Хотя при таком решении страницы не освобождаются так быстро, как это делается алгоритмом в 4BSD, оно значительно увели­чивает вероятность того, что освобожденная страница не потребуется тут же снова. Во-вторых, вместо единственной переменной lotsfree в System V используются две переменные: min и max. Когда количество свободных страниц опускается ниже min, страничный демон начинает освобождать страницы. Демон продолжает рабо­тать до тех пор, пока число свободных страниц не сравняется со значением max. Такой подход позволяет избежать неустойчивости, возможной в системе 4DSD, например, в ситуации, когда количество свободных страниц на единицу мень­ше, чем lotsfree. При этом страничный демон освобождает одну страницу, чтобы привести количество свободных страниц в соответствие со значением lotsfree. Затем происходит еще одно страничное прерывание, и количество свободных страниц опять становится на единицу меньше lotsfree, в результате страничный демон сно­ва начинает работать. Если установить значение max существенно большим, чем min, страничный демон, завершив освобождение страниц, создает достаточный за­пас для своего бездействмя в течение относительно продолжительного времени.
 
 
 7.4. Ввод-вывод в системе UNIX
 
 7.4.1. Основные понятия
 
  Подход, применяемый в операционной системе UNIX для доступа программ к устройствам ввода-вывода (таким как диски, принтеры, сетевые устройства и т.п.), заключается в интегрировании всех устройств в файловую систему в виде так называемых специальных файлов. Каждому устройству ввода-вывода назначается имя пути, обычно в ката­логе /dev. Например, диск может иметь путь /dev/hd1, у принтера может быть путь /dev/lр, а у сети – /dev/net. Доступ к этим специальным файлам осуществляется так же, как и к обычным файлам. Для этого не требуется никаких специальных команд или системных вызо­вов, а используются обычные системные вызовы read и write. Программы мо­гут открывать, читать специальные файлы, а также писать в них тем же способом, что и в обычные файлы. Таким образом, для выполнения ввода-вывода не требуется специального механизма.
  Специальные файлы подразделяются на две категории: блочные и символьные. Блочный специальный файл – это специальный файл, состоящий из последова­тельности нумерованных блоков. Основное свойство блочного специального фай­ла заключается в том, что к каждому его блоку можно адресоваться и получить доступ отдельно. Другими словами, программа может открыть блочный специаль­ный файл и прочитать, скажем, 124-й блок, не читая сначала блоки с 0 по 123. Блоч­ные специальные файлы обычно используются для дисков. Символьные специальные файлы, как правило, используются для устройств ввода или вывода символьного потока. Символьные специальные файлы исполь­зуются такими устройствами, как клавиатуры, принтеры, сети, мыши, плоттеры и т. д.
  С каждым специальным файлом связан драйвер устройства, осуществляющий управление соответствующим устройством. У каждого драйвера есть так называе­мый номер старшего устройства, служащий для его идентификации. Если драй­вер одновременно поддерживает несколько устройств, например два диска одного типа, то каждому диску присваивается номер младшего устройства, идентифици­рующий это устройство. Вместе номера старшего устройства и младшего устройства однозначно обозначают каждое устройство ввода-вывода.
  Другим примером ввода-вывода является работа с сетью, впервые появившаяся в Berkeley UNIX. Ключевым понятием в схеме Berkeley UNIX является сокет. Сокеты образуют пользовательский интерфейс с сетью. Сокеты могут динамически создаваться и разрушаться. При создании сокета вызывающему процессу возвращается дескриптор файла, требующийся для уста­новки соединения, чтения и записи данных, а также разрыва соединения.
  Каждый сокет поддерживает определенный тип работы в сети, указываемый при создании сокета. Наиболее распространенными типами сокетов являются: 1) надежный байтовый поток (ориентиро-ванный на соединение), 2) надежный поток пакетов (ориенти-рованный на соединение), 3) ненадежная передача пакетов.
  Первый тип сокетов позволяет двум процессам на различных машинах устано­вить между собой эквивалент «трубы» (канала между процессами на одной машине). Байты подаются в канал с одного конца и в том же порядке выходят с другого. Такая система гарантирует, что все посланные байты прибудут на другой конец канала и прибудут именно в том порядке, в котором были отправлены.
  Второй тип сокетов отличается от первого тем, что он сохраняет границы меж­ду пакетами. Если отправитель пять раз отдельно обращается к системному вызо­ву write, каждый раз отправляя по 512 байт, а получатель запрашивает 2560 байт по сокету типа 1, он получит все 2560 байт сразу. При использовании сокета типа 2 ему будут выданы только первые 512 байт. Чтобы получить остальные байты, получателю придется выполнить системный вызов read еще четыре раза.
  Третий тип сокета особенно полезен для приложений реального времени и ситуаций, в которых пользователь хочет реализовать специальную схему обработки ошибок. Сеть может терять пакеты или доставлять их в неверном порядке. В отличие от сокетов первых двух типов, сокет типа 3 не предоставляет никаких гарантий доставки. Преиму­щество этого режима заключается в более высокой производительности, которая в некоторых ситуациях оказывается важнее надежности (например, для доставки мультимедиа, при которой скорость ценится существенно выше, нежели сохран­ность данных).
  При создании сокета один из параметров указывает протокол, используемый для него. Для надежных байтовых потоков, как правило, используется протокол TCP (Transmission Control Protocol – протокол управления передачей). Для ненадежной передачи пакетов обычно применяется протокол UDP (User Data Protocol – пользовательский протокол данных). Все эти протоколы составляют основу Интернета. Для надежного потока пакетов специального протокола нет.
  Прежде чем сокет может быть использован для работы в сети, с ним должен быть связан адрес. Этот адрес может принадлежать к одному из нескольких про­странств адресов. Наиболее распространенным пространством является простран­ство адресов Интернета, использующее 32-разрядные числа для идентификации конечных адресатов в протоколе IPv4 и 128-разрядные числа в протоколе IPv6.
  Как только сокеты созданы на машине-источнике и машине-приемни­ке, между ними может быть установлено соединение (для ориентированной на соединение связи). Одна сторона обращается к системному вызову listen, указы­вая в качестве параметра локальный сокет. При этом системный вызов создает буфер и блокируется до тех пор, пока не прибудут данные. Другая сторона обра­щается к системному вызову connect, задавая в параметрах дескриптор файла для локального сокета и адрес удаленного сокета. Если удаленная машина прини­мает вызов, тогда система устанавливает соединение между двумя сокетами.
  Функции установленного соединения аналогичны функциям канала. Процесс может читать из канала и писать в него, используя дескриптор файла для локаль­ного сокета. Когда соединение далее не требуется, оно может быть закрыто обычным способом, при помощи системного вызова close.
 
 7.4.2. Реализация ввода-вывода в системе UNIX
 
  Ввод-вывод в операционной системе UNIX реализуется набором драйверов уст­ройств, по одному для каждого типа устройств. Функция драйвера заключается в изолировании остальной части системы от индивидуальных отличительных осо­бенностей аппаратного обеспечения. При помощи стандартных интерфейсов меж­ду драйверами и остальной операционной системой большая часть системы вво­да-вывода может быть помещена в машинно-независимую часть ядра.
  Когда пользователь получает доступ к специальному файлу, файловая система определяет номера старшего и младшего устройств, а также выясняет, является ли файл блочным специальным файлом или символьным специальным файлом. Но­мер старшего устройства используется в качестве индекса для одного из двух внут­ренних массивов структур – bdevsw для блочных специальных файлов или cdevsw для символьных специальных файлов. Найденная таким образом структура содер­жит указатели на процедуры открытия устройства, чтения из устройства, записи на устройство и т. д. Номер младшего устройства передается в виде параметра. Добавление нового типа устройства к системе UNIX означает добавление нового элемента к одной из этих таблиц, а также предоставление соответствующих про­цедур выполнения различных операций с устройством.
  Каждый драйвер разделен на несколько частей. Верхняя часть драйвера работа­ет в режиме вызывающего процесса и служит интерфейсом с остальной системой UNIX. Нижняя часть работает в контексте ядра и взаимодействует с устройством. Драйверам разрешается обращаться к процедурам ядра для выделения памяти, управления таймером, управления DMA и т. д.
  Система ввода-вывода разделена на два основных компонента: обработку блоч­ных специальных файлов и обработку символьных специальных файлов. Цель той части системы, которая занимается операциями ввода-вывода с блоч­ными специальными файлами (например, дисковым вводом-выводом), заключа­ется в минимизации количества операций переноса данных. Для достижения дан­ной цели в системах UNIX между дисковыми драйверами и файловой системой помещается буферный кэш. Буферный кэш представляет собой таб­лицу в ядре, в которой хранятся тысячи недавно использованных блоков. Когда файловой системе требуется блок диска (например, блок i-узла, каталога или дан­ных), сначала проверяется буферный кэш. Если нужный блок есть в кэше, он по­лучается оттуда, при этом обращения к диску удается избежать. Буферный кэш значительно улучшает производительность системы. Если же блока нет в буферном кэше, он считывается с диска в кэш, а оттуда копируется туда, куда нужно. Поскольку в буферном кэше есть место только для фиксированного количества блоков, требуется некий алгоритм управления кэшем. Обычно блоки в кэше организуются в связный список. При каждом обращении к блоку он перемещается в начало списка. Если в кэше не хватает места для нового блока, то из него удаляется самый старый блок, находящийся в конце списка. Буферный кэш поддерживает не только операцию чтения с диска, но также и запись на диск. Когда программа пишет блок, этот блок не попадает напрямую на диск, а отправляется в кэш. Только когда кэш наполняется, модифицированные блоки кэша сохраняются на диске. Чтобы модифицированные блоки не хранились в кэше слишком дол-го, их принудительная выгрузка на диск про­изводится каждые 30 с.
  В течение десятилетий драйверы устройств системы UNIX статически компоно­вались вместе с ядром, так что все они постоянно находились в памяти при каждой загрузке системы. Такая схема хорошо работала в условиях мало меняющихся кон­фигураций вычислительных машин. Все изменилось с появлением системы Linux, ориентированной в первую очередь на поддержку персональных компьютеров. Количество всевозможных устройств ввода-вывода на персональных компьютерах значительно больше, чем у классических вычислительных машин. Кроме того, хотя у пользователей системы Linux есть возможность иметь полный набор исходных текстов операционной систе­мы, подавляющее большинство пользователей будет испытывать существенные трудности с добавлением нового драйвера, обновлением файлов cdevsw или bdevsw, компоновкой ядра и установкой его как загружаемой системы. В операционной системе Linux подобные проблемы были решены при помощи концепции подгружаемых модулей. Это куски кода, которые могут быть загру­жены в ядро во время работы операционной системы. Как правило, это драйверы символьных или блочных устройств, но подгружаемым модулем также могут быть целая файловая система, сетевые протоколы, программы для отслеживания про­изводительности системы и т. д.
  При загрузке модуля должно выполняться несколько определенных действий. Во-первых, модуль должен быть «на лету» перенастроен на новые адреса. Во-вторых, система должна проверить, доступны ли ресурсы, необходимые драйверу (напри­мер, определенные уровни запроса прерывания), и если они доступны, то поме­тить их как используемые. В-третьих, должны быть настроены все необходимые векторы прерываний. В-четвертых, для поддержки нового типа старшего устрой­ства следует обновить таблицу переключения драйверов. Наконец, драйверу по­зволяется выполнить любую специфическую для данного устройства процедуру инициализации. Когда все эти этапы выполнены, драйвер является полностью ус­тановленным, как и драйвер, установленный статически. Некоторые современные системы UNIX также поддерживают подгружаемые модули.
 
 7.4.3. Потоки данных в UNIX
 
  Так как символьные специальные файлы имеют дело с символьными потоками, а не перемещают блоки данных между памятью и диском, они не пользуются бу­ферным кэшем. Вместо этого в первых версиях системы UNIX каждый драйвер символьного устройства выполнял всю работу, требуемую для данного устройства. Однако с течением времени стало ясно, что многие драйверы, например програм­мы буферизации, управления потоком и сетевые протоколы, дублировали проце­дуры друг друга. Поэтому для структурирования драйверов символьных устройств и придания им модульности было разработано два решения.
  Первое решение, реализованное в системе BSD, основано на структурах данных, при­сутствующих в классических системах UNIX и называемых С-списками. Каждый С-список представляет собой блок размером до 64 символов плюс счетчик и указатель на следующий блок. Символы, поступающие с терминала или любого другого символьного устройства, буферизируются в цепочках таких блоков. Когда пользовательский процесс считывает данные из /dev/tty (то есть из стандартного входного потока), символы не передаются процессу напрямую из С-списков. Вместо этого они пропускаются через процедуру, расположенную в ядре и называемую дисциплиной линии связи. Дисциплина линии связи ра­ботает как фильтр, принимая необработанный поток символов от драйвера терми­нала, обрабатывая его и формируя то, что называется обработанным символьным потоком. В обработанном потоке выполняются операции локального строково­го редактирования (например, удаляются отмененные пользователем символы и строки), а также выполняются другие специальные операции обработки. Обработанный поток передается процессу. Однако если процесс желает воспринимать каждый символ, введенный пользователем, он может принимать необработанный поток, минуя дисциплину линии связи. Вывод работает аналогично, заменяя табуляторы пробелами, добавляя символы-заполнители и т. д. Как и входной поток, выходной символьный поток может быть пропущен через дисциплину линии связи (обработанный режим) или мино­вать ее (необработанный режим). Необработанный режим особенно полезен при отправке двоичных данных на другие машины по линии последовательной передачи или для графических интерфейсов пользователя. Здесь не требуется ни­какого преобразования.
  Второе решение реализовано в системе System V под названием потоков данных. Потоки данных основаны на возможности динамически соединять процесс пользователя с драйвером, а также динамически, во время исполнения, вставлять модули обра­ботки в поток данных. В некотором смысле поток представляет собой работающий в ядре аналог каналов в пространстве пользователя.
  У потока данных всегда есть голова потока у вершины и соединение с драйве­ром у основания. В поток может быть вставлено столько модулей, сколько необ­ходимо. Обработка может происходить в обоих направлениях, так что каждому модулю может понадобиться одна секция для чтения (из драйвера) и одна секция для записи (в драйвер). Когда процесс пользователя пишет данные в поток, про­грамма в голове потока интерпретирует системный вызов и запаковывает данные в буферы потока, передаваемые от модуля к модулю вниз, при этом каждый мо­дуль выполняет соответствующие преобразования. У каждого модуля есть очередь чтения и очередь записи, так что буферы обрабатываются в правильном порядке. У модулей есть строго определенные интерфейсы, определяемые инфраструкту­рой потока, что позволяет объединять вместе несвязанные модули.
  Важное свойство потоков данных – мультиплексирование. Мультиплексный модуль может взять один по­ток и расщепить его на несколько потоков или, наоборот, объединить несколько потоков в единый поток.
 
 
 7.5. Файловые системы UNIX
 
 7.5.1. Основные понятия
 
  Файл в системе UNIX – это последовательность байтов произвольной длины (от 0 до некоторого максимума), содержащая произвольную информацию. Не делается принципиального различия между текстовыми (ASCII) файлами, двоичными фай­лами и любыми другими файлами. Значение битов в файле целиком определяется владельцем файла. Системе это безразлично. Изначально размер имен файлов был ограничен 14 символами, но в системе Berkeley UNIX этот предел был расширен до 255 символов, что впоследствии было принято в System V, а также в большин­стве других версий. В именах файлов разрешается использовать все ASCII-симво­лы, кроме символа NUL.
  По соглашению многие программы ожидают, что имена файлов должны состоять из основного имени и расширения, отделяемого от основного имени файла точкой (которая в системе UNIX также считается символом). Эти со­глашения никак не регулируются операционной системой, но некоторые компи­ляторы и другие программы ожидают файлов именно с такими расширениями. Расширения могут иметь произвольную длину, кроме того, файлы могут иметь по нескольку расширений.
  Для удобства использования файлы могут группироваться в каталоги. Катало­ги хранятся на диске в виде файлов, и до определенного предела с ними можно работать как с файлами. Каталоги могут содержать подкаталоги, что приводит к иерархической файловой системе. Корневой каталог называется / и, как правило, содержит несколько подкаталогов. Символ / также используется для разделения имен каталогов.
  Существует два способа задания имени файла в системе UNIX, как в оболочке, так и при открытии файла из программы. Первый способ заключается в использо­вании абсолютного пути, указывающего, как найти файл от корневого каталога. Пример абсолютного пути: /man/labs/operat/numb4. Он сообщает системе, что в корневом каталоге следует найти каталог man, затем в нем найти каталог labs, ко­торый содержит каталог operat, а в нем расположен файл numb4.
  Абсолютные имена путей часто бывают длинными и неудобными. По этой при­чине операционная система UNIX позволяет пользователям и процессам обозначить каталог, в котором они работают в данный момент, как рабочий каталог (также называемый текущим каталогом). Имена путей также могут указываться относи­тельно рабочего каталога. Путь файла, заданный относительно рабочего каталога, называется относительным путем.
  Если пользователю необходимо обратиться к файлам, принадлежа­щим другим пользователям, или к своим файлам, расположенным в другом месте дерева файлов, а абсолют­ное имя пути представляется доста­точно длинным, то в системе UNIX эта проблема решается при помощи так называемых связей, представляющих собой записи каталога, которые указывают на другие файлы.
  Кроме обычных файлов, системой UNIX также поддерживаются символьные специальные файлы и блочные специальные файлы. Символьные специальные файлы используются для моделирования последовательных устройств ввода-вы­вода, таких как клавиатуры и принтеры. Если процесс откроет файл /dev/tty и про­читает из него, он получит символы, введенные с клавиатуры. Если открыть файл /dev/lp и записать в него данные, то эти данные будут распечатаны на принтере.
  Блочные специальные файлы, часто с такими именами, как /dev/hd1, могут исполь­зоваться для чтения и записи необработанных дисковых разделов, минуя файло­вую систему. Таким образом, поиск байта номер k, за которым последует чтение, приведет к чтению k-то байта на соответствующем дисковом разделе, игнорируя i-узел и файловую структуру. Необработанные блочные устройства используют­ся для страничной подкачки и свопинга программами установки файловой систе­мы (например, mkfs) и программами, исправляющими ломаные файловые систе­мы (например, fsck).
  При наличии у вычислительной машины нескольких дисков возникает вопрос управления ими. Одно из решений заключается в том, чтобы установить самостоятельную файло­вую систему на каждый отдельный диск и управлять ими как отдельными файло­выми системами. При таком решении пользователь должен помимо каталогов указывать также и устройство, если оно отличается от используемого по умолчанию. Такой подход применяется в операционных системах MS-DOS, Windows 98 и VMS. Решение, применяемое в операционной системе UNIX, заключается в том, чтобы позволить монтировать один диск в дерево файлов другого диска. Например, можно смонтировать дискету в каталог жесткого диска. При этом пользователь будет видеть еди­ное дерево файлов и уже не должен думать о том, какой файл на каком устройстве хранится.
  Другим интересным свойством файловой системы UNIX является бло­кировка. В некоторых приложениях два и более процессов могут одновременно использовать один и тот же файл, что может привести к конфликту. Одно из ре­шений данной проблемы заключается в том, чтобы создать в приложении крити­ческие области. Однако если эти процессы принадлежат независимым пользова­телям, такой способ координации действий, как правило, очень неудобен. Рассмотрим, например, базу данных, состоящую из многих файлов в одном или нескольких каталогах, доступ к которым могут получить никак не связанные меж­ду собой пользователи. С каждым каталогом или файлом можно связать семафор и достичь взаимного исключения, заставляя процессы выполнять операцию down на соответствующем семафоре, прежде чем читать или писать определенные дан­ные. Недостаток этого решения заключается в том, что недоступным становится весь каталог или файл, даже если процессам нужна всего одна запись. По этой причине стандартом POSIX предоставляется гибкий и детальный ме­ханизм, позволяющий процессам за одну неделимую операцию блокировать даже единственный байт файла или целый файл по желанию. Механизм блокировки требует от вызывающего его процесса указать блокируемый файл, начальный байт и количество байтов. Если операция завершается успешно, система создает запись в таблице, в которой указывается, что определенные байты файла заблокированы.
  Стандартом определены два типа блокировки: блокировка с монополизацией и блокировка без монополизации. Если часть файла уже содержит блокировку без монополизации, то повторная установка блокировки без монополизации на это место файла разрешается, но попытка установки блокировку с монополизацией будет отвергнута. Если же какая-либо область файла содержит блокировку с моно­полизацией, то любые попытки заблокировать любую часть этой области файла бу­дут отвергаться, пока не будет снята монопольная блокировка. Для успешной уста­новки блокировки необходимо, чтобы каждый байт в данной области был доступен. При установке блокировки процесс должен указать, хочет ли он сразу получить управление или будет ждать, пока не будет установлена блокировка. Если процесс выбрал вызов с ожиданием, то он блокируется до тех пор, пока с запрашиваемой области файла не будет снята блокировка, установленная другим процессом, пос­ле чего процесс активизируется, и ему сообщается, что блокировка установлена. Если процесс решил воспользоваться системным вызовом без ожидания, он не­медленно получает ответ об успехе или неудаче операции.
 
 7.5.2. Реализация классической файловой системы UNIX
 
  Все системы UNIX могут под­держивать несколько дисковых разделов, каждый со своей файловой системой.
  В классической системе UNIX раздел диска содержит файловую систему со следующей структурой. Блок 0 не используется системой и час­то содержит программу загрузки компьютера. Блок 1 представляет собой супер­блок. В нем хранится критическая информация о размещении файловой системы, включая количество i-узлов, количество дисковых блоков, а также начало списка свободных блоков диска (обычно несколько сот записей). При уничтожении супер­блока файловая система окажется нечитаемой. Следом за суперблоком располагаются i-узлы (i-nodes, сокращение от index-nodes – индекс-узлы). Они нумеруются от 1 до некоторого максимального числа. Каждый i-узел имеет 64 байт в длину и описывает ровно один файл, i-узел содер­жит учетную информацию (включая всю информацию, возвращаемую системным вызовом stat, который ее просто берет в i-узле), а также достаточное количество информации, чтобы найти все блоки файла на диске. Следом за i-узлами располагаются блоки с данными. Здесь хранятся все фай­лы и каталоги. Если файл или каталог состоит более чем из одного блока, блоки файла не обязаны располагаться на диске подряд. В действительности блоки боль­шого файла, как правило, оказываются разбросанными по всему диску.
  Каталог в классической файловой системе представляет собой несортированный набор 16-байтовых записей. Каждая запись состоит из 14-байтного имени файла и номера i-узла. Чтобы открыть файл в рабочем каталоге, система просто считывает каталог, сравнивая имя искомого файла с каж­дой записью, пока не найдет нужную запись или пока не закончится каталог.
  Если искомый файл присутствует в каталоге, система извлекает его i-узел и использует его в качестве индекса в таблице i-узлов (на диске), чтобы найти соответствующий i-узел и считать его в память. Этот i-узел помещается в таблицу i-узлов – структуру данных в ядре, содержащую все i-узлы открытых в данный мо­мент файлов и каталогов. Формат i-узлов варьируется от одной версии UNIX к другой. Как минимум i-узел должен содержать все поля, возвращаемые системным вызовом stat.
  Поиск файла по абсолютному пути, например /man/labs/operat немного сложнее. Сначала система находит корневой каталог, как правило, использующий i-узел с но­мером 2 (i-узел 1 обычно резервируется для хранения дефектных блоков). Затем он ищет в корневом каталоге строку «man», чтобы получить номер i-узла каталога /man. Затем считывается этот i-узел, и из него извлекаются номера блоков, в которых располагается каталог /man. После этого считывается каталог /man, в котором ищет­ся строка «labs». Когда нужная запись найдена, из нее извлекается номер i-узла для каталога /man/labs и т. д. Таким образом, использование относительного имени фай­ла не только удобнее для пользователя, но также представляет существенно мень­шее количество работы для файловой системы.
  Рассмотрим далее, как система считывает файл. По дескриптору файла файловая система должна найти i-узел соответствую­щего файла. С каждым дескриптором файла должен быть связан указатель в файле, определяющий байт в файле, кото­рый будет считан или записан при следующем обращении к файлу. Указатель помещается в таблицу деск­рипторов файла. При этом каждый процесс, открывающий файл, получает соб­ственную позицию в файле. Для передачи указателя от одного процесса другому вводится в обращение новая таблица – таблица открытых файлов, которая располагается между таблицей дескрипторов файлов и таблицей i-узлов. Указатели хранятся в файле, а бит чтения/записи в этой таблице. Задача таблицы открытых файлов заключа­ется в том, чтобы позволить родительскому и дочернему процессам совместно использовать один указатель в файле, но для посторонних процессов выделять отдельные указатели.
  Каждый i-узел содержит дисковые адреса первых 10 блоков файла. Если позиция в файле попадает в его первые 10 блоков, то считывается нужный блок файла, а данные копируются пользователю. Для поддержки файлов, длина которых превышает 10 блоков, в i-узле содержится дисковый адрес одинарного косвенного блока. Этот блок содержит дисковые адреса дополни­тельных блоков файла. Например, если размер блока составляет 1 Кбайт, а диско­вый адрес занимает 4 байта, то одинарный косвенный блок может хранить до 256 дис­ковых адресов. Такая схема позволяет поддержать файлы размером до 266 Кбайт. Для файлов, размер которых превосходит 266 Кбайт, используется двойной косвенный блок. Он содержит адреса 256 одинарных косвенных блоков, каждый из которых содержит адреса 256 блоков данных. Такая схема позволяет поддер­жать файлы размером до 10 + 2 в степени 16 блоков (67 119 104 байт). Если и этого оказыва­ется недостаточно, в i-узле есть место для тройного косвенного блока. Его указа­тели показывают на 256 двойных косвенных блоков.
 7.5.3. Реализация файловой системы Berkeley Fast
  Приведенное выше описание объясняет принципы работы классической файло­вой системы UNIX. Теперь познакомимся с усовершенствованиями этой системы, реализованными в версии Berkeley. Во-первых, были реорганизованы каталоги. Длина имен файлов была увеличена до 255 символов. Для обеспечения совместимости двух систем в системе Berkeley были разработа­ны специальные системные вызовы, чтобы программы мог­ли читать каталоги, не зная их внутренней структуры. Позднее длинные имена файлов и эти системные вызовы были добавлены ко всем другим версиям UNIX и к стандарту POSIX.
  Каждый каталог BSD, поддерживающей имена файлов длиной до 255 сим­волов, состоит из некоторого целого коли­чества дисковых блоков, так что каталоги могут записываться на диск как единое целое. Внутри каталога записи файлов и каталогов никак не отсортированы, при этом каждая запись сразу следует за предыдущей записью. В конце каждого блока может оказаться несколько неиспользованных байтов, так как записи могут быть различного размера. Каждая каталоговая запись состоит из четырех полей фиксиро­ванной длины и одного поля переменной длины. Первое поле представляет собой номер i-узла. Следом за номером i-узла идет поле, сообщающее размер всей каталоговой записи в байтах, возможно, вместе с дополнительными байтами-заполнителями в конце записи. Это поле необходимо, чтобы найти следующую запись. Затем располагается поле типа файла, определяющее, является ли этот файл каталогом и т. д. Последнее поле содержит длину имени файла в байтах. Наконец, идет само имя файла, заканчивающееся нулевым байтом и дополненное до 32-битовой границы. За ним могут следовать дополнительные байты-заполнители.
  Поскольку поиск в каталогах производится линейно, он может занять много времени, пока не будет найдена запись у конца большого каталога. Для увеличе­ния производительности в BSD было добавлено кэширование имен. Прежде чем искать имя в каталоге, система проверяет кэш. Если имя файла есть в кэше, то в каталоге его уже можно не искать.
  Вторым существенным изменением, введенным в Berkeley, было разбиение диска на группы цилиндров, у каждой из которых был собственный суперблок, i-узлы и блоки данных. Идея такой организации диска заключается в том, чтобы хранить i-узел и блоки данных файла ближе друг к другу. Тогда при обращении к файлам снижается время, затрачиваемое жестким диском на перемещение блоков головок. По мере возможности блоки для файла выделяются в группе цилиндров, в которой содержится i-узел.
  Третье изменение заключалось в использовании блоков не одного, а двух раз­меров. Для хранения больших файлов значительно эффективнее использовать небольшое количество крупных блоков, чем много маленьких блоков. С другой стороны, размер многих файлов в системе UNIX невелик, поэтому при использо­вании только блоков большого размера расходовалось бы слишком много диско-вого пространства. Наличие блоков двух размеров обеспечивает эффективное чте­ние/запись для больших файлов и эффективное использование дискового про­странства для небольших файлов. Платой за эффективность является значитель­ная дополнительная сложность программы.
 7.5.4. Реализация файловой системы Linux
  Изначально в операционной системе Linux использовалась файловая система опе­рационной системы MINIX. Однако в системе MINIX длина имен файлов ограни­чивалась 14 символами (для совместимости с UNIX Version 7), а максимальный размер файла был равен 64 Мбайт. Поэтому у разработчиков операционной систе­мы Linux практически сразу появился интерес к усовершенствованию файловой системы. Первым шагом вперед стала файловая система Ext, в которой длина имен файлов была увеличена до 255 символов, а размер файлов – до 2 Гбайт. Однако эта система была медленнее файловой системы MINIX, поэтому была разработана файловая система Ext2 с длинными именами файлов, длинными файлами и высокой производительностью. Эта файловая система и стала основной файловой системой Linux. Однако опера­ционная система Linux также поддерживает еще более десятка файловых систем, используя для этого файловую систему NFS (описанную в следующем разделе). При компоновке операционной системы Linux предлагается сделать выбор файло­вой системы, которая будет встроена в ядро. Другие файловые системы при необхо­димости могут динамически подгружаться во время исполнения в виде модулей.
  Файловая система Ext2 очень похожа на файловую систему Berkeley Fast с небольшими изменениями. Вместо того, чтобы использовать группы цилин­дров, что практически ничего не значит при современных дисках с виртуальной геометрией, она делит диск на группы блоков, независимо от того, где располага­ются границы между цилиндрами. Каждая группа блоков начинается с суперблока, в котором хранится информация о том, сколько блоков и i-узлов находятся в дан­ной группе, о размере группы блоков и т. д. Затем следует описатель группы, содер­жащий информацию о расположении битовых массивов, количестве свободных блоков и i-узлов в группе, а также количестве каталогов в группе. Эта информация важна, так как файловая система Ext2 пытается распространить каталоги равномер­но по всему диску. В двух битовых массивах ведется учет свободных блоков и свободных i-узлов. Размер каждого битового массива равен одному блоку. При размере блоков в 1 Кбайт такая схема ограничивает размер группы блоков 8192 блоками и 8192 i-узлами. На практике ограничение числа i-узлов никогда не встречается, так как блоки заканчиваются раньше. Затем располагаются сами i-узлы. Размер каждого i-узла – 128 байт, что в два раза больше размера стандартных i-узлов в UNIX. Дополнительные байты в i-узле используются следующим образом. Вместо 10 пря­мых и 3 косвенных дисковых адресов файловая система Linux позволяет 12 пря­мых и 3 косвенных дисковых адреса. Кроме того, длина адресов увеличена с 3 до 4 байт, и это позволяет поддерживать дисковые разделы размером более 224 бло­ков (16 Гбайт), что уже стало проблемой для UNIX.
  Работа файловой системы похожа на функционирование быстрой файловой системы Berkeley. Однако в отличие от BSD, в системе Linux используются дис­ковые блоки только одного размера – 1 Кбайт. Быстрая файловая система Berkeley использует 8-килобайтные блоки, которые затем разбиваются при необходимости на килобайтные фрагменты. Файловая система Ext2 делает примерно то же самое, но более простым способом. Как и система Berkeley, когда файл увеличивается в размерах, файловая система Ext2 пытается поместить новый блок файла в ту же группу блоков, что и остальные блоки, желательно сразу после предыдущих бло­ков. Кроме того, при создании нового файла в каталоге файловая система Ext2 ста­рается выделить ему блоки в той же группе блоков, в которой располагается ката­лог. Новые каталоги, наоборот, равномерно распределяются по всему диску.
  Другой файловой системой Linux является файловая система /рrос (process – процесс). Идея этой файловой системы изначально была реализована в 8-й редак­ции операционной системы UNIX, созданной лабораторией Bell Labs, а позднее скопированной в 4.4BSD и System V. Однако в операционной системе Linux дан­ная идея получила дальнейшее развитие. Основная концепция этой файловой системы заключается в том, что для каждого процесса системы создается подката­лог в каталоге /рrос. В этом каталоге располагаются файлы, которые хранят информацию о процессе – его командную строку, строки окружения и маски сигналов. В действительности этих файлов на диске нет. Когда они считываются, система получает информацию от фактического процесса и возвращает ее в стандартном формате. Многие расширения, реализованные в операционной системе Linux, относятся к файлам и каталогам, расположенным в каталоге /рrос. Они содержат информацию о центральном процессоре, дисковых разделах, векторах прерывания, счетчиках ядра, файловых системах, подгружаемых модулях и о многом другом. Непривилегирован­ные программы пользователя могут читать большую часть этой информации, что позволяет им узнать о поведении системы безопасным способом. Некоторые из этих файлов могут записываться в каталог /рrос, чтобы изменить параметры системы.
 
 7.5.5. Реализация файловой системы NFS
  Файловая система NFS (Network File System – сетевая файловая система) корпо­рации Sun Microsystems, использующуюся на всех современных системах UNIX (а также на некоторых не-UNIX системах) для объединения на логическом уров­не файловых систем отдельных сетевых машин в единое целое.
  В основе файловой системы NFS лежит представление о том, что пользоваться общей файловой системой может произвольный набор клиентов и серверов. Во мно­гих случаях все клиенты и серверы располагаются на одной и той же локальной сети, хотя этого не требуется. Файловая система NFS может также работать в гло­бальной сети, если сервер находится далеко от клиента. Для простоты мы будем говорить о клиентах и серверах, как если бы они работали на различных машинах, хотя файловая система NFS позволяет каждой машине одновременно быть клиентом и сервером. Каждый сервер файловой системы NFS экспортирует один или несколько ее каталогов, предоставляя доступ к ним удаленным клиентам. Как правило, доступ к каталогу предоставляется вместе со всеми его подкаталогами, то есть все дерево каталогов экспортируется как единое целое. Список экспортируемых сервером каталогов хранится в файле /etc/fexports, таким образом, эти каталоги экспортиру­ются автоматически при загрузке сервера. Клиенты получают доступ к экспорти­руемым каталогам, монтируя эти каталоги. Когда клиент монтирует удаленный каталог, этот каталог становится частью иерархии каталогов клиента. У одного и того же файла могут быть различные имена на различных клиентах, так как их каталоги могут монтироваться в различных узлах каталоговых деревьев. Выбор узла, в котором монтируется уда­ленный каталог, целиком зависит от клиента. Сервер не знает, где клиент монти­рует его каталог.
  Так как одна из целей файловой системы NFS заключается в поддержке разнород­ных систем, в которых клиенты и серверы могут работать под управлением раз­личных операционных систем и на различном оборудовании, существенно, чтобы интерфейс между клиентами и серверами был тщательно определен. Только в этом случае можно ожидать, что новый написанный клиент будет корректно работать с существующими серверами, и наоборот. В файловой системе NFS эта задача выполняется при помощи двух протоко­лов клиент-сервер. Протокол – это набор запросов, посылаемых клиентами сер­верам, и ответов серверов, посылаемых клиентам.
  Первый протокол NFS управляет монтированием каталогов. Клиент может послать серверу путь к каталогу и запросить разрешение смонтировать этот каталог где-либо в своей иерархии каталогов. Данные о месте, в котором клиент намерева­ется смонтировать удаленный каталог, серверу не посылаются, так как серверу это безразлично. Если путь указан верно и указанный каталог был экспортирован, тогда сервер возвращает клиенту дескриптор файла, содержащий поля, однознач­но идентифицирующие тип файловой системы, диск, i-узел каталога и информа­цию о правах доступа. Этот дескриптор файла используется последующими обра­щениями чтения и записи к файлам в монтированном каталоге или в любом из его подкаталогов.
  Во время загрузки операционная система UNIX, прежде чем перейти в много­пользовательский режим, запускает сценарий оболочки /etc/rc. В этом сценарии можно разместить команды монтировки файловых систем. Таким образом, все необходимые удаленные файловые системы будут автоматически смонтированы прежде, чем будет разрешена регистрация в системе. В качестве альтернативы в большинстве версий системы UNIX также поддерживается автомонтировка. Это свойство позволяет ассоциировать с локальным каталогом несколько удаленных каталогов. Ни один из этих удаленных каталогов не монтируется во время загруз­ки операционной системы (не происходит даже контакта с сервером). Вместо это­го при первом обращении к удаленному файлу (когда файл открывается) опера­ционная система посылает каждому серверу сообщение. Побеждает ответивший первым сервер, чей каталог и монтируется.
  У автомонтировки есть два принципиальных преимущества перед статической монтировкой с использованием файла /etc/rc. Во-первых, если один из серверов, перечисленных в файле /etc/rc, окажется выключенным, запустить клиента будет невозможно, по крайней мере без определенных трудностей, задержки и большого количества сообщений об ошибках. Если пользователю в данный момент этот сервер не нужен, вся работа просто окажется напрасной. Во-вторых, предоставление кли­енту возможности связаться с несколькими серверами параллельно позволяет зна­чительно повысить устойчивость системы к сбоям (так как для работы достаточно всего одного работающего сервера) и улучшить показатели производи-тельности (так как первый ответивший сервер скорее всего окажется наименее загруженным). С другой стороны, при таком подходе неявно подразумевается, что все указан­ные как альтернативные файловые системы идентичны для автомонтировки. Так как файловая система NFS не предоставляет поддержки репликации файлов или каталогов, то следить за идентичностью всех файловых систем должен сам пользо­ватель. Поэтому автомонтировка, как правило, используется для файловых сис­тем, в которых клиенту разрешено только чтение. Такие файловые системы обыч­но содержат системные двоичные файлы, а также другие редко изменяемые файлы.
  Второй протокол NFS предназначен для доступа к каталогам и файлам. Клиен­ты могут посылать серверам сообщения, содержащие команды управления ката­логами и файлами, что позволяет им создавать, удалять, читать и писать файлы. Кроме того, у клиентов есть доступ к атрибутам файла, таким как режим, размер и время последнего изменения файла. Файловой системой NFS поддерживается большинство системных вызовов операционной системы UNIX, за исключением системных вызовов open и close. Пропуск системных вызовов open и close не случаен. Это сделано намеренно. Нет необходимости открывать файл, прежде чем прочитать его. Также не нужно закрывать файл после того, как данные из него прочитаны. Вместо этого, чтобы прочитать файл, клиент посылает на сервер сообщение lookup, содержащее имя файла, с запросом найти этот файл и вернуть дескриптор файла, представляющий собой структуру, идентифицирующую файл (то есть содержащую идентификатор файловой системы и номер i-узла вместе с прочей информацией). В отличие от системного вызова open, операция lookup не копирует никакой информации во внутренние системные таблицы. Системному вызову read подается на входе деск­риптор файла, который предстоит прочитать, смещение в файле, а также количе­ство байтов, которые нужно прочитать. Таким образом, каждое сообщение явля­ется самодостаточным. Преимущество такой схемы заключается в том, что серверу не нужно помнить что-либо об открытых соединениях между обращениями к нему. Поэтому если на сервере произойдет сбой с последующей перезагрузкой, не будет потеряно никакой информации об открытых файлах, так как терять просто нече­го. Такие серверы называются серверами без состояния.
  К сожалению, метод файловой системы NFS усложняет достижение точной файловой семантики системы UNIX. Например, в операционной системе UNIX файл может быть открыт и заблокирован, так что никакой другой процесс не сможет получить к этому файлу доступ. Когда файл закрывается, все его блокировки сни­маются. В сервере без состояния, как в файловой системе NFS, с открытыми фай­лами нельзя связать блокировку, так как сервер не знает, какие файлы открыты. Следовательно, в файловой системе NFS требуется отдельный специальный ме­ханизм осуществления блокировки.
  Файловая система NFS использует стандартный механизм защиты UNIX с би­тами rwx для владельца, группы и всех прочих пользователей. Изначально каждое со­общение с запросом просто содержало идентификаторы пользователя и группы вызывающего процесса, которые сервер NFS использовал для проверки прав до­ступа. В настоящее время для установки надежного ключа для аутентификации клиента и сервера при каждом запросе и каждом отве­те можно использовать шифрование с открытым ключом. При этом злоумышлен­ник не сможет выдать себя за другого клиента (или другой сервер), так как ему неизвестен секретный ключ этого клиента (или сервера).
  Хотя реализация программ клиента и сервера не зависит от протоколов NFS, в боль­шинстве систем UNIX используется их трехуровневая реализация. Верхний уровень представляет собой уровень системных вызовов. Он управляет такими системными вызовами, как open, read и close. После анализа системного вызова и проверки его параметров он вызывает второй уровень – уровень VFS (Virtual File System – виртуальная файловая система).
  Задача уровня VFS заключается в управлении таблицей, содержащей по одной записи для каждого открытого файла, аналогичной таблице i-узлов для открытых файлов в системе UNIX. В обычной системе UNIX i-узел однозначно указывается парой: устройство – номер i-узла. Вместо этого уровень VFS содержит для каждо­го открытого файла записи, называемые v-узлами (virtual i-node – виртуальный i-узел). V-узлы используются, чтобы отличать локальные файлы от удаленных. Для удаленных файлов предоставляется информация, достаточная для доступа к ним. Для локальных файлов записываются сведения о файловой системе и i-узле, так как современные системы UNIX могут поддерживать несколько файловых систем (например, V7, Berkeley Fast, ext2, /proc, FAT и т. д.). Хотя уровень VFS был создан для поддержки файловой системы NFS, сегодня он поддерживается большинством современных систем UNIX как составная часть операционной сис­темы, даже если NFS не используется.
  Чтобы понять, как используются v-узлы, рассмотрим выполнение последова­тельности системных вызовов mount, open и read. Чтобы смонтировать файловую систему, системный администратор (или сценарий /etc/rc) вызывает программу mount, указывая ей удаленный каталог, локальный каталог, в котором следует смонтировать удаленный каталог, и прочую информацию. Программа mount ана­лизирует имя удаленного каталога и обнаруживает имя сервера NFS, на котором располагается удаленный каталог. Затем она соединяется с этой машиной, запра­шивая у нее дескриптор удаленного каталога. Если этот каталог существует и его удаленное монтирование разрешено, сервер возвращает его дескриптор. Наконец, программа mount обращается к системному вызову mount, передавая ядру получен­ный от сервера дескриптор каталога. Затем ядро формирует для удаленного каталога v-узел и просит программу клиента NFS создать в своих внутренних таблицах r-узел (удален­ный i-узел) для хранения дескриптора файла. V-узел указывает на r-узел. Каждый v-узел на уровне VFS будет в конечном итоге содержать либо указатель на r-узел в программе клиента NFS, либо указатель на i-узел в одной из локальных файловых систем. По содержимому v-узла можно понять, является ли файл или каталог локальным или удаленным. Если он локальный, то может быть найдена соответствующая файловая система и i-узел. Если файл удаленный, может быть найден удаленный хост и дескриптор файла.
  Когда на клиенте открывается удаленный файл, при анализе пути файла ядро обнаруживает каталог, в котором смонтирована удаленная файловая система. Оно видит, что этот каталог удаленный, а в v-узле каталога находит указатель на r-узел. Затем она просит программу клиента NFS открыть файл. Программа клиента NFS просматривает оставшуюся часть пути на удаленном сервере, ассоциированном с монтированным каталогом, и получает обратно дескриптор файла для него. Он создает в своих таблицах r-узел для удаленного файла и докладывает об этом уровню VFS, который помещает в свои таблицы v-узел для файла, указывающий на r-узел. Таким образом и в этом случае у каждого открытого файла или каталога есть v-узел, указывающий на r-узел или i-узел.
  Вызывающему процессу выдается дескриптор удаленного файла. Этот дескрип­тор файла отображается на v-узел при помощи таблиц уровня VFS. Необходимо обратить вни­мание, что на сервере не создается никаких записей в таблицах. Хотя сервер готов предоставить дескрипторы файлов по запросу, он не следит за состоянием дескрип­торов файлов. Когда дескриптор файла присылается серверу для доступа к файлу, сервер проверяет дескриптор и использует его, если дескриптор действителен. При проверке может проверяться ключ аутентификации, содержащийся в заголовках вызова удаленной процедуры RPC.
  Когда дескриптор файла используется в последующем системном вызове, на­пример read, уровень VFS находит соответствующий v-узел и по нему определяет, является ли он локальным или удаленным, а также какой i-узел или r-узел его опи­сывает. Затем он посылает серверу сообщение, содержащее дескриптор, смещение в файле (хранящееся на стороне клиента, а не сервера) и количество байтов. Для повышения эффективности обмен информацией между клиентом и сервером вы­полняется большими порциями, как правило, по 8192 байт, даже если запрашива­ется меньшее количество байтов.
  Когда сообщение с запросом прибывает на сервер, оно передается там уровню VFS, который определяет файловую систему, содержащую файл. Затем уровень VFS обращается к этой файловой системе, чтобы прочитать и вернуть байты. Эти данные передаются клиенту. После того, как уровень VFS клиента получает 8-ки-лобайтную порцию данных, которую запрашивал, он автоматически посылает за­прос на следующую порцию, чтобы она была под рукой, когда понадобится. Такая функция, называемая опережающим чтением, позволяет значительно увеличить производительность.
  При записи в удаленный файл проходится аналогичный путь от клиента к серверу. Данные также передаются 8-килобайтными порциями. Если системному вызову write подается менее 8 Кбайт данных, данные просто накапливаются ло­кально. Только когда порция в 8 Кбайт готова, она посылается серверу. Если файл закрывается, то весь остаток немедленно посылается серверу.
  Кроме того, для увеличения производительности применяется кэширование, как в обычной системе UNIX. Серверы кэшируют данные, чтобы снизить количе­ство обращений к дискам, но это происходит незаметно для клиентов. Клиенты управляют двумя кэшами, одним для атрибутов файлов (i-узлов) и одним для дан­ных. Когда требуется либо i-узел, либо блок файла, проверяется, нельзя ли полу­чить эту информацию из кэша. Если да, то обращения к сети можно избежать.
  Хотя кэширование на стороне клиента во много раз повышает производитель­ность, оно также приводит к появлению непростых проблем. Например, если два клиента сохранили в своих кэшах один и тот же блок файла, а затем один из клиентов его модифицировал, тогда другой клиент, считывая этот блок, получает из кэша старое значение блока. Учитывая серьезность данной проблемы, реализация NFS пытается смягчить ее остроту несколькими способами. Во-первых, с каждым блоком кэша ассоцииро­ван таймер. Когда время истекает, запись считается недействительной. Как прави­ло, для блоков с данными таймер устанавливается на 3 секунды, а для блоков каталога – на 30 секунд. Таким образом риск несколько снижается. Кроме того, при каждом открытии кэшированного файла серверу посылается сообщение, чтобы определить, когда в последний раз был модифицирован этот файл. Если последнее изменение про­изошло после того, как была сохранена в кэше локальная копия файла, эта копия из кэша удаляется, а с сервера получается новая копия. Наконец, каждые 30 секунд ис­текает время таймера, и все модифицированные («грязные») блоки кэша посылаются на сервер. Хотя такая схема и далека от совершенства, но она успешно используется системой в большинстве практических случаев.
 
 7.6. Безопасность в UNIX
 
 7.6.1. Основные понятия
 
  Каждый пользователь операционной системы UNIX регистрируется в системе, получая свой уникальный UID (User ID – идентификатор пользователя). UID представляет собой целое число в пределах от 0 до 65 535. Идентификатором владельца помеча­ются файлы, процессы и другие ресурсы. По умолчанию владельцем файла явля­ется пользователь, создавший этот файл, хотя владельца можно сменить.
  Пользователи могут организовываться в группы, которые также нумеруются 16-разрядными целыми числами, называемыми GID (Group ID – идентификатор группы). Назначение пользователя к группе выполняется вручную системным администратором и заключается в создании нескольких записей в системной базе данных, в которой содержится информация о том, какой пользователь к какой группе принадлежит. Вначале пользователь мог принадлежать только к одной группе, но теперь в некоторых версиях системы UNIX пользователь может одно­временно принадлежать к нескольким группам.
  Основной механизм безопасности в операционной системе UNIX заключается в следующем. Каж­дый процесс несет на себе UID и GID своего владельца. Когда создается файл, он получает UID и GID создающего его процесса. Файл также получает набор разре­шений доступа, определяемых создающим процессом. Эти разрешения определя­ют доступ к этому файлу для владельца файла, для других членов группы владель­ца файла и для всех прочих пользователей. Для каждой из этих трех категорий определяется три вида доступа: чтение, запись и исполнение файла, что обознача­ется соответственно буквами r, w и х (read, write, execute). Возможность исполнять файл, конечно, имеет смысл только в том случае, если этот файл является испол­няемой двоичной программой. Попытка запустить файл, у которого есть разре­шение на исполнение, но который не является исполняемым (то есть не начинает­ся с соответствующего заголовка), закончится ошибкой. Поскольку существует три категории пользователей и три вида доступа для каждой категории, все режимы доступа к файлу можно закодировать 9 битами.
  Пользователь, UID которого равен 0, является особым пользователем и назы­вается суперпользователем (superuser или root). Суперпользователь может читать и писать все файлы в системе, независимо от того, кто ими владеет и как они защищены. Процессы с UID=0 также обладают возможностью обращаться к не­большой группе системных вызовов, доступ к которым запрещен для обычных пользователей. Как правило, пароль суперпользователя известен только систем­ному администратору.
  Каталоги представляют собой файлы и обладают теми же самыми режимами защиты, что и обычные файлы. Отличие состоит в том, что бит х интерпретиру­ется в отношении каталогов как разрешение не исполнения, а поиска в каталоге. У специальных файлов, соответствующих устройствам ввода-вывода, есть те же самые биты защиты. Благодаря этому может использоваться тот же самый механизм для ограничения доступа к устройствам ввода-вывода. Существует общая проблема регулируемого доступа ко всем устройствам ввода-вывода и другим системным ресурсам. Эта проблема решается с помощью добавления к указанным выше 9 бит нового бита защиты – бита SETUID. Когда выполняется программа с уста­новленным битом SETUID, то запускаемому процессу присваивается не UID вы­звавшего его пользователя или процесса, a UID владельца файла. Когда процесс пытается открыть файл, то проверяется его рабочий UID, а не UID запустившего его пользователя. Таким образом, если программой, обращающейся к принтеру, будет владеть демон с установленным битом SETUID, тогда любой пользователь сможет запустить ее и запущенный процесс будет обладать полномочиями демона, но только для запуска этой программы (которая может устанавливать задания в очередь на принтер).
  Помимо бита SETUID, есть также еще и бит SETGID, работающий аналогич­но и временно предоставляющий пользователю рабочий GID программы. Однако на практике этот бит почти не используется.
 7.6.2. Реализация безопасности в UNIX
 
  Когда пользователь входит в систему, программа регистрации login (которая явля­ется SETUID root) запрашивает у пользователя его имя и пароль. Затем она хэширует пароль и ищет его в файле паролей /etc/passwd, чтобы определить, соответ­ствует ли хэш-код содержащимся в нем значениям. Хэширование применяется, чтобы избежать хранения паро­ля в незашифрованном виде где-либо в системе. Если пароль введен верно, про­грамма регистрации считывает из файла /etc/passwd имя программы оболочки, которую предпочитает пользователь. Ей может быть программа sh, но это также может быть и другая оболочка, например csh или ksh. Затем программа регистрации ис­пользует системные вызовы setuld и setgid, чтобы установить для себя UID и GID. После этого программа регистрации открывает клавиатуру для стандартного ввода (файл с дескрипто­ром 0) и экран для стандартного вывода (файл с дескриптором 1), а также экран для вывода стандартного потока сообщений об ошибках (файл с дескриптором 2). Наконец, она выполняет оболочку, которую указал пользователь, таким образом, завершая свою работу.
  С этого момента начинает работу оболочка с установленными UID и GID, а так­же стандартными потоками ввода, вывода и ошибок, настроенными на устройства ввода-вывода по умолчанию. Все процессы, которые она запускает при помощи системного вызова fork (то есть команды, вводимые пользователем с клавиатуры), автоматически наследуют UID и GID оболочки, поэтому у них будет верное зна­чение владельца и группы. Все файлы, создаваемые этими процессами, также будут иметь эти значения.
  Когда любой процесс пытается открыть файл, система сначала проверяет биты защиты в i-узле файла для заданных значений рабочих UID и GID, чтобы опреде­лить, разрешен ли доступ для данного процесса. Если доступ разрешен, файл от­крывается и процессу возвращается дескриптор файла. В противном случае файл не открывается, а процессу возвращается значение –1. При последующих обраще­ниях к системным вызовам read и write проверка не выполняется. В результате, если режим защиты файла изменяется уже после того, как файл открыт, новый режим не повлияет на процессы, которые уже успели открыть этот файл.
  В операционной системе Linux защита файлов и ресурсов осуществляется так же, как и в UNIX.
 
 Резюме
 
  Операционная система UNIX широко используется на вычислительных маши­нах различных классов от ноутбуков до суперкомпьютеров. В операционной системе UNIX есть три интерфейса: оболочка, библиотека языка С и сами системные вызовы. Оболочка позво­ляет пользователям вводить команды и исполнять их. Это могут быть простые команды, конвейеры или более сложные структуры. Ввод и вывод могут пере­направляться. В библиотеке С содержатся системные вызовы, а также множество расширенных вызовов. Каждый из системных вызовов выполняет определенные необходимые функции.
  К ключевым понятиям операционной системы UNIX относятся процесс, модель памяти, ввод-вывод и файловая система. Процессы могут создавать дочерние про­цессы, в результате чего формируются деревья процессов. Для управления про­цессами в UNIX используются две ключевые структуры данных: таблица процес­сов и структура пользователя. Таблица процессов постоянно находится в памяти, а структура пользователя может выгружаться на диск. При создании процесса дублируется запись в таблице процессов, а также образ памяти процесса. Для пла­нирования применяется алгоритм, основанный на приоритетах, отдающий пред­почтение интерактивным процессам.
  Модель памяти состоит из трех сегментов для каждого процесса: для текста (исполняемого кода), данных и стека. Изначально для управления памятью ис­пользовался свопинг, но в большинстве современных версий системы UNIX для этого применяется страничная подкачка. Состояние каждой страницы отслежи­вается в карте памяти, а страничный демон поддерживает достаточное количество свободных страниц при помощи алгоритма часов.
  Доступ к устройствам ввода-вывода осуществляется при помощи специальных файлов, у каждого из которых есть старший номер устройства и младший номер устройства. Для снижения числа обращений к диску в блочных устройствах вво­да-вывода применяется буферный кэш. Для управления кэшем используется ал­горитм LRU (Least-Recently-Used – «наиболее давнего использования»). Сим­вольный ввод-вывод может осуществляться в обработанном и необработанном режимах. Для дополнительных возможностей символьного ввода-вывода приме­няются дисциплины линии связи или потоки.
  Файловая система в UNIX – иерархическая, с файлами и каталогами. Все диски монтируются в единое дерево каталогов, начинающееся в одном корне. Отдельные файлы могут быть связаны с любым каталогом дерева. Чтобы пользоваться файлом, его нужно сначала открыть. При этом процессу, открывающему файл, возвращает­ся дескриптор файла, который затем используется при чтении этого файла и запи­си в файл. Внутри файловая система использует три основные таблицы: таблицу дескрипторов файлов, таблицу дескрипторов открытых файлов и таблицу i-узлов. Из этих таблиц таблица i-узлов является наиболее важной. В ней содержится ин­формация, необходимая для управления файлом и позволяющая найти его блоки.
  Защита файлов основывается на регулировании доступа для чтения, записи и исполнения, предоставляемого владельцу файла, членам его группы и всем осталь­ным пользователям. Для каталогов бит исполнения интерпретируется как разре­шение поиска в каталоге.
 
 
 Контрольные вопросы и задания
 
 1. Опишите интерфейсы ОС UNIX.
 2. Каковы особенности оболочки и утилит системы UNIX?
 3. Дайте определение программы, называемой фильтром.
 4. Как называются файлы, содержащие команды оболочки?
 5. В чем заключается идея стандартизации обслуживающих программ UNIX?
 6. Представьте состав нижнего уровня ядра UNIX.
 7. Какие функции выполняет уровень системы виртуальной памяти UNIX?
 8. Назовите главные функции уровня интерфейсов системы UNIX.
 9. Какие функции выполняют в UNIX фоновые процессы, называемые демо­нами?
 
 10. Опишите механизмы взаимодействия и синхронизации процессов в UNIX.
 11. Какие структуры данных, относящиеся к процес­сам, поддерживает ядро системы UNIX?
 12. Перечислите категории информации, хранящейся в таблице процессов.
 13. Какие данные составляют структуру пользователя?
 14. Опишите этапы создания процесса в системе UNIX.
 15. Охарактеризуйте методы планирования в ОС семейства UNIX .
 16. Из каких сегментов состоит адресное пространство в UNIX?
 17. В чем заключается свойство отображения файлов на адресное пространство памяти?
 18. Опишите способы реализации управления памятью в UNIX .
 19. Как работает механизм страничной подкачки в UNIX?
 20. Поясните структуру карты памяти и реализацию алгоритма замещения страниц.
 21. Опишите реализацию ввода-вывода в ОС UNIX .
 22. Дайте определение понятию «сокет» и перечислите наиболее распространенные типы сокетов.
 23. Охарактеризуйте решения, применяемые в UNIX для структурирования драйверов символьных устройств и придания им свойства модульности.
 24. Какие типы файлов поддерживаются в ОС UNIX?
 25. Как реализована классическая файловая система UNIX.
 26. В чем заключаются особенности реализация файловой системы Berkeley Fast?
 27. Представьте реализацию файловых систем Linux.
 28. Охарактеризуйте файловую систему NFS.
 29. Каким образом реализуется свойство автомонтировки файловых систем в UNIX?
 30. К каким проблемам приводит использование кэширования данных
 в файловой системе NFS?
 31. Опишите функционирование системы безопасности в UNIX.8. Пример практической реализации
 операционной системы: Windows 2000
 
 8.1. Обзор структуры операционной систем Windows 2000
 
 8.1.1. Структура системы
 
  Операционная система Windows 2000 состоит из двух основных частей: самой опе­рационной системы, работающей в режиме ядра, и подсистем окружения, работа­ющих в режиме пользователя. Ядро является традиционным ядром в том смысле, что оно управляет процессами, памятью, файловой системой и т. д. Подсистемы окружения являются отдельными процессами, помогающими пользователю выполнять определенные систем­ные функции.
  Одно из многих усовершенствований системы NT по сравнению с Windows 3.1 заключалось в ее модульной структуре. Она состояла из относительно небольшо­го ядра, работавшего в режиме ядра, плюс нескольких серверных процессов, рабо­тавших в режиме пользователя. Процессы пользователя взаимодействовали с сер­верными процессами с помощью модели клиент-сервер: клиент посылал серверу сообщение, а сервер выполнял определенную работу и возвращал клиенту резуль­тат в ответном сообщении. Такая модульная структура упрощала перенос систе­мы на другие компьютеры. В результате операционная система Windows NT была успешно перенесена на платформы с процессорами, отличными от процессоров Intel, а именно: Alpha корпорации DEC, Power PC корпорации IBM и MIPS фир­мы SGI. Кроме того, такая структура защищала ядро от ошибок в коде серверов. Однако для увеличения производительности, начиная с версии NT 4.0, довольно большая часть операционной системы (например, управление системными вызо­вами и вся экранная графика) были возвращены в ядро. Такая схема сохранилась и в Windows 2000.
  Тем не менее в операционной системе Windows 2000 сохранилась некоторая структура. Система разделена на несколько уровней, каждый из которых пользу­ется службами лежащего ниже уровня. Два нижних уровня программного обеспечения – уровень так называемых аппаратных абстрак­ций и ядро написаны на языке С и ассемблере и являются частично машинно-зависимыми. Верхние уровни написаны исключи­тельно на С и почти полностью машинно-независимы. Драйверы написаны на С или, в некоторых случаях, на C++.
  Рассмотрим подробнее различ­ные компоненты системы, начиная с самых нижних уровней и постепенно продви­гаясь наверх.
  Одна из целей создания Windows 2000 (и Windows NT) заключалась в возмож­ности переносить систему на другие платформы. В идеале при появлении новой машины для запуска операционной системы на ней нужно всего лишь переком­пилировать операционную систему новым компилятором для данной машины. К сожалению, в реальности сделать это не совсем просто. Хотя можно добиться полной переносимости верхних уровней операционной системы (так как в основном они имеют дело с внут­ренними структурами данных), нижние уровни работают с регистрами устройств, прерываниями, DMA и другими аппаратными особенностями, которые очень силь­но отличаются на разных машинах. Несмотря на то, что большая часть кода нижнего уровня напи­сана на С, даже ее нельзя просто перенести с процессора Pentium на процессор Alpha, перекомпилировать и перезагрузить, так как существует большое количе­ство мелких различий между этими процессорами, не имеющих отношения к раз­личиям в наборе команд, которые невозможно спрятать компилятором.
  Корпорация Microsoft, хорошо представляя себе эту проблему, предприняла серьез­ные попытки скрыть многие из аппаратных различий в тонком уровне на самом дне системы, названном уровнем аппаратных абстракций (HAL, Hardware Abstraction Layer). Работа уровня HAL заключается в том, чтобы предоставлять всей остальной системе абстрактные аппаратные устройства, свободные от индиви­дуальных отличительных особенностей, которыми так богато реальное аппарат­ное обеспечение. Эти устройства представляются в виде машинно-независимых служб (процедурных вызовов и макросов), которые могут использоваться осталь­ной операционной системой и драйверами. Поскольку драйверы и ядро пользуются службами HAL (идентичными на всех операционных системах Windows 2000, не­зависимо от аппаратного обеспечения) и не обращаются напрямую к устройствам, требуется значительно меньше изменений для их переноса на другую платформу. Перенос самого уровня HAL довольно прост, так как весь машинно-зависимый код сконцентрирован в одном месте, а цель переделки четко определена, то есть за­ключается в реализации всех служб уровня HAL.
  В уровень HAL включены те службы, которые зависят от набора микросхем материнской платы и меняются от машины к машине в разумных предсказуемых пределах. Другими словами, он разработан, чтобы скрывать различия между материнскими платами различных производителей, но не различия между процес­сорами Pentium и Alpha. К службам уровня HAL относятся: доступ к регистрам устройств, адресация к устройствам, независящим от шины, обработка прерыва­ний и возврат из прерываний, операции прямого доступа к памяти (DMA), управление таймерами, часами реального времени, спин-бло­кировками нижнего уровня и синхронизация многопроцессорных конфигура­ций, интерфейс с BIOS и доступ к CMOS-памяти. Уровень HAL не предоставляет абстракций или служб для специфических устройств ввода-вывода – клавиатур, мышей или дисков, а также блоков управления памятью.
  Драйверам часто бывает нужно получить доступ к специфическим устройствам ввода-вывода. На аппаратном уровне у драйвера есть один или несколько адресов определенной шины. Поскольку у современных компьютеров часто есть несколь­ко шин (PCI, SCSI, USB, IEEE1394 и т. д.), может случиться, что два или более устройств имеют один и тот же адрес шины, поэтому требуется некоторый способ отличать эти устройства. Уровень HAL предоставляет службу для идентифика­ции устройств, отображая адреса устройств на шине на логические системные адреса. Поэтому драйверам не нужно следить за тем, которое устройство находит­ся на какой шине. Такая логическая адресация аналогична дескрипторам, выдава­емым операционной системой программам пользователя для обращения к файлам и другим системным ресурсам. Этот механизм также защищает более высокие уровни от свойств структур шин и соглашений об адресации.
  С прерываниями связана схожая проблема – они также являются зависимыми от шины. Здесь уровень HAL предоставляет службы для именования прерываний уникальным в пределах всей системы способом, а также службы, позволяющие драйверам связывать процедуры обработки прерываний с прерываниями перено­симым способом. При этом не нужно знать, какой вектор к какой шине относится. Управление уровнем запроса прерывания также осуществляется на уровне HAL.
  Другая служба HAL занимается управлением операциями DMA независимым от устройств способом. HAL может управлять как единым для всей системы меха­низмом DMA, так и механизмами DMA, специфичными для конкретных плат вво­да-вывода. Обращение к устройствам осуществляется по их логическим адресам.
  Уровень HAL также реализует программные операции чтения/записи с разнесе­нием данных (с обращением к не являющимся соседними блокам памяти).
  Уровень HAL управляет часами и таймерами, обеспечивая переносимость работающих с ними программ. Время хранится в интервалах по 100 нc, что существенно точнее, чем то, как это делалось в MS-DOS в 2-секундных интервалах. Временные службы уровня HAL обеспечивают независимость драйверов от фактических частот, на которых работают часы.
  Иногда требуется синхронизация компонентов ядра на очень низком уровне, особенно для того, чтобы избежать конфликтов на многопроцессорных системах. Уровень HAL предоставляет несколько примитивов для управления этой синхро­низацией. Примером являются спин-блокировки, в которых один центральный процессор просто ждет, пока другой центральный процессор не освободит опреде­ленный ресурс. В частности, такой метод синхронизации применяется в ситуаци­ях, в которых доступ к ресурсу, как правило, получается всего на несколько ко­манд процессора.
  Наконец, после загрузки операционной системы уровень HAL общается с BIOS и инспектирует память конфигурации CMOS, если она используется, чтобы определить, какие шины и устройства ввода-вывода содержатся в системе и как их следует настроить. Затем эта информация помещается в реестр, чтобы другие компоненты системы могли просматривать их, не обращаясь напрямую к BIOS или CMOS-памяти.
  Поскольку уровень HAL является в большой степени машинно-зависимым, он должен в совершенстве соответствовать системе, на которой установлен, поэтому набор различных уровней HAL поставляется на компакт-диске Windows 2000. Во время установки системы из них выбирается подходящий уровень и копируется на жесткий диск в системный каталог \winnt\system32 в виде файла hal.dll. При всех последующих загрузках операционной системы используется эта версия уровня HAL. Если удалить этот файл, то система загрузиться не сможет.
  Хотя эффективность уровня HAL является довольно высокой, для мульти­медийных приложений ее может быть недостаточно. По этой причине корпора­ция Microsoft также производит пакет программного обеспечения, называемый DirectX, расширяющий функциональность уровня HAL дополнительными про­цедурами и предоставляющий пользовательским процессам прямой доступ к ап­паратному обеспечению.
  Над уровнем аппаратных абстракций располагается уровень, содержащий то, что корпорация Microsoft называет ядром, а также драйверы устройств. Начиная с NT 4.0, практически вся операционная система была помещена в пространство ядра. При описании операционной системе UNIX термин «ядро» используется для обозначения всего, что работает в режиме ядра. В данном разделе все программное обеспечение, работающее в режиме ядра, будем называть «операционной системой».
  Часть ядра (и большая часть уровня HAL) постоянно находится в оперативной памяти (то есть не выгру­жается). При помощи установки соответствующего приоритета эта часть ядра может решать, допустимо ли прерывание от устройств ввода-вывода или нет. Хотя значительная часть ядра представляет собой машинно-зависимую програм­му, тем не менее большая ее часть написана на С, кроме тех мест, в которых произ­водительность считается важнее всех остальных задач.
  Назначение ядра заключается в том, чтобы сделать всю остальную часть опера­ционной системы независимой от аппаратуры и, таким образом, легко переноси­мой на другие платформы. Оно начинается там, где заканчивается уровень HAL. Ядро получает доступ к аппаратуре через уровень HAL. Оно построено на чрезвы­чайно низкоуровневых службах уровня HAL, формируя из них абстракции более высоких уровней. Например, у уровня HAL есть вызовы для связывания процедур обработки прерываний с прерываниями и установки их приоритетов, но больше практически ничего уровень HAL в этой области не делает. Ядро, напротив, предо­ставляет полный механизм для переключения контекста. Оно должным образом сохраняет все регистры центрального процессора, изменяет таблицы страниц, со­храняет кэш центрального процессора и т. д. Когда все эти действия выполнены, работавший ранее поток оказывается полностью сохраненным в таблицах, распо­ложенных в памяти. Затем ядро настраивает карту памяти нового потока и загру­жает его регистры, после чего новый поток готов к работе.
  Программа планирования потоков также располагается в ядре. Когда насту­пает пора проверить, не готов ли к работе новый поток, например, после того, как истечет выделенный потоку квант времени или по завершении процедуры обработ­ки прерываний ввода-вывода, ядро выбирает поток и выполняет переключение контекста, необходимое, чтобы запустить этот поток. С точки зрения остальной операционной системы переключение потоков автоматически осуществляется более низкими уровнями, так что для более высоких уровней не остается никакой работы. Сам алгоритм планирования будет обсуждаться далее в разделе, посвященном процессам и потокам.
  Помимо предоставления абстрактной модели аппаратуры более высоким уровням и управления переключениями потоков, ядро также выполняет еще одну ключевую функцию: предоставляет низко-уровневую поддержку двум классам объектов – управляющим объектам и объектам диспетчеризации. Эти объекты не являются объектами, к которым пользовательские процессы получают дескрип­торы, но представляют собой внутренние объекты, на основе которых исполняю­щая система строит объекты пользователя.
  Управляющие объекты – это объекты, управляющие системой, включая при­митивные объекты процессов, объекты прерываний и два объекта, называемых DPC и АРС. Объект DPC (Deferred Procedure Call – отло­женный вызов процедуры) используется, чтобы отделить часть процедуры обра­ботки прерываний, для которой время является критичным, от той ее части, для которой время некритично. Как правило, процедура обработки прерываний сохра­няет несколько аппаратных регистров, связанных с прерывающим устройством ввода-вывода, чтобы их можно было потом восстановить, и разрешает аппаратуре продолжать работу, но оставляет большую часть обработки на потом. Например, когда пользователь нажимает на клавишу, процедура обработки прерываний от клавиатуры считывает из регистра код нажатой клавиши и разреша­ет прерывания от клавиатуры. Но эта процедура не должна немедленно обрабаты­вать введенный символ, особенно если в данный момент происходит нечто более важное (то есть нечто с более высоким приоритетом). Пока обработка клавиши за­нимает не более 100 мс, пользователь ничего не заметит. Отложенные вызовы про­цедуры также применяются для слежения за таймерами и другой активностью, для которой не требуется немедленная обработка. Очередь DPC представляет собой механизм напоминания о том, что есть работа, которую следует выполнить позднее.
  Объект АРС (Asynchronous Procedure Call – асинхронный вызов процедуры) похож на отложенный вызов процедуры DPC, но отличается тем, что асинхрон­ный вызов процедуры выполняется в контексте определенного процесса. Когда обрабатывается нажатая клавиша, не имеет значения, в каком контексте работает DPC, так как все, что требуется сделать, – это исследовать введенный код и, воз­можно, поместить его в буфер в ядре. Однако если по прерыванию потребуется скопировать буфер из пространства ядра в адресное пространство пользовательс­кого процесса (например, по завершении операции чтения модема), тогда проце­дура копирования должна работать в контексте получателя. Контекст получателя нужен для того, чтобы в таблице страниц одновременно содержались и буфер ядра, и буфер пользователя. По этой причине в разных ситуаци­ях используются АРС или DPC.
  Еще один тип объектов ядра – объекты диспетчеризации. К ним относятся семафоры, мьютексы, события, таймеры и другие объекты, изменения состояния которых могут ждать потоки. Причина, по которой они должны обра­батываться ядром, заключается в том, что они тесно переплетены с планированием потоков, что входит в круг задач ядра.
  Над ядром и драйверами устройств располагается верхняя часть операционной системы, называемая исполняющей системой (а также иногда супервизором или диспетчером). Исполняющая система написана на С, она не зависит от архитектуры и может быть перенесена на новые машины с относительно небольшими усилиями. Исполняющая система состоит из 10 компонентов, каждый из которых представляет собой просто набор процедур, работающих вместе для выполнения некоторой задачи. Между отдель­ными компонентами нет жестких границ, и различные авторы, описывающие ис­полняющую систему, могут даже по-разному группировать составляющие ее про­цедуры в компоненты. Следует заметить, что компоненты одного уровня могут вызывать друг друга, и на практике они этим довольно активно пользуются.
  Менеджер объектов управляет всеми объектами, известными операционной системе. К ним относятся процессы, потоки, файлы, каталоги, семафоры, устрой­ства ввода-вывода, таймеры и многое другое. При создании объекта менеджер объектов получает в адресном пространстве ядра блок виртуальной памяти и воз­вращает этот блок в список свободных блоков, когда объект уничтожается. Его работа заключается в том, чтобы следить за всеми объектами. Отметим, что большинство компонентов исполня­ющей системы не являются процесса­ми или потоками, а представляют собой просто набор процедур, которые могут выполняться другими потоками в режиме ядра. Однако некоторые из них, такие как менеджер питания и менеджер plug-and-play, являются настоящими потоками. Менеджер объектов также управляет пространством имен, в которое помеща­ется созданный объект, чтобы впоследствии к нему можно было обратиться по имени. Все остальные компоненты исполняющей системы активно пользуются объектами во время своей работы. Объекты занимают центральное место в функ­ционировании операционной системы Windows 2000.
  Менеджер ввода-вывода формирует каркас для управления устройствами ввода-вывода и предоставляет общие службы ввода-вывода. Он предоставляет остальной части системы независимый от устройств ввод-вывод, вызывая для выполнения физического ввода-вывода соответствующий драйвер. Здесь также располагаются все драйверы устройств. Файловые системы формально являются драйверами устройств под управлением менеджера ввода-вывода. Существует два драйвера для файловых систем FAT и NTFS, независимые друг от друга и управляющие различными разделами диска. Все файловые системы FAT управляются одним драйвером. (Ввод-вывод будет рассмотрен далее в подразделе «Ввод-вывод в Windows 2000», а файловая система NTFS – в подразделе «Файловая система Windows 2000»).
  Менеджер процессов управляет процессами и потоками, включая их созда­ние и завершение. Он занимается не стратегиями, применяемыми по отношению к процессам, а механизмом, используемым для управления ими. Менеджер про­цессов основывается на объектах потоков и процессов ядра и добавляет к ним до­полнительные функции. Это ключевой элемент многозадачности в Windows 2000. Управление процессами будет рассматриваться далее в подразделе «Процессы и потоки в Windows 2000».
  Менеджер памяти реализует архитектуру виртуальной памяти со страничной подкачкой по требованию операционной системы Windows 2000. Он управляет преобразованием виртуальных страниц в физические страничные блоки. Таким образом, он реализует правила защиты, ограничивающие доступ каждого про­цесса только теми страницами, которые принадлежат его адресному пространству, а не адресным пространствам других процессов (кроме специальных случаев). Он также контролирует определенные системные вызовы, относящиеся к вирту­альной памяти. Управление памятью будет рассматриваться в подразделе «Управле­ние памятью».
  Менеджер безопасности приводит в исполнение сложный механизм без­опасности Windows 2000, удовлетворяющий требованиям класса С2 Оранжевой книги Министерства обороны США. В Оранжевой книге перечислено множество правил, которые должна соблюдать система, начиная с аутентификации при реги­страции и заканчивая управлением доступом, а также обнулением страниц перед их повторным использованием. Менеджер безопасности будет обсуждаться в подраз­деле «Безопасность в Windows 2000».
  Менеджер кэша хранит в памяти блоки диска, которые использовались в по­следнее время, чтобы ускорить доступ к ним в случае, если они понадобятся вновь. Его работа состоит в том, чтобы определить, какие блоки понадобятся снова, а ка­кие нет. Операционная система Windows 2000 может одновременно использовать несколько файловых систем. В этом случае менеджер кэша обслуживает все фай­ловые системы, таким образом, каждой файловой системе не нужно заниматься управлением собственного кэша. Когда требуется блок, он запрашивается у менед­жера кэша. Если у менеджера кэша нет блока, он обращается за блоком к соответ­ствующей файловой системе. Поскольку файлы могут отображаться в адресное пространство процессов, менеджер кэша должен взаимодействовать с менеджером виртуальной памяти, чтобы обеспечить требуемую непротиворечивость. Количе­ство памяти, выделенной для кэша, динамически изменяется и может увеличивать­ся или уменьшаться при необходимости. Менеджер кэша будет описан в подразделе «Кэширование в Windows 2000».
  Менеджер plug-and-play получает все уведомления об установленных новых устройствах. Для некоторых устройств проверка производится при загрузке сис­темы, но не после нее. Другие устройства, например устройства USB (Universal Serial Bus – универсальная последовательная шина), могут подключаться в лю­бое время, и их подключение запускает пересылку сообщения менеджеру plug-and-play, который затем находит и загружает соответствующий драйвер.
  Менеджер энергопотребления управляет потреблением электроэнергии. Он выключает монитор и диски, если к ним не было обращений в течение определен­ного интервала времени. На переносных компьютерах менеджер энергопотребле­ния следит за состоянием батарей и, когда заряд батарей подходит к концу, пред­принимает соответствующие действия. Эти действия, как правило, заключаются в том, что он сообщает работающим программам о состоянии батарей. В результа­те программы могут сохранить свои файлы и приготовиться к корректному завер­шению работы.
  Менеджер конфигурации отвечает за состояние реестра. Он добавляет новые записи и ищет запрашиваемые ключи.
  Менеджер вызова локальной процедуры обеспечивает высокоэффективное взаимодействие между процессами и их подсистемами. Поскольку этот путь ну­жен для выполнения некоторых системных вызовов, эффективность оказывается критичной, вот почему для этого не используются стандартные механизмы меж­процессного взаимодействия.
  Исполняющий модуль Win32 GDI обрабатывает определенные системные вызовы (но не все). Изначально он располагался в пространстве пользователя, но в версии NT 4.0 для увеличения производительности был перенесен в простран­ство ядра. Интерфейс графических устройств GDI (Graphic Device Interface) за­нимается управлением графическими изображениями для монитора и принтеров. Он предоставляет системные вызовы, позволяющие пользовательским програм­мам выводить данные на монитор и принтеры независящим от устройств спосо­бом. Он также содержит оконный менеджер и драйвер дисплея. До версии NT 4.0 интерфейс графических устройств также находился в пространстве пользователя, но производительность при этом оставляла желать лучшего, поэтому корпорация Microsoft переместила его в ядро.
  Над исполняющей системой размещается уровень, называемый систем­ными службами. Его функции заключаются в предоставлении интерфейса к ис­полняющей системе. Он принимает настоящие системные вызовы Windows 2000 и вызывает другие части исполняющей системы для их выполнения.
  При загрузке операционная система Windows 2000 загружается в память как набор файлов. Основная часть операционной системы, состоящая из ядра и ис­полняющей системы, хранится в файле ntoskrnl.exe. Уровень HAL представляет собой библиотеку общего доступа, расположенную в отдельном файле hal.dll. Интерфейс Win32 и интерфейс графических устройств хранятся вместе в тре­тьем файле – win32k.sys. Кроме этого, загружается множество драйверов устройств, у большинства которых расширение sys. Существует две версии файла ntoskrnl.exe: для однопроцессорных и многопроцессорных систем. Также существуют версии для процессора Хеоn, способного поддерживать более 4 Гбайт физичес­кой памяти, и для процессора Pentium, который так много оперативной памяти поддержать не может. Наконец, этот модуль может содержать или не содержать отладочные функции, предназначенные для отлад­ки системы.
  Каждый из драйверов устройств могут управлять одним или несколькими устройствами ввода-вывода, но драйвер устройства может также выполнять действия, не относящиеся к какому-либо специфическому устройству – шифровать поток данных или даже просто предоставлять доступ к структурам данных ядра. Драйверы устройств не являются частью двоичного файла ntoskrnl.exe. Преимущество такого подхода заключается в том, что как только драйвер устанавливается в систему, он добавляется в реестр и затем динамически загружается при каждой загрузке системы. Таким образом, файл ntoskrnl.exe остается одинаковым для всех конфигураций систем, но каждая система точно настраивается на конфигурацию аппаратуры.
  Существуют драйверы для реально видимых и осязаемых устройств ввода-вы­вода, таких как диски и принтеры, но также есть драйверы для многих внутренних устройств и микросхем. Кроме того, как уже было сказано, файловые системы также представлены в виде драй­веров устройств. Самым большим является драйвер устройства для интерфейса Win32 и GDI. Он обрабатывает множество системных вызовов и управляет большей частью графики.
 8.1.2. Реализация объектов
 
  Объекты представляют собой, вероятно, самое важное понятие операционной си­стемы Windows 2000. Они предоставляют однородный и непротиворечивый ин­терфейс ко всем системным ресурсам и структурам данных, таким как процессы, потоки, семафоры и т. д. У этой однородности есть много граней. Во-первых, все объекты именуются по одной и той же схеме. Доступ ко всем объектам также пре­доставляется одинаково, при помощи дескрипторов объектов. Во-вторых, посколь­ку доступ к объектам всегда осуществляется через менеджер объектов, все провер­ки, связанные с защитой, могут быть размещены в одном месте, с гарантией, что ни один процесс не сможет обойти их. В-третьих, возможно совместное исполь­зование объектов по одной и той же схеме. В-четвертых, поскольку все объекты открываются и закрываются через менеджер объектов, несложно отследить, ка­кие объекты все еще используются, а какие можно безопасно удалить. В-пятых, эта однородная модель для управления объектов позволяет легко регулировать квоты ресурсов.
 
  Ключом к пониманию объектов является тот факт, что исполняемый объект представляет собой просто набор последовательных слов в памяти (то есть в вир­туальном адресном пространстве ядра). Объект представляет собой структуру дан­ных в памяти. Файл на диске не является объектом, хотя для файла при его открытии создается объект (то есть структура данных в вирту­альном адресном пространстве ядра). Из того факта, что объекты представляют собой всего лишь структуры данных в виртуальном адресном пространстве ядра, следует, что при перезагрузке (или сбое) системы все объекты теряются. Дей­ствительно, когда операционная система загружается, нет никаких объектов (кро­ме бездействующих системных процессов, чьи объекты жестко прошиты в файле ntoskrnl.exe). Все остальные объекты создаются на ходу при загрузке системы и во время работы различных программ инициализации, а позднее пользовательских программ.
  Каждый объект содержит заголовок с определенной информацией, общей для всех объектов всех типов. Поля заголов­ка включают имя объекта, каталог, в котором объект живет в пространстве объек­тов, информацию защиты (при открытии объекта выполняется определенная про­верка), а также список процессов, у которых есть открытые дескрипторы к данному объекту (если установлен определенный флаг отладки).
  Каждый заголовок объекта также содержит поле цены квоты, представляющей собой плату, взимаемую с процесса за открытие объекта. Если файловый объект стоит один пункт, а процесс принадлежит к заданию, у которого есть 10 пунктов квоты, то суммарно все процессы этого задания могут открыть не более 10 фай­лов. Таким образом, для объектов каждого типа могут реализовываться ограниче­ния на ресурсы.
  Объекты занимают важный ресурс – участки виртуального адресного про­странства ядра – поэтому, когда объект более не нужен, он должен быть удален, а его адресное пространство возвращено системе. Для этого в заголовке каждого объекта содержится счетчик ссылок на объект. Этот счетчик увеличивается на единицу каждый раз, когда объект открывается, и уменьшается на единицу при закрытии объекта. Когда значение счетчика уменьшается до 0, это означает, что никто более не пользуется этим объектом. Когда объект открывается или освобож­дается компонентом исполняющей системы, используется второй счетчик, даже если настоящий дескриптор при этом не создается. Когда оба счетчика умень­шаются до 0, это означает, что этот объект более не используется ни одним пользо­вателем и ни одним исполняющим процессом, то есть объект может быть удален, а его память освобождена.
  Менеджеру объектов бывает необходимо получать доступ к динамическим структурам данных (объектам), но он не единственная часть исполняющей систе­мы, которой это нужно. Другим частям исполняющей системы также бывает нуж­но динамически получать на время участки памяти. Для этого исполняющая сис­тема содержит два пула в адресном пространстве ядра: для объектов и для других динамических структур данных. Один пул является выгружаемым, а другой невыгружаемым (фиксированным в памяти). Объекты, обращения к которым часты, хранятся в невыгружаемом пуле. Объекты, обращения к которым редки, например ключи реестра и некоторая информация, относящаяся к безопасности, хранятся в выгружаемом пуле. Когда памяти не хва­тает, этот пул может быть выгружен на диск и загружен обратно по страничному прерыванию. В действительности значительная часть программ и структур дан­ных операционной системы также является выгружаемой, что позволяет снизить потребление памяти. Объекты, которые могут понадобиться, когда система выпол­няет критический участок программы (и когда подкачка не разрешается), должны храниться в невыгружаемом пуле. Когда требуется небольшое количество памяти, страница может быть получена из любого пула, а затем разбита на мелкие участки размером от 8 байт.
  Объекты подразделяются на типы. Это означает, что у каждого объекта есть свойства, общие для всех объектов этого типа. Тип объекта определяется указате­лем на объект типа. Информация о типе объекта включает такие пункты, как название типа, данные о том, может ли поток ждать изменения состояния этого объекта («да» для мьютексов, «нет» для открытых файлов), и должен ли объект этого типа храниться в выгружаемом или невыгружаемом пуле. Каждый объект указывает на свой объект типа.
  Наконец, самая важная часть объекта – это указатели на программы для опре­деленных стандартных операций, таких как open, close и delete. Когда вызывается одна из этих операций, используется указатель на типовой объект, в котором выби­рается и выполняется соответствующая процедура. Такой механизм предоставля­ет системе возможность инициализировать новые объекты и освобождать память при их удалении.
  Компоненты исполняющей системы могут динамически создавать новые типы. Фиксированного списка типов объектов не существует, но некоторые наиболее употребительные типы рассмотрим ниже. Давайте кратко рассмотрим эти типы объектов. Существует один объект для каждого процесса и для каждого потока. В объекте хранятся основные свойства, необходимые для управления этим процессом или потоком. Следующие три объек­та – семафор, мьютекс и событие – имеют отношение к синхронизации процессов. Семафоры и мьютексы работают так, как и ожидается, но с дополнительными функ­циями (например, максимальными значениями и тайм-аутами). События могут быть в одном из двух состояний: сигнализирующем и несигнализирующем. Если поток ждет события, находящегося в сигнализирующем состоянии, он немедлен­но получает управление. Если же ожидаемое потоком событие находится в несиг­нализирующем состоянии, тогда поток блокируется до тех пор, пока какой-либо другой поток не переведет это событие в сигнализирующее состояние (проще гово­ря, пока это событие не произойдет). Событие может также быть настроено таким образом, что после получения сигнала ожидающим его процессом это событие автоматически перейдет в несигнализирующее состояние. В противном случае событие останется в сигнализирующем состоянии.
  Объекты порт, таймер и очередь также имеют отношение к связи и синхрони­зации. Порты представляют собой каналы между процессами, использующиеся для обмена сообщениями. Таймеры предоставляют способ блокировать процесс или поток на определенный срок. Очереди применяются для уведомления пото­ков о том, что начатая ранее асинхронная операция ввода-вывода завершена.
  Объекты открытых файлов создаются при открытии файла. У неоткрытых фай­лов нет объектов, управляемых менеджером объектов. Маркеры доступа представ­ляют собой объекты безопасности. Они идентифицируют пользователя и сообща­ют, какие привилегии имеет этот пользователь. Профили представляют собой структуры, используемые для хранения периодически фиксируемых значений счетчика команд работающего потока, которые позволяют определить, на что дан­ная программа тратит свое время.
  Секции являются объектами, используемыми системой памяти для управле­ния отображаемыми на память файлами. Они хранят сведения о том, какой файл (или часть файла) на какие адреса памяти отображается. Ключи представляют со­бой ключи реестра и применяются для установки связи между именем и значени­ем.
  Каталоги объектов являются полностью локальными по отношению к менед­жеру объектов. Они предоставляют способ объединять связанные объекты тем же способом, каким обычные каталоги объединяют файлы в файловой системе. Символьные ссылки также подобны своим двойникам в файловой системе: они позволяют имени в одной части пространства имен объектов ссылаться на объект в другой части этого пространства имен. У каждого известного системе устрой­ства есть объект устройства, содержащий информацию о нем и использующийся для ссылки на устройство в системе. Наконец, у каждого загруженного драйвера устройства есть объект в пространстве объектов.
  Пользователи могут создавать новые объекты или открывать уже существующие объекты при помощи вызовов Win32, таких как CreateSemaphore и OpenSemaphore. Эти вызовы являются библиотечными процедурами, которые в конечном итоге об­ращаются к настоящим системным вызовам. При успешном выполнении первый вы­зов создает, а второй открывает объект, создавая в результате 64-разрядную запись в таблице дескрипторов, хранящуюся в приватной таблице дескрипторов процес­са в памяти ядра. Пользователю для последующей работы возвращается 32-раз­рядный индекс, указывающий положение дескриптора в таблице.
  64-разрядный элемент таблицы дескрипторов в ядре содержит два 32-разряд­ных слова. Одно слово содержит 29-разрядный указатель на заголовок объекта. Младшие три разряда используются как флаги (например, указывающие, насле­дуется ли дескриптор дочерним процессом). Когда указатель используется, эти разряды маскируются. Второе слово содержит 32-разрядную маску прав доступа. Она нужна, потому что проверка разрешений выполняется только в то время, когда объект создается или открывается. Если у процесса есть только разрешение для чтения объекта, тогда все остальные биты маски будут нулями, что дает системе возможность отвергать любую операцию, кроме операции чтения.
  По мере того как во время выполнения программы создаются и удаляются объек­ты, менеджеру объектов необходимо следить за ними. Для выполнения этой работы он поддерживает пространство имен, в котором располагаются все объекты систе­мы. Пространство имен может использоваться процессом, чтобы найти и открыть дескриптор объекта другого процесса при условии, что для этого у него есть необхо­димые разрешения. Пространство имен объектов является одним из трех пространств имен, поддерживаемых в Windows 2000. Остальные два представляют собой про­странство имен файловой системы и пространство имен реестра. Все три являются иерархическими пространствами имен со множеством уровней каталогов для орга­низации элементов. Объекты каталогов предоставляют средства реализации этого иерархического пространства имен для объектов.
  Поскольку объекты исполняющей системы являются временными (то есть ис­чезают при выключении компьютера, в отличие от файловой системы и элемен­тов реестра), в начале загрузки системы в памяти нет объектов и пространство имен объектов пусто. Во время загрузки различные части исполняющей системы создают каталоги и заполняют их объектами. Например, когда менеджер plug-and-play обнаруживает новые устройства, он создает по объекту для каждого устройства и помещает эти объекты в пространство имен. Когда система полностью загружена, все устройства ввода-вывода, дисковые разделы и другие открытия системы оказываются в пространстве имен объектов.
  Необходимо отметить, что некоторые компоненты исполняющей системы перед созданием объектов обращаются к реестру, чтобы определить свои дальнейшие действия. Важнейший пример – драйверы устройств. При загрузке система смотрит в реестр, чтобы узнать, какие драйверы ей нужны. При загрузке каждого драйвера создается объект, а его имя добавляется в про­странство имен объектов. В системе обращение к драйверу осуществляется по ука­зателю на его объект.
 8.1.3. Подсистемы окружения
  Итак, операционная система Windows 2000 состоит из компонентов, работающих в режиме ядра, и компонентов, работающих в режиме пользователя. Выше были рассмотрены компоненты, работающие в режиме ядра. Теперь перейдем к рассмотрению компонентов, работающих в ре­жиме пользователя.
  Существует три типа таких компонентов: динамические биб­лиотеки DLL (Dynamic Link Library – динамически подключаемая библиотека), подсистемы окружения и служебные процессы. Эти компоненты работают вместе, предоставляя каждому пользовательскому процессу интерфейс, отличный от интерфейса системных вызовов Windows 2000.
  Операционной системой Windows 2000 поддерживаются три различных доку­ментированных интерфейса прикладного программирования API: Win32, POSIX и OS/2. У каждого из этих интерфейсов есть список библиотечных вызовов, ко­торые могут использовать программисты. Работа библиотек DLL и подсистем окружения за­ключается в том, чтобы реализовать функциональные возможности опубликован­ного интерфейса, тем самым скрывая истинный интерфейс системных вызовов от прикладных программ. В частности, интерфейс Win32 является официальным интерфейсом для операционных систем Windows 2000, Windows NT, Windows 95/ 98/Me. При использовании библиотеки DLL и подсистемы окружения Win32 программа может быть написана в соот­ветствии со спецификацией Win32, в результате чего она сможет без каких-либо изменений работать на всех этих версиях Windows, несмотря на то, что сами сис­темные вызовы в различных системах различны.
  Рассмотрим способ реализации этих интерфейсов на примере Win32. Програм­ма, пользующаяся интерфейсом Win32, как правило, состоит из большого коли­чества обращений к функциям Win32 API, например CreateWindow, DrawMenuBar и OpenSemaphore. Существуют тысячи подобных вызовов, и большинство программ использует значительное их количество. Один из возможных способов реализа­ции заключается в статическом связывании каждой программы, использующей интерфейс Win32, со всеми библио-течными процедурами, которыми она пользу­ется. При таком подходе каждая двоичная программа будет содержать копию всех используемых ею процедур в своем исполняемом двоичном файле. Недостаток такого подхода заключается в том, что при этом расходуется много памяти, если пользователь одновременно откроет несколько программ, использу­ющих одни и те же библиотечные процедуры. Например, программы Word, Excel и Powerpoint используют абсолютно одинаковые процедуры для открытия диалого­вых окон, рисования окон, отображения меню, работы с буфером обмена и т. д. Поэтому, если одновременно открыть все эти программы, при такой реализации программ в памяти будут находиться три идентичные копии каждой библиотеч­ной процедуры. Чтобы избежать подобной проблемы, все версии Windows поддерживают ди­намические библиотеки DLL. Каждая динамическая библиотека содержит набор тесно связанных библиотечных процедур и все их структуры данных в одном фай­ле, как правило (но не всегда), с расширением dll. Когда приложение компонуется, компоновщик видит, что некоторые библиотечные процедуры принадлежат к ди­намическим библиотекам, и записывает эту информацию в заголовок исполняе­мого файла. Обращения к процедурам динамических библиотек производятся не напрямую, а при помощи вектора передачи в адресном пространстве вызывающе­го процесса. Изначально этот вектор заполнен нулями, так как адреса вызываемых процедур еще неизвестны.
  При запуске прикладного процесса все требуемые динамические библиотеки обнаруживаются (на диске или в памяти) и отображаются на виртуальное адрес­ное пространство процесса. Затем вектор передачи заполняется верными адреса­ми, что позволяет вызывать библиотечные процедуры через этот вектор с незна­чительной потерей производительности. Выигрыш такой схемы заключается в том, что при запуске нескольких приложений, использующих одну и ту же динамичес­кую библиотеку, в физической памяти требуется только одна копия текста DLL (но каждый процесс получает свою собственную копию приватных статических данных в DLL). В операционной системе Windows 2000 динамические библиоте­ки используются очень активно для всех аспектов системы.
  Каждый пользовательский процесс, как правило, связан с несколькими динамическими библиотеками, совместно реа­лизующими интерфейс Win32. Чтобы обратиться к вызову API, вызывается одна из процедур в DLL. Дальнейшие действия зависят от вызова Win32 API. Различные вызовы реализованы по-разному. В некоторых случаях динамические библиотеки обращаются к другой динами­ческой библиотеке, которая, в свою очередь, обращается к ядру операци­онной системы. Динамическая биб­лиотека может также выполнить всю работу самостоятельно, совсем не обращаясь к системным вызовам. Для других вызовов Win32 API выбирается другой маршрут, а именно: сначала процессу подсистемы Win32 посылается сообщение, выполняющее некоторую работу и обращающееся к системному вызову. При этом в некоторых случаях подсистема также выполняет всю работу в пространстве пользователя и немедленно возвращает управление.
  Следует также отметить, что DLL не являются единственными динамическими библиотеками в системе. В каталоге \winnt\system32 есть более 800 отдельных файлов DLL общим объемом в 130 Мбайт. Количество содержащихся в них вызовов API превышает 13 000.
  Хотя интерфейс процессов Win32 является наиболее важным, в операционной системе Windows 2000 существует еще два интерфейса: POSIX и OS/2. Среда POSIX предоставляет минимальную поддержку для приложений UNIX. Этим интерфейсом, например, не поддерживаются потоки, работа с окнами или сетью. Перенос любой реальной программы из системы UNIX в Windows 2000 при помо­щи этой подсистемы практически невозможен. Эта подсистема не является самодостаточной и пользуется вызовами подсистемы Win32 для большей части своей работы, но не предостав­ляя пользовательским программам полного интерфейса Win32. Функциональность подсистемы OS/2 ограничена практически в той же степе­ни, что и функциональность подсистемы POSIX.
 
 8.2. Процессы и потоки в Windows 2000
 
 8.2.1. Основные понятия
 
  В операционной системе Windows 2000 поддерживаются традиционные процес­сы, способные общаться и синхронизироваться друг с другом так же, как это дела­ют процессы в UNIX. Каждый процесс содержит по крайней мере один поток, со­держащий, в свою очередь, как минимум одно волокно (облегченный поток). Более того, для управления определенными ресурсами процессы могут объединяться в задания. Все вместе – задания, процессы, потоки и волокна – образует общий набор инструментов для управления ресурсами и реализации параллелизма как на однопроцессорных, так и на многопроцессорных машинах.
  Задание в Windows 2000 представляет собой набор, состоящий из одного или нескольких процессов, управляемых как единое целое. В частности, с каждым заданием ассоциированы квоты и лимиты ресурсов, хранящиеся в соот­ветствующем объекте задания. Квоты включают такие пункты, как максимальное количество процессов (не позволяющее процессам задания создавать бесконтроль­ное количество дочерних процессов), суммарное время центрального процессора, доступное для каждого процесса в отдельности и для всех процессов вместе, а также максимальное количество используемой памяти для процесса и для всего задания. Задания также могут ограничивать свои процессы в вопросах безопасности, на­пример, запрещать им получать права администратора (суперпользователя) даже при наличии правильного пароля.
  Как и в системе UNIX, процессы представляют собой контейнеры для ресур­сов. У каждого процесса есть 4-гигабайтное адресное пространство, в котором пользователь занимает нижние 2 Гбайт (в версиях Windows 2000 Advanced Server и Datacenter Server этот размер может быть по желанию увеличен до 3 Гбайт), а операционная система занимает остальную его часть. Таким образом, операци­онная система присутствует в адресном пространстве каждого процесса, хотя она и защищена от изменений с помощью аппаратного блока управления памятью MMU. У процесса есть идентификатор процесса, один или несколько потоков, список дескрипторов (управляемых в режиме ядра) и маркер доступа, хранящий информацию защиты. Процессы создаются с помощью вызова Win32, который принимает на входе имя исполняемого файла, определяющего начальное содер­жимое адресного пространства, и создает первый поток.
  Каждый процесс начинается с одного потока, но новые потоки могут создавать­ся динамически. Потоки формируют основу планирования центрального процес­сора, так как операционная система всегда для запуска выбирает поток, а не про­цесс. Соответственно, у каждого потока есть состояние (готовый, работающий, блокированный и т. д.), тогда как у процессов состояний нет. Потоки могут дина­мически создаваться вызовом Win32, которому в адресном пространстве процесса задается адрес начала исполнения. У каждого потока есть идентификатор потока, выбираемый из того же пространства, что и идентификаторы процессов, поэтому один и тот же идентификатор никогда не будет использован одновременно для процесса и для потока. Идентификаторы процессов и потоков кратны четырем, поэтому они могут использоваться в роли байтовых индексов в таблицах ядра, как и другие объекты.
  Как правило, поток работает в пользовательском режиме, но когда он обраща­ется к системному вызову, то переключается в режим ядра, после чего продолжает выполнять тот же поток, с теми же свойствами и ограничениями, которые были у него в режиме пользователя. У каждого потока есть два стека – один используется в режиме ядра, а другой в режиме пользователя. Помимо состояния, идентифика­тора и двух стеков, у каждого потока есть контекст (в котором сохраняются его регистры, когда он не работает), приватная область для локальных переменных, а также может быть свой собственный маркер доступа. Если у потока есть свой мар­кер доступа, то он перекрывает маркер доступа процесса, чтобы клиентские потоки могли передать свои права доступа серверным потокам, выполняющим работу для них. Когда поток завершает свою работу, он может прекратить свое существование. Когда прекращает существование последний активный поток, процесс завершается.
  Важно понимать, что потоки представляют собой концепцию планирования, а не концепцию владения ресурсами. Любой поток может получить доступ ко всем объектам его процесса. Все, что ему для этого нужно сделать, – это заполучить дес­криптор и обратиться к соответствующему вызову Win32. Для потока нет ника­ких ограничений доступа к объекту, связанных с тем, что этот объект создан или открыт другим потоком. Система даже не следит за тем, какой объект каким потоком создан. Как только дескриптор объекта помещен в таблицу дескрипторов про­цесса, любой поток процесса может его использовать.
  Помимо нормальных потоков, работающих в процессах пользователя, в опера­ционной системе Windows 2000 есть множество процессов-демонов, не связанных ни с каким пользовательским процессом (они ассоциированы со специальной си­стемой или простаивающими процессами). Некоторые демоны выполняют адми­нистративные задачи, как, например, запись «грязных» (модифицированных) страниц на диск, тогда как другие формируют пул, и ими могут пользоваться компоненты исполняющей системы или драйверы, которым нужно выполнить какие-либо асинхронные за­дачи в фоновом режиме. Переключение потоков в операционной системе Windows 2000 занимает до­вольно много времени, так как для этого необходимо переключение в режим ядра, а затем возврат в режим пользователя. Для предоставления сильно облегченного псевдопараллелизма в Windows 2000 используются волокна, подобные потокам, но планируемые в пространстве пользователя создавшей их программой (или ее системой поддержки исполнения). У каждого потока может быть несколько воло­кон, так же как у процесса может быть несколько потоков, с той разницей, что когда волокно логически блокируется, оно помещается в очередь блокированных волокон, после чего для работы выбирается другое волокно в контексте того же потока. Операционная система не знает о смене волокон, так как все тот же поток продолжает работу. Так как операционная система ничего не знает о волокнах, то с ними, в отличие от заданий, процессов и потоков, не связаны объекты испол­няющей системы. Для управления волокнами нет и настоящих системных вызо­вов. Однако для этого есть вызовы Win32 API. Они относятся к тем вызовам Win32 API, которые не обращаются к системным вызовам.
  Отметим, что операци­онная система Windows 2000 может работать на симметричных многопроцессор­ных системах. Это означает, что код операционной системы должен быть полнос­тью реентерабельным, то есть каждая процедура должна быть написана таким образом, чтобы два или более центральных процессора могли поменять свои пере­менные без особых проблем. Во многих случаях это означает, что программные секции должны быть защищены при помощи спин-блокировки или мьютексов, удерживающих дополнительные центральные процессоры в режиме ожидания, пока первый центральный процессор не выполнит свою работу (при помощи по­следовательного доступа к критическим областям).
 Верхний предел в 32 центральных процессора является жестким пределом, так как во многих местах операционной системы для учета использования централь­ных процессоров используются битовые массивы размером в 32-разрядное машин­ное слово. Например, один однословный битовый массив используется для того, чтобы следить, какой из центральных процессоров свободен в данный момент, а другой массив используется в каждом процессе для перечисления центральных процессоров, на которых этому процессу разрешено работать. 64-разрядная версия Windows 2000 должна будет без особых усилий поддерживать до 64 центральных процессоров. Для превышения этого ограничения потребуется существенная пере­делка программы (с использованием по нескольку слов для битовых массивов).
 

<< Пред.           стр. 5 (из 8)           След. >>

Список литературы по разделу