ГЛАВА 8 

Дополнительные возможности файловых систем

 

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

 

Специальные файлы и аппаратные драйверы

 

Специальные файлы как универсальный интерфейс

 

Понятие «специальный файл» появилось в операционной системе UNIX. Специ­альный файл, называемый также виртуальным файлом, связан с некоторым уст­ройством ввода-вывода и представляет его для остальной части операционной системы и прикладных процессов в виде неструктурированного набора байт, то есть в виде обычного файла. Однако в отличие от обычного файла специальный файл не хранит статичные данные, а является интерфейсом к одному из аппарат­ных драйверов ОС.

Использование специальных файлов во многих случаях существенно упрощает программирование операций с внешними устройствами. Со специальным фай­лом можно работать так же, как и с обычным, то есть открывать, считывать из него или же записывать в него определенное количество байт, а после заверше­ния операции закрывать. Для этого используются привычные многим програм­мистам системные вызовы для работы с обычными файлами: open, create, read, write и close. Кроме того, имеется несколько системных вызовов, используемых только при работе со специальными файлами, например вызов ioctl, с помощью которого можно передать команду контроллеру устройства. Для того чтобы вы­вести на алфавитно-цифровой терминал, с которым связан специальный файл /dev/ttu3, сообщение «Hello, friends!», достаточно открыть этот файл с помощью системного вызова open:

 

fd = open ("/dev/tty3", 2)

 

 Затем можно вывести сообщение с помощью системного вызова write:

 

write (fd. "Hello, friends! ". 15)

 

Для устройств прямого доступа имеет смысл также указатель текущего положе­ния в файле, которым можно управлять с помощью системного вызова 1 seek.

Очевидно, что представление устройства в виде файла и использование для управ­ления устройством файловых системных вызовов позволяет выполнять только простые операции управления, которые сводятся к передаче в устройство после­довательности байт. Для некоторых устройств такие операции вполне адекват­ны — в основном это устройства, отображающие строки символов (алфавит­но-цифровые терминалы, алфавитно-цифровые принтеры) или принимающие от пользователя строки символов (клавиатура). Форматирование ввода-вывода в устройствах этого класса осуществляется с помощью служебных символов на­чала кодовой таблицы и их последовательностей, например для перевода строки и возврата каретки принтера или терминала достаточно к последовательности символов текста добавить восьмеричные коды <12> <15>.

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

Файловый интерфейс доступен пользователю, поэтому прикладной программист может воспользоваться им для создания собственного интерфейса к какому-ли­бо устройству, обходя лежащие над аппаратным драйвером данного устройства слои высокоуровневых драйверов. Например, если прикладного программиста по какой-то причине не устраивают файловые системы, поддерживаемые неко­торой операционной системой, то он может обратиться к диску как к устройству с помощью интерфейса специального файла, который будет вызывать аппарат­ный драйвер диска, поддерживающий модель диска в виде последовательности байт (рис. 8.1). С помощью такого аппаратного драйвера прикладной програм­мист может организовать данные в каком-либо разделе диска оригинальным способом, соответствующим его потребностям. При этом ему не нужно разраба­тывать высокоуровневый драйвер для собственной файловой системы, что явля­ется более сложной задачей по сравнению с разработкой прикладной программы.

 

                                  

 

В UNIX специальные файлы традиционно помещаются в каталог /dev, хотя ни­что не мешает созданию их в любом каталоге файловой системы. При появлении нового устройства и соответственно нового драйвера администратор системы может создать новую запись с помощью команды mknod. Например, следующая команда создает блок-ориентированный специальный файл для представления третьего раздела на втором диске четвертого SCSI-контроллера:

 

mknod   /dev/dsk/scsi b 32 33                            

 

Связь специального файла с драйвером устанавливается  За счет информации, на­ходящейся в индексном дескрипторе специального файла.

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

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

Адресная информация специального файла состоит из двух элементов:

□   major номер драйвера;

□   minor — номер устройства.

Значение major (номер драйвера) определяет выбор драйвера, обслуживающего данный специальный файл, а значение minor (номер устройства) передается драйверу в качестве параметра вызова и указывает ему на одно из нескольких однотипных устройств, которыми драйвер может управлять. Например, для дис­ковых драйверов номер устройства задает не только диск, но и раздел на диске.

В приведенном выше примере команды создания специального файла /dev/dsk/scsi аргумент b определяет создание специального файла для блок-ориентированного драйвера, аргумент 32 определяет номер драйвера, который будет вызываться при открытии устройства /dev/dsk/scsi, а аргумент 33 декодируется самим драйвером (в нем закодированы данные о том, что нужно управлять третьим разделом на втором диске четвертого SCSI-контроллера).

ОС UNIX использует для хранения информации об установленных аппаратных драйверах две системные таблицы:

□   bdevsw — таблица блок-ориентированных драйверов;

□   cdevsw — таблица байт-ориентированных драйверов.

Номер драйвера (major) является индексом соответствующей таблицы. При от­крытии специального файла операционная система обнаруживает, что она имеет дело со специальным файлом только после того, как прочитает с диска или най­дет в системном буфере его индексный дескриптор. При этом она узнает, явля­ется ли вызываемый драйвер блок- или байт-ориентированным, после чего ис­пользует номер драйвера для обращения к определенной строке одной из двух таблиц: bdevsw или cdevsw (рис. 8.2).

 

 

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

Концепция специальных файлов ОС UNIX была реализована во многих опера­ционных системах, хотя для связи с драйверами в них часто используются механизмы, отличные от описанного выше. Так, в ОС Windows NT для связи вирту­альных устройств (аналогов специальных файлов) с драйверами используется механизм объектов. При этом в объектах-устройствах имеются ссылки на объек­ты-драйверы, за счет чего при открытии виртуального устройства система нахо­дит нужный драйвер.

 

Структурирование аппаратных драйверов

 

Аппаратные драйверы можно назвать «истинными» драйверами, так как в отли­чие от высокоуровневых драйверов, они выполняют все традиционные функции по управлению устройствами, включая обработку прерываний и непосредственное взаимодействие с устройствами ввода-вывода.

Более точно, аппаратный драйвер имеет дело не с устройством, а с его контролле­ром. Контроллер, как правило, выполняет достаточно простые функции, например преобразует поток бит в блоки данных и осуществляют контроль и исправление возникающих в процессе обмена данными ошибок. Каждый контроллер имеет не­сколько регистров, которые используются для взаимодействия с центральным процес­сором. Обычно у контроллера имеются регистры данных, через которые осуществ­ляется обмен данными между драйвером и устройством, и управляющие регистры, в которые драйвер помещает команды. В некоторых типах компьютеров регистры являются частью физического адресного пространства, при этом в таких компьюте­рах отсутствуют специальные инструкции ввода-вывода — их функции выполняют инструкции обмена с памятью. В других компьютерах адреса регистров ввода-выво­да, называемых часто портами, образуют собственное адресное пространство за счет введения специальных операций ввода-вывода (например, команд IN и OUT в про­цессорах Intel Pentium).

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

Аппаратный драйвер выполняет ввод-вывод данных, записывая команды в реги­стры контроллера. Например, контроллер диска персонального компьютера при­нимает такие команды, как READ, WRITE, SEEK, FORMAT и т. д. Когда команда принята, процессор оставляет контроллер и занимается другой работой. По завершении команды контроллер генерирует запрос прерывания для того, чтобы передать управление процессором операционной системе, которая должна проверить ре­зультаты операции. Процессор получает результаты и данные о статусе устрой­ства, читая информацию из регистров контроллера.

Аппаратные драйверы могут в своей работе опираться на микропрограммные драй­веры (firmware drivers), поставляемые производителем компьютера и находящиеся в постоянной памяти компьютера (в персональных компьютерах это программ­ное обеспечение получило название BIOS Basic Input-Output System). Микро­программное обеспечение представляет собой самый нижний слой программно­го обеспечения компьютера, управляющий устройствами. Модули этого слоя

выполняют функции транслирующих драйверов и конверторов, экранирующих специфические интерфейсы аппаратуры данной компьютерной системы от опе­рационной системы и ее драйверов.

Драйвер выполняет операцию ввода-вывода, которая представляет собой обмен с устройством заданным количеством байт по заданному адресу оперативной па­мяти (и адресу устройства ввода-вывода в том случае, когда оно является адре­суемым). Примерами операций ввода-вывода могут служить чтение нескольких смежных секторов диска или печать на принтере нескольких строк документа. Операция задается одним системным вызовом ввода-вывода, например read или write.

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

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

• В подсистеме ввода-вывода каждой современной операционной системы сущест­вует стандарт на структуру драйверов. Несмотря на специфику управляемых устройств, в любом драйвере можно выделить некоторые общие части, выпол­няющие определенный набор действий, такие как запуск операции ввода-выво­да, обработка прерывания от контроллера устройства и т. п. Рассмотрим принци­пы структуризации драйверов на примере операционных систем Windows NT и UNIX.

 

Структура драйвера Windows NT

 

Особенностью Windows NT является общая структура драйверов любого уровня и расширенное толкование самого понятия «драйвер». В Windows NT и аппарат­ный драйвер диска, и высокоуровневый драйвер файловой системы построены единообразно, поэтому другие модули ОС взаимодействуют с драйверами одним и тем же способом.

Драйвер Windows NT состоит из следующих (не обязательно всех) процедур:

Процедура инициализации драйвера. Эта процедура выполняется при загрузке драйвера в подсистему ввода-вывода, при этом создаются системные объек­ты, которые позволяют менеджеру ввода-вывода найти нужный драйвер для управления определенным устройством или выполнения некоторых высоко­уровневых функций с информацией, получаемой от устройства или переда­ваемой на устройство.

□  Набор диспетчерских процедур. Эти процедуры составляют основу драйвера, так как именно они выполняют операции ввода-вывода, поддерживаемые данным драйвером, например чтение данных, запись данных, перемотку ленты и т. п.

□   Стартовая процедура предназначена для приведения устройства в исходное состояние перед началом очередной операции. Выполняет «открытие» (open) устройства.

□  Процедура обработки прерывания (ISR) включает наиболее важные действия, которые нужно выполнить при возникновении очередного аппаратного преры­вания от контроллера устройства. Процедура обработки прерывания драйвера имеет достаточно высокий приоритет запроса прерывания IRQL (например, выше приоритета диспетчера потоков), поэтому в нее рекомендуется вклю­чать только те функции, которые требуют незамедлительной реакции, чтобы не задерживать надолго работу других модулей и процессов. По завершении работы ISR может поставить в очередь диспетчера прерываний процедуру DPC драйвера.

□  Процедура отложенных вызовов (DPC). Эта процедура также состоит из функ­ций, которые нужно выполнить при возникновении прерывания от контрол­лера устройства, однако эти функции не требует такой быстрой реакции, как функции ISR. В результате процедура DPC обслуживается с более низким значением приоритета IRQL, давая возможность процедурам ISR и другим приоритетным запросам обслуживаться в первую очередь. Обычно большая часть действий драйвера по обработке прерывания включается в процедуру DPC.

□   Процедура завершения операции уведомляет менеджер ввода-вывода о том, что операция завершена и данные находятся в системной области памяти. Менед­жер при этом может вызвать драйвер более высокого уровня для продолже­ния обработки данных или же вызывать процедуру АРС, рассмотренную в разделе «Процедуры обработки прерываний и текущий процесс» главы 4 «Процессы и потоки» для копирования данных из системной области в об­ласть памяти пользовательского процесса.

□   Процедура отмены ввода-вывода. Для разных стадий выполнения операции могут существовать разные процедуры отмены.

Процедура выгрузки драйвера вызывается при динамической выгрузке драй­вера из подсистемы ввода-вывода. Удаляет созданные для драйвера объекты   и освобождает системную память.

□   Процедура регистрации ошибок. При возникновении ошибки в процессе вы­полнения операции данная процедура уведомляет о ней менеджера ввода-вы­вода, который в свою очередь делает соответствующую запись в журнале ре­гистрации.

Адреса всех перечисленных процедур представляют собой точки входа в драй­вер, известные менеджеру ввода-вывода. Эти адреса хранятся в объекте, создаваемом для каждого драйвера Windows NT, и менеджер использует такие объек­ты для вызова той или иной функции драйвера. Процедура диспетчеризации используется как общая точка входа для нескольких процедур обмена данными (чтение, запись, управление и т. п.), набор которых изменяется от драйвера к драйверу и, следовательно, не может быть стандартизован.

Большое количество стандартизованных функций драйвера Windows NT обуслов­лено желанием разработчиков этой ОС использовать единую модель для драйве­ров всех типов, от сравнительно простого аппаратного драйвера СОМ-порта до весьма сложного драйвера файловой системы NTFS. В результате некоторые функции для некоторого драйвера могут оказаться невостребованными. Напри­мер, для высокоуровневых драйверов не нужна секция обработки прерываний ISR, так как прерывания от устройства обрабатывает соответствующий низко­уровневый драйвер, который затем вызывает высокоуровневый драйвер с помо­щью менеджера ввода-вывода, не используя механизм прерываний.

Рассмотрим особенности вызова функций аппаратного драйвера Windows NT на примере выполнения операции чтения с диска (рис. 8.3). Диск рассматривается в этой операции как виртуальное устройство, следовательно, слой драйверов файловых систем в выполнении операции не участвует.

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

При выполнении системного вызова управление с помощью менеджера ввода-вывода передается стартовой функции драйвера диска DD, которая проверяет, открыт ли виртуальный файл диска и готов ли контроллер диска к выполнению операции обмена данными. После возврата управления от стартовой процедуры менеджер вызывает функцию диспетчеризации драйвера, которой передается па­кет запроса ввода-вывода IRP, содержащий параметры операции — начальный адрес и количество блоков диска. В результате функция диспетчеризации драй­вера вызывает внутреннюю функцию чтения данных с диска, которая передает контроллеру диска запрос на чтение первой порции запрошенных данных. На рисунке работа всех перечисленных функций показана как один этап работы драйвера DD. Тот факт, что драйвер DD выполняет работу для процесса А, отме­чен на рисунке нижним индексом, то есть как DDA.

После завершения чтения порции данных контроллер генерирует аппаратный за­прос прерывания, который вызывает процедуру обработки прерываний драйвера диска ISR, имеющую высокий уровень IRQL. После короткого периода выпол­нения самых необходимых действий с регистрами контроллера (этот период для упрощения рисунка не показан) эта процедура делает запрос на выполнение менее срочной DPC-процедуры драйвера, которая должна выполнить передачу имеющейся у контроллера порции данных в системную область Запрос на вы­полнение DPC-процедуры драйвера DDA некоторое время стоит в очереди уров­ня DPC, так как в это время в процессоре выполняются более приоритетные ISR-процедуры DSB (драйвера стриммера для процесса В) и DPF (драйвера прин­тера для процесса F). После завершения этих процедур начинается выполнение DPC-процедуры драйвера DDA, при этом текущим для ОС процессом является процесс В, сменивший процесс А и прерванный на время ISR-процедурами. Од­нако на выполнение DPC-процедуры драйвера диска это обстоятельство не ока­зывает никакого влияния, так как данные перемещаются в системную область, общую для всех процессов.

Кроме перемещения данных DPC-процедура драйвера выдает контроллеру дис­ка указание о чтении второй и последней для операции порции данных (если контроллер использует режим прямого доступа к памяти и самостоятельно переме­щает данные из своего буфера в системный буфер, то запуск нового действия будет единственной обязанностью DPC-процедуры). Контроллер выполняет чтение и вы­дает новый запрос прерывания, который снова вызывает процедуру обработки пре­рываний драйвера диска. Данная процедура ставит в IRQL-очереди две процедуры: DPC-процедуру, которая, как и в предыдущем цикле чтения, должна переписать данные из буфера контроллера в системный буфер, и АРС-процедуру, которая долж­на переписать все полученные данные из системного буфера в заданную пользова­тельскую область памяти процесса А.

DPC-процедура вызывается раньше, так как имеет более высокий приоритет в очереди диспетчера прерываний. АРС-процедура ждет дольше, так как она име­ет более низкий приоритет и, кроме того, она обязана ждать до тех пор, пока те­кущим процессом не станет процесс А. DPC-процедура после выполнения своей работы фиксирует в операционной системе событие — завершение операции ввода-вывода. По наступлении события вызывается планировщик потоков, кото­рый переводит процесс А в состояние готовности (но не ставит его на выполнение, так как текущий процесс С еще не исчерпал своего кванта времени). И толь­ко после того, как планировщик снимает процесс С  выполнения и делает текущим процесс А, вызывается АРС-процедура, которая вытесняет пользова­тельский код процесса А, имеющий низший приоритет IRQL. АРС-процедура переписывает считанные с  диска-данные из системного буфера в область данных процесса А. Для доступа к системному буферу АРС-процедура должна иметь нужный уровень привилегий. После завершения работы АРС-процедуры управ­ление возвращается пользовательскому коду приложения А, который обрабаты­вает запрошенные у диска данные.

 

Структура драйвера UNIX

 

В ОС UNIX вместо одной общей структуры драйвера существуют две стандарт­ные структуры, одна — для блок-ориентированных драйверов, а другая — для байт-ориентированных. По этой причине в UNIX используются две таблицы, bdevsw и cdevsw, хранящие точки входа в функции драйверов. Каждая из таблиц имеет свою структуру, соответствующую стандартным функциям блок-ориенти­рованных и байт-ориентированных драйверов.

 

Блок-ориентированные драйверы

 

Драйвер блок-ориентированного устройства состоит из следующих функций:

□   open — выполняет процедуру логического открытия устройства;

□   close — выполняет процедуру логического закрытия устройства;

□   strategy — читает или записывает блок;

□   print — выводит сообщение об ошибке;

□   size — возвращает размер раздела, который представляет данное устройст­во.

Указатели на эти функции (то есть их адреса) составляют строку в таблице bdevsw, описывающую один драйвер системы. Ядро UNIX вызывает нужную функцию драйвера, передавая ей параметры, необходимые для работы. Например, при вы­зове функции open ей передается номер устройства (minor), режим открытия (для чтения, для записи, для чтения и записи и т. д.), а также указатель на идентифи­каторы безопасности процесса, открывающего файл.

Процедуры обработки прерываний драйвера в таблице bdevsw не указываются, их адреса помещаются в специальную системную структуру — таблицу прерываний. В UNIX все обработчики прерываний, в том числе и обработчики прерываний аппаратных драйверов, состоят из двух процедур, называемых соответственно top_half — верхняя часть обработчика прерываний и bottom_half — нижняя часть обработчика прерываний. Верхняя часть обработчика прерываний соответствует по назначению ISR-процедуре драйвера Windows NT — она вызывается при воз­никновении аппаратного запроса прерывания от устройства. В обязанности верх­ней части входит быстрая реакция на событие в устройстве, вызвавшее генери­рование сигнала прерывания. При обработке верхних половин все прерывания с более низкими приоритетами блокируются аппаратно, за счет управления кон­троллером прерываний (или аналогичным по назначению блоком компьютера). Верхняя половина отвечает также за постановку в очередь на выполнение ниж­ней половины обработчика прерываний драйвера, который выполняет менее срочную и более трудоемкую работу.

Нижние половины драйверов выполняются с низким уровнем приоритета, так что любые запросы прерываний устройств могут прервать их обработку. Ниж­ние полавины обработчиков прерываний драйверов UNIX по назначению соот­ветствуют DPC-процедурам драйверов Windows NT. Часто единственной обя­занностью верхней половины обработчика прерываний является постановка в очередь нижней половины для последующего выполнения.

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

Функция стратегии драйвера strategy выполняет чтение и запись блока данных на основании информации в буфере — особой структуре ядра с именем buf, управ­ляющей обменом данных с диском. Функция strategy выполняет обмен только с системной памятью, так как блок-ориентированный драйвер непосредственно не взаимодействует с пользовательским процессом. Между ним и пользовательским процессом всегда работает промежуточный программный слой или слои — либо слой дискового кэша вместе со слоем файловой системы, либо слой байт-ориен­тированного драйвера диска, с помощью которого пользовательский процесс мо­жет открыть специальный файл, соответствующий диску.

В число наиболее важных элементов структуры buf входят следующие:

□  b_flags набор бит, в котором задаются тип операции (чтение или запись), синхронный или асинхронный режим операции (при записи), признак актив­ности операции с буфером, признак завершения операции, признак ожидания буфера процессом и некоторые другие;

□   b_forw, b_back указатели на последующий и предыдущий буферы в списке активных (используемых) буферов;

□   av_forw, av_back — указатели на последующий и предыдущий буферы в спис­ке свободных буферов;

□   b_dev номер драйвера (major) и номер устройства (minor) из индексного дескриптора специального устройства, для которого выполняется операция обмена данными;                                                                                             □  b_bcount — количество байт, которые нужно передать;                                 

□   b_addr адрес буфера памяти, куда нужно записать или откуда нужно про­читать данные;

□   b_blkno номер блока в разделе диска;

□   b_bufsize размер блока (в ранних версиях UNIX использовался только один размер блока — 512 байт, в версиях, основанных на коде System V Release 4, можно работать с блоками разного размера);

□  b_iodone — указатель на функцию, которая вызывается по завершении опера­ции ввода-вывода.

Функция strategy при вызове получает указатель на структуру buf, описываю­щую требуемую операцию. На рис. 8.4 приведен пример блок-схемы двух функ­ций драйвера диска — стратегии (hd_strategy) и нижней половины обработчика прерываний (hd_bottom). Функция hd_strategy преобразует логический номер бло­ка в номера цилиндра, головки и сектора и помещает эту информацию в заголо­вок запроса операции для передачи ее контроллеру диска. В заголовок запроса помещается также другая информация, необходимая для работы контроллера, — это операция чтения или записи, адрес системной памяти, куда нужно поместить прочитанную информацию или откуда контроллеру нужно считать записывае­мые данные. Драйвер ведет две очереди для передачи запросов на выполнение операций чтения и записи контроллеру диска: рабочую очередь, в которой нахо­дятся обрабатываемые контроллером запросы, и очередь приостановленных за­просов, куда помещаются новые запросы в том случае, если рабочая очередь заполнена, а ее размер зависит от возможностей контроллера по параллельной обработке запросов.

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

 

Байт-ориентированные драйверы

 

Драйвер байт-ориентированного устройства состоит из следующих стандартных функций:

□  open — открывает устройство;

□   close — закрывает устройство;

□   read — читает данные из устройства;

  write — записывает данные в устройство;

□   ioctl — управляет вводом-выводом;

□   poll — опрашивает устройство для выяснения, не произошло ли некоторое событие;

mmap, segmap — используются при отображении файла-устройства в виртуаль­ную память.                               

Функции чтения и записи данных выполняют обмен заданной последовательно­сти байт из буфера в области пользователя с контроллером символьного устрой­ства.

Функция управления ioctl обеспечивает интерфейс к драйверу устройства, ко­торый выходит за рамки возможностей функций read и write. С помощью функ­ции ioctl обычно устанавливается режим работы устройства, например задаются параметры СОМ-порта, такие как разрядность символов, количество стоповых бит, режим проверки четности и т. п.

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

Если драйвер не поддерживает какую-либо из стандартных функций, то в табли­цу bdevsw помещается указатель на специальную функцию nodev ядра. Напри­мер, драйвер принтера может не поддерживать функцию read. Функция nodev при вызове просто возвращает код ошибки ENODEV и на этом завершает свою работу. Для тех случаев, когда функция должна обязательно поддерживаться (примера­ми таких функций являются функции open и close), но она не выполняет ника­кой полезной работы, в операционной системе имеется функция nodev, которая похожа на функцию nodev, но в отличие от нее возвращает значение 0, которое во всех системных вызовах означает успешное завершение.

На рис. 8.5 показано взаимодействие функции записи драйвера байт-ориентиро­ванного устройства с обработчиком прерываний. Функция записи осуществляет передачу данных из пользовательского буфера процесса, выдавшего запрос на обмен, в системный буфер, организованный в виде очереди байт. Передача байт идет до тех пор, пока системный буфер не заполнится до некоторого, заранее определенного в драйвере уровня. Затем функция записи драйвера приостанав­ливается, выполнив системную функцию sleep, переводящую процесс, в рамках которого работает функция записи write, в состояние ожидания.

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

 

 

Отображаемые в память файлы

 

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

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

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

Механизм отображения файлов в память использует возможности системы вир­туальной памяти для файлов, содержащих произвольные данные (а не только данные исполняемого модуля программы).

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

В UNIX SystemV Release 4 отображение файла в память выполняется с помо­щью системного вызова mmap. Этот вызов имеет следующие аргументы:

□   addr виртуальный адрес начала сегмента, если он задается нулевым, то сис­тема сама выбирает подходящий адрес и возвращает его в качестве значения функции mmap;

□   len — размер сегмента;

□  prot атрибуты защиты сегмента: только чтение, только запись и т. п.;

□  flags флаги, определяющие режим использования сегмента: разделяемый (shared) или закрытый (private);

□  fd — дескриптор открытого файла, данные которого отображаются;

offset — смещение в файле, с которого начинаются отображаемые данные.

Для сравнения рассмотрим две функции, которые выполняют одни и те же дей­ствия с файлом, но с помощью разных средств — функция f f i 1 е использует тра­диционные файловые операции, а функция fmap работает с отображенным в па­мять файлом.

Пусть файл /data/base1 .dat состоит из записей фиксированной длины, каждая из которых включает переменную, отражающую значение баланса предприятия (пере­менная balance) и признак типа баланса (переменная mode):

 

В некоторых операционных системах, например в версиях UNIX, основанных на коде System V Release 4, можно отобразить в память не только обычные файлы, но и некоторые другие типы файлов, например специальные файлы. Отображе­ние в память блок-ориентированного специального файла, то есть раздела или части раздела диска, дает простой доступ к любой области диска, рассматривае­мого как последовательность байт. При отображении байт-ориентированных устройств в оперативную память отображается внутренняя память контроллера устройства, например память сетевого адаптера Ethernet.

В общем случае не все типы файлов можно отобразить в память, например в UNIX SVR4 нельзя отображать каталоги и символьные связи.

Отображение файла эффективней -непосредственного использования файловых операций в нескольких отношениях.

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

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

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

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

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

Механизм отображения файлов в память используется большинством современ­ных операционных систем.

 

Дисковый кэш

 

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

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

Дисковый кэш обычно занимает достаточно большую часть оперативной систем­ной памяти, чтобы максимально повысить вероятность попадания в кэш при вы­полнении дисковых операций. В таких специализированных операционных сис­темах, как NetWare 3.x и 4.x дисковый кэш занимает большую часть физической памяти компьютера, что во многом и обеспечивает высокую скорость файлового сервиса, который находит значительную часть запрошенных по сети данных в кэше. Доля оперативной памяти, отводимой под дисковый кэш, зависит от спе­цифики функций, выполняемых компьютером, — например, сервер приложений выделяет под дисковый кэш меньше памяти, чем файловый сервер, чтобы обес­печить более высокую скорость работы приложений за счет предоставления им большего объема оперативной памяти.

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

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

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

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

 

Традиционный дисковый кэш

 

Рассмотрим традиционный дисковый кэш на примере его организации в ОС UNIX, где он появился в первых же версиях.

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

С каждым буфером связан заголовок, структура которого уже рассматривалась, — это та же структура buf, которая используется блок-ориентированным драйве­ром при выполнении операций чтения и записи. В заголовке buf драйверу пере­даются такие параметры, как адрес самого буфера в системной памяти, тип операции — чтение или запись, а также адресная информация для нахождения блока на диске — номер диска, номер раздела диска и логический номер блока.

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

 

 

При наличии кэша большая часть операций, которые выполняет функция strategy блок-ориентированного драйвера, производится с данными, размещенными в бу­ферах дискового кэша, хотя драйверу все равно, кто инициировал данную опера­цию — диспетчер кэша или другой модуль ядра ОС, лишь бы она описывалась структурой buf с правильно заполненными полями. Отличие для драйвера за­ключается только в том, что диспетчер кэша всегда указывает в buf в качестве  размера переписываемых данных размер блока диска (который чаще всего со­ставляет 512 байт), а другие модули ОС могут указывать произвольный размер области данных. Например, менеджер виртуальной памяти запрашивает обмен страницами.

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

Интерфейс диспетчера кэша образуют следующие функции.

□   Функция bwrite при сброшенных признаках B_ASYNC и B_DELWRI в buf (рас­сматриваются ниже) — синхронная запись. В результате выполнения данной функции немедленно инициируется физический обмен с внешним устройст­вом. Процесс, выдавший запрос, переходит в состояние ожидания результа­та выполнения операции ввода-вывода, используя функцию sieep. В данном случае в процессе может быть предусмотрена собственная реакция на оши­бочную ситуацию. Такой тип записи используется тогда, когда необходима гарантия правильного завершения операции ввода-вывода.

□   Функция bwrite при установленном признаке B_ASYNC и сброшенном признаке B_DELWRI — асинхронная запись. Признак B_ASYNC задает асинхронный харак­тер выполнения операции, при этом так же, как и в предыдущем случае, ини­циируется физический обмен с устройством, однако завершения операции ввода-вывода функция bwrite не дожидается и немедленно возвращает управ­ление. В этом случае возможные ошибки ввода-вывода не могут быть переда­ны в процесс, выдавший запрос. Такая операция записи целесообразна при поточной обработке файлов, когда ожидание завершения операции ввода-вы­вода не обязательно, но есть уверенность в возможности повторения этой операции.

□ Функция bwrite с установленными признаками B_ASYNC и B_DELWRI — отложен­ная запись. При этом передача данных из системного буфера не производит­ся, а в заголовке буфера делается отметка о том, что буфер заполнен и может быть выгружен, если потребуется его освободить. Управление немедленно воз­вращается вызвавшей функции.

□   Функции bread и getblk — прочитать и получить блок. Каждая из этих функ­ций ищет в пуле буфер, содержащий указанный блок данных (по номеру уст­ройства и номеру блока). Если такого блока в буферном пуле нет, то в случае использования функции getblk осуществляется поиск любого свободного бу­фера, а при его отсутствии возможна выгрузка на диск буфера, содержащего в заголовке признак отложенной записи. В случае использования функции bread при отсутствии заданного блока в буферном пуле сначала вызывается функция getblk для получения свободного буфера, а затем организуется за­грузка в него данных с помощью вызова блок-ориентированного драйвера.

Функция getbl k используется тогда, когда содержимое зарезервированного блока не существенно, например при записи на устройство блока данных.

Упрощенный алгоритм выполнения запросов к подсистеме буферизации приве­ден на рис.

 8.7.

 

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

 

Дисковый кэш на основе виртуальной памяти

 

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

В операционных системах UNIX на основе кода SVR4 в виртуальном адресном пространстве системы существует специальный сегмент segkmap, в который ото­бражаются данные всех открытых файлов. Диспетчер кэша поддерживает массив структур smap, каждая из которых хранит описание одного отображения. Одно отображение состоит из 8096 последовательных блоков одного файла. Отобра­жаемый файл описывается в структуре smap парой vnode /offset, где vnode явля­ется указателем на виртуальный дескриптор операции с файлом (см. раздел «Файловые операции» в главе 7 «Ввод-вывод и файловая система»), a offset смещением в файле на диске, начиная с которого отображаются данные файла. Кроме того, в структуре smap указывается виртуальный адрес внутри сегмента segkmap, на который отображаются данные файла.

При выполнении системного вызова read для чтения данных из некоторого от­крытого файла подсистема ввода-вывода вызывает функцию segmap_getmap дис­петчера кэша, которой передается в качестве параметра пара vnode/offset (эти значения берутся из структуры file, описывающей операцию с файлом). Функ­ция segmap_getmap ищет в массиве smap элемент, который содержит требуемую пару vnode/offset. Если такого элемента нет, то это значит, что требуемый уча­сток файла еще не был отображен в системную память и для нового отображе­ния создается новый элемент smap, а значение виртуального адреса, на который он указывает, возвращается в read. После этого системный вызов read пытается скопировать данные из системной памяти, начиная с указанного адреса, в поль­зовательский буфер. Так как страницы, содержащей требуемый виртуальный адрес, в оперативной памяти пока нет, то при обращении к памяти возникает страничное прерывание, которое обслуживается соответствующим обработчиком прерываний. В результате блок-ориентированный драйвер читает блоки отсутст­вующей страницы (а при упреждающей загрузке — и нескольких окружающих ее страниц) с диска и помещает их в системную память. Затем системный вызов read продолжает свою работу, фактически копируя данные в пользовательский буфер. Последующие обращения с помощью вызова read к близкой к прочитан­ным данным области файла (в пределах 8096 блоков) уже не вызывают нового отображения, а страничные прерывания будут загружать новые блоки файла в кэш по мере необходимости.

На основе таких же принципов работают диспетчеры кэша и в других современ­ных ОС, поддерживающих виртуальную память, например Windows NT, OS/2.

 

Отказоустойчивость файловых и дисковых систем

 

Диски и файловые системы, используемые для упорядоченного хранения дан­ных на дисках, часто представляют собой последний «островок стабильности», на котором находит спасение пользователь после неожиданного краха системы,

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

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

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

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

 

Восстанавливаемость файловых систем

 

Причины нарушения целостности файловых систем

 

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

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

Рассмотрим, например, последствия сбоя при удалении файла в файловой сис­теме FAT. Для выполнения этой операции требуется пометить как недействи­тельную запись об этом файле в каталоге, а также обнулить все элементы FAT, которые соответствуют кластерам удаляемого файла. Предположим, что сбой питания произошел после того, как была объявлена недействительной запись в каталоге и обнулено несколько (но не все) элементов FAT, занимаемых удаляе­мым файлом. В этом случае после сбоя файловая система сможет продолжать нормальную работу, за исключением того, что несколько последних кластеров удаленного файла будут теперь «вечно» помечены занятыми. Хуже было бы, если бы операция удаления начиналась с обнуления элементов FAT, а корректиров­ка каталога происходила бы после. Тогда при возникновении сбоя между этими подоперациями содержимое каталога не соответствовало бы действительному состоянию файловой системы: файл как будто существует, а на самом деле его нет. Не исправленная запись в каталоге содержит адрес кластера, который уже объявлен свободным и может быть назначен другому файлу; это может привести к разного рода коллизиям.

Некорректность файловой системы может возникать не только в результате насильственного прерывания операций ввода-вывода, выполняемых непосредст­венно с диском, но и в результате нарушения работы дискового кэша. Кэширова­ние данных с диска предполагает, что в течение некоторого времени результаты операций ввода-вывода никак не сказываются на содержимом диска — все изме­нения происходят с копиями блоков диска, временно хранящихся в буферах опе­ративной памяти. В этих буферах оседают данные из пользовательских файлов и служебная информация файловой системы, такая как каталоги, индексные деск­рипторы, списки свободных, занятых и поврежденных блоков и т. п. Для согласования содержимого кэша и диска время от времени выполняется за­пись всех модифицированных блоков, находящихся в кэше, на диск. Выталкива­ние блоков на диск может выполняться либо по инициативе менеджера дисково­го кэша, либо по инициативе приложения. Менеджер дискового кэша вытесняет блоки из кэша в следующих случаях:

□   если необходимо освободить место в кэше для новых данных;

□   если к менеджеру поступил запрос от какого-либо приложения или модуля ОС на запись указанных в запросе блоков на диск;

□   при выполнении регулярного, периодического сброса всех модифицирован­ных блоков кэша на диск (как это происходит, например, в результате работы системного вызова sync в ОС UNIX).

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

Несмотря на то что период полного сброса кэша на диск обычно выбирается весьма коротким (порядка 10-30 секунд), все равно остается высокая вероят­ность того, что при возникновении сбоя содержимое диска не в полной мере бу­дет соответствовать действительному состоянию файловой системы — копии некоторых блоков с обновленным содержимым система может не успеть перепи­сать на диск. Для восстановления некорректных файловых систем, использую­щих кэширование диска, в операционных системах предусматриваются специ­альные утилиты, такие как fsck для файловых систем s5/uf, ScanDisk для FAT или Chkdsk для файловой системы HPFS. Однако объем несоответствий может быть настолько большим, что восстановление файловой системы после сбоя с помощью стандартных системных средств становится невозможным.

 

Протоколирование транзакций

 

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

Модель неделимой транзакции пришла из бизнеса. Пусть, например, идет пере­говорный процесс двух фирм о покупке-продаже некоторого товара. В процессе переговоров условия договора могут многократно меняться, уточняться. Пока до­говор еще не подписан обеими сторонами, каждая из них может от него отказать­ся. Но после подписания контракта сделка (транзакция) должна быть выполнена от начала и до конца. Если же контракт не подписан, то любые действия, кото­рые были уже проделаны, отменяются или объявляются недействительными. В файловых системах такими транзакциями являются операции ввода-вывода, изменяющие содержимое файлов, каталогов или других системных структур файловой системы (например, индексных дескрипторов ufs или элементов FAT). Пусть к файловой системе поступает запрос на выполнение той или иной опера­ции ввода-вывода. Эта операция включает несколько шагов, связанных с созда­нием, уничтожением и модификацией объектов файловой системы. Если все подоперации были благополучно завершены, то транзакция считается выполнен­ной. Это действие называется фиксацией {committing) транзакции. Если же одна или более подопераций не успели выполниться из-за сбоя питания или краха ОС, тогда для обеспечения целостности файловой системы все измененные в рамках транзакции данные файловой системы должны быть возвращены точно в то состояние, в котором они находились до начала выполнения транзакции. Так, например, транзакцией может быть представлена операция удаления файла. Действительно, для целостности файловой системы необходимо, чтобы все тре­буемые при выполнении данной операции изменения каталога и таблицы рас­пределения дисковой памяти были сделаны в полном объеме. Либо, если во вре­мя операции произошел сбой, каталог и таблица распределения памяти должны быть приведены в исходное состояние.

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

Незавершенная операция с диском несет угрозу целостности файловой системы. Каким же образом файловая система может реализовать свойство транзакций «все или ничего»? Очевидно, что решение в этом случае может быть одно — не­обходимо протоколировать (запоминать) все изменения, происходящие в рам­ках транзакции, чтобы на основе этой информации в случае прерывания тран­закции можно было отменить все уже выполненные подоперации, то есть сделать так называемый откат транзакции.

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

 

ПРИМЕЧАНИЕ ----------------------------------------------------------------------------------------------

При восстановлении часто возникают ситуации, когда система пытается отменить тран­закцию, которая уже была отменена или вообще не выполнялась. Аналогично при повто­рении некоторой транзакции может оказаться, что она уже была выполнена. Учитывая это, необходимо так определить подоперации транзакции, чтобы многократное выполне­ние каждой из этих подопераций не имело никакого добавочного эффекта по сравнению с первым выполнением этой подоперации. Такое свойство операции называется идемпо­тентностью (idempotency}. Примером идемпотентной операции может служить, напри­мер, многократное присвоение переменной некоторого значения (сравните с операциями инкремента и декремента).

 

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

 

 

 

 

Восстанавливаемость файловой системы NTFS

 

Файловая система NTFS является восстанавливаемой файловой системой, однако восстанавливаемость обеспечивается только для системной информации файло­вой системы, то есть каталогов, атрибутов безопасности, битовой карты занято­сти кластеров и других системных файлов. Сохранность данных пользователь­ских файлов, работа с которыми выполнялась в момент сбоя, в общем случае не гарантируется.

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

Журнал регистрации транзакций в NTFS делится на две части: область рестарта и область протоколирования (рис. 8.8)t

□   Область рестарта содержит информацию о том, с какого места необходимо будет начать читать журнал транзакций для проведения процедуры восста­новления системы после сбоя или краха ОС. Эта информация представляет собой указатель на определенную запись в области протоколирования. Для надежности в файле журнала регистрации хранятся две копии области рес­тарта.

□   Область протоколирования содержит записи обо всех изменениях в системных данных файловой системы, произошедших в результате выполнения транзак­ций в течение некоторого, достаточно большого периода. Все записи иденти­фицируются логическим последовательным номером LSN (Logical Sequence Number). Записи о подоперациях, принадлежащих одной транзакции, образу­ют связанный список: каждая последующая запись содержит номер предыду­щей записи. Заполнение области протоколирования идет циклически после исчерпания всей памяти, отведенной под область протоколирования, новые записи помещаются на место самых старых.

 

Существует несколько типов записей в журнале транзакций: запись модифика­ции, запись контрольной точки, запись фиксации транзакции, запись таблицы модификации, запись таблицы модифицированных страниц.

Запись модификации заносится в журнал транзакций относительно каждой под­операции, которая модифицирует системные данные файловой системы. Эта за­пись состоит из двух частей: одна содержит информацию, необходимую системе для повторения этого действия, а другая — информацию для его отмены. Инфор­мация о модификации хранится в двух формах — в физическом и в логическом описаниях. Логическое описание используется программным обеспечением уровня приложений и формулируется в терминах операций, например «выделить фай­ловую запись в MFT» или «удалить имя из корневого индекса». На нижнем уровне программного обеспечения, к которому относятся модули самой NTFS, используется менее компактное, но более простое физическое описание, сводя­щееся к указанию диапазона байт на диске, в которые необходимо поместить оп­ределенные значения.

Пусть, например, регистрируется транзакция создания файла lotus.doc, которая включает, как показано на рисунке, три подоперации. Тогда в журнале будут сде­ланы три записи модификации, содержимое которых приведено в табл. 8.1.

 

 

 

 

 

 

 

 

 

 

 


                                                                                                                             

 

Журнал транзакций, как и все остальные файлы, кэшируется в буферах опера­тивной памяти и периодически сбрасывается на диск.

Файловая система NTFS все действия с журналом транзакций выполняет только путем запросов к специальной службе LFS (Log File Service). Эта служба разме­щает в журнале новые записи, сбрасывает на диск все записи до некоторого за­данного номера, считывает записи в прямом и обратном порядке и выполняет некоторые другие действия над записями журнала.

Прежде чем выполнить любую транзакцию, NTFS вызывает службу журнала тран­закций LFS для регистрации всех подопераций в журнале транзакций. И только после этого описанные подоперации действительно выполняются над копиями блоков данных файловой системы, находящимися в кэше. Когда все подопера­ции транзакции выполнены, с помощью службы LFS транзакция фиксируется. Это выражается в том, что в журнал заносится специальный вид записи — запись фиксации транзакции.

Параллельно с регистрацией и выполнением транзакций происходит процесс выталкивания блоков кэша на диск. Сброс на диск измененных блоков выполня­ется в два этапа: сначала сбрасываются блоки журнала, а потом — модифици­рованные блоки транзакций. Такой порядок реализуется следующим образом. Каждый раз, когда диспетчер кэша принимает решение о том, что определенные модифицированные блоки (не обязательно все) должны быть вытеснены на диск, он сообщает об этом службе LFS. В ответ на это сообщение LFS обращает­ся к диспетчеру кэша с запросом о записи на диск всех измененных блоков журнала. После того как блоки журнала сброшены на диск, сбрасываются на диск модифицированные блоки транзакций, среди которых могут быть, конечно, и бло­ки системных данных файловой системы.

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

Какие же дефекты может иметь файловая система после сбоя? Во-первых, это несогласованность системных данных, возникшая в результате незавершенности транзакций, которые были начаты еще до момента последнего сброса данных из кэша на диск. На рис. 8.9 показана транзакция А, две подопера­ции которой — а( и а2 — были сделаны до сброса кэша, а еще две — а3 и а4 — после сброса кэша. К моменту сбоя результаты первых двух подопераций могли быть записаны на диск, в то время как изменения, вызванные подоперациями а3 и а4, отразились только на копиях блоков файловой системы в кэше и были потеряны в результате сбоя. Чтобы устранить несогласованность, вызванную этой причи­ной, требуется сделать откат для всех транзакций, незафиксированных к моменту последнего сброса кэша. Для примера, изображенного на рисунке, такими тран­закциями являются транзакции А и С. В каждый момент времени NTFS распо­лагает списком незафиксированных транзакций, называемым таблицей незавер­шенных транзакций {transaction table). Для каждой незавершенной транзакции эта таблица содержит последовательный номер LSN последней по времени под­операции, выполненной в рамках данной транзакции. По этому номеру может быть найдена вся цепочка подопераций транзакции.

Во-вторых, противоречия в файловой системе могут быть вызваны потерей тех изменений, которые были сделаны транзакциями, завершившимися еще до сброса кэша, но которые не были записаны на диск в ходе последнего сброса. На рисун­ке такой транзакцией может оказаться транзакция В. Чтобы определить, какие завершенные транзакции надо повторять, система ведет таблицу модифициро­ванных страниц1 {dirty page table), находящихся в данный момент в кэше. В таб­лице для каждой модифицированной страницы указывается, какая транзакция вызвал, эти изменения. Повторение транзакций, которые имели дело со страни­цами, указанными в данном списке, гарантирует, что ни одно изменение не будет потеряно.

Таблицы модифицированных страниц и незавершенных транзакций создаются NTFS на основании записей журнала транзакций и поддерживаются в оператив­ной памяти. Следует подчеркнуть, что обе эти таблицы не добавляют новой ин­формации в журнал транзакций, они лишь представляют информацию, содер­жащуюся в записях журнала, в концентрированном виде, более удобном для использования при восстановлении. Содержимое таблиц фиксируется в журна­ле транзакций во время выполнения операции контрольная точка. Операция контрольная точка выполняется каждые 5 секунд и включает выпол­нение следующих действий (рис. 8.10). Сначала в области протоколирования журнала транзакций создаются две записи — запись таблицы незавершенных транзакций и запись таблицы модифицированных страниц, содержащие копии соответствующих таблиц. Затем номера этих записей включаются в запись кон­трольной точки, которая также создается в области протоколирования журнала транзакций. Сделав запись контрольной точки, NTFS помещает ее номер LSN в область рестарта.

 

 

                                                                                                                                                                                                                                                                                      

 

 

 

Заметим, что процессы создания контрольных точек и сброса блоков данных из кэша на диск протекают асинхронно. Когда в результате сбоя из оперативной па­мяти исчезает вся информация, в том числе из таблиц незавершенных транзакций и модифицированных страниц, состояние этих таблиц; хотя и несколько ус­таревшее, сохраняется на диске в файле журнала транзакций. Кроме того, здесь же имеется несколько более поздних записей, которые были сделаны в период между сохранением таблиц и сбросом кэша (на рисунке это записи Ml, M2, МЗ). При восстановлении файловая система обрабатывает эти записи и вносит из­менения в таблицы незавершенных транзакций и модифицированных страниц, сохраненные в журнале. Так, например, если запись Ml является записью фик­сации транзакции, то соответствующая транзакция исключается из таблицы не­завершенных транзакций, а если это запись модификации, то в таблицу модифи­цированных страниц заносится информация об еще одной странице. Процесс восстановления файловой системы включает следующие шаги:

1.   Чтение области рестарта из файла журнала транзакций и определение номера самой последней по времени записи о контрольной точке.

2.   Чтение записи контрольной точки и определение номеров записей таблицы незавершенных транзакций и таблицы модифицированных страниц.

3.   Чтение и корректировка таблиц незавершенных транзакций и модифициро­ванных страниц на основании записей, сделанных в журнале транзакций уже после сохранения таблиц в журнале, но еще до записи журнала на диск (рис. 8.11).

 

 

 

4.   Анализ таблицы модифицированных страниц, определение номера самой ран­ней записи модификации страницы.

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

6.   Анализ таблицы незавершенных транзакций, определение номера самой позд­ней подоперации, выполненной в рамках незавершенной транзакции.

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

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

 

Избыточные дисковые подсистемы RAID

 

В основе средств обеспечения отказоустойчивости дисковой памяти лежит об­щий для всех отказоустойчивых систем принцип избыточности, и дисковые под­системы RAID (Redundant Array of Inexpensive Disks, дословно — «избыточный массив недорогих дисков») являются примером реализации этого принципа. Идея технологии RAID-массивов состоит в том, что для хранения данных использует­ся несколько дисков, даже в тех случаях, когда для таких данных хватило бы места на одном диске. Организация совместной работы нескольких централизо­ванно управляемых дисков позволяет придать их совокупности новые свойства, отсутствовавшие у каждого диска в отдельности.

RAID-массив может быть создан на базе нескольких обычных дисковых уст­ройств, управляемых обычными контроллерами, в этом случае для организации управления всей совокупностью дисков в операционной системе должен быть установлен специальный драйвер. В Windows NT, например, таким драйвером является FtDisk — драйвер отказоустойчивой дисковой подсистемы. Сущест­вуют также различные модели дисковых систем, в которых технология RAID реализуется полностью аппаратными средствами, в этом случае массив дисков управляется общим специальным контроллером.

Дисковый массив RAID представляется для пользователей и прикладных про­грамм единым логическим диском. Такое Логическое устройство может обладать различными качествами в зависимости от стратегии, заложенной в алгоритмы работы средств централизованного управления и размещения информации на всей совокупности дисков. Это логическое устройство может, например, обладать по­вышенной отказоустойчивостью или иметь производительность, значительно боль­шую, чем у отдельно взятого диска, либо обладать обоими этими свойствами. Различают несколько вариантов RAID-массивов, называемых также уровнями: RAID-0, RAID-1, RAID-2, RAID-3, RAID-4, RAID-5 и некоторые другие.

При оценке эффективности RAID-массивов чаще всего используются следую­щие критерии:

□   степень избыточности хранимой информации (или тесно связанная с этим критерием стоимость хранения единицы информации);

□   производительность операций чтения и записи;

□   степень отказоустойчивости.

В логическом устройстве RAID-0 (рис. 8.12) общий для дискового массива кон­троллер при выполнении операции записи расщепляет данные на блоки и. пере­дает их параллельно на все диски, при этом первый блок данных записывается

на первый диск, второй — на второй и т. д. Различные варианты реализации тех­нологии RAID-0 могут отличаться размерами блоков данных, например в набо­рах с чередованием, представляющих собой программную реализацию RAID-0 в Windows NT, на диски поочередно записываются полосы данных (strips) по 64 Кбайт. При чтении контроллер мультиплексирует блоки данных, поступаю­щие со всех дисков, и передает их источнику запроса.

 

По сравнению с одиночным диском, в котором данные записываются и считываются с диска последовательно, производительность дисковой конфигурации RAID-0 значительно выше за счет одновременности операций записи/чтения по всем дискам массива.

Уровень RAID-0 не обладает избыточностью данных, а значит, не имеет возмож­ности повысить отказоустойчивость. Если при считывании произойдет сбой, то данные будут безвозвратно испорчены. Более того, отказоустойчивость даже снижается, поскольку если один из дисков выйдет из строя, то восстанавливать придется все диски массива. Имеется еще один недостаток — если при работе с RAID-0 объем памяти логического устройства потребуется изменить, то сделать это путем простого добавления еще одного диска к уже имеющимся в RAID-мас­сиве дискам невозможно без полного перераспределения информации по всему изменившемуся набору дисков.

Уровень RAID-1 (рис. 8.13) реализует подход, называемый зеркальным копиро­ванием (mirroring). Логическое устройство в этом случае образуется на основе одной или нескольких пар дисков, в которых один диск является основным, а другой диск (зеркальный) дублирует информацию, находящуюся на основном диске. Если основной диск выходит из строя, зеркальный продолжает сохранять данные, тем самым обеспечивается повышенная отказоустойчивость логическо­го устройства. За это приходится платить избыточностью — все данные хранятся на логическом устройстве RAID-1 в двух экземплярах, в результате дисковое пространство используется лишь на 50 %.

 

 

 

При внесении изменений в данные, расположенные на логическом устройстве RAID-1, контроллер (или драйвер) массива дисков одинаковым образом моди­фицирует и основной, и зеркальный диски, при этом дублирование операций абсолютно прозрачно для пользователя и приложений. Удвоение количества операций записи снижает, хотя и не очень значительно, производительность дисковой подсистемы, поэтому во многих случаях наряду с дублированием дис­ков дублируются и их контроллеры. Такое дублирование (duplexing) помимо по­вышения скорости операций записи обеспечивает большую надежность систе­мы — данные на зеркальном диске останутся доступными не только при сбое диска, но и в случае сбоя дискового контроллера.

Некоторые современные контроллеры (например, SCSI-контроллеры) обладают способностью ускорять выполнение операций чтения с дисков, связанных в зер­кальный набор. При высокой интенсивности ввода-вывода контроллер распреде­ляет нагрузку между двумя дисками так, что две операции чтения могут быть выполнены одновременно. В результате распараллеливания работы по считыва­нию данных между двумя дисками время выполнения операции чтения может быть снижено в два раза Таким образом, некоторое снижение производительно­сти, возникающее при выполнении операций записи, с лихвой компенсируется повышением скорости выполнения операций чтения.

Уровень RAID-2 расщепляет данные побитно: первый бит записывается на пер­вый диск, второй бит — на второй диск и т. д. Отказоустойчивость реализуется в RAID-2 путем использования для кодирования данных корректирующего кода Хэмминга, который обеспечивает исправление однократных ошибок и обнару­жение двукратных ошибок. Избыточность обеспечивается за счет нескольких дополнительных дисков, куда записывается код коррекции ошибок. Так, массив с числом основных дисков от 16 до 32 должен иметь три дополнительных диска для хранения кода коррекции. RAID-2 обеспечивает высокую производительность и надежность, но он применяется в основном в мэйнфреймах и суперкомпьюте­рах. В сетевых файловых серверах этот метод в настоящее время практически не используется из-за высокой стоимости его реализации.

В массивах RAID-3 используется расщепление (stripping) данных на массиве дис­ков с выделением одного диска на весь набор для контроля четности. То есть если имеется массив из N дисков, то запись на N-1 из них производится параллельно с побайтным расщеплением, а N-й диск используется для записи контрольной ин­формации о четности. Диск четности является резервным. Если какой-либо диск выходит из строя, то данные остальных дисков плюс данные о четности резервно­го диска позволяют не только определить, какой из дисководов массива вышел из строя, но и восстановить утраченную информацию. Это восстановление может выполняться динамически, по мере поступления запросов, или в результате выпол­нения специальной процедуры восстановления, когда содержимое отказавшего дис­ка заново генерируется и записывается на резервный диск.

Рассмотрим пример динамического восстановления данных. Пусть массив RAID-3 состоит из четырех дисков: три из них — ДИСК 1, ДИСК 2 и ДИСК 3 — хранят данные, а ДИСК 4 хранит контрольную сумму по модулю 2 (XOR). И пусть на ло­гическое устройство, образованное этими дисками, записывается последователь­ность байт, каждый из которых имеет значение, равное его порядковому номеру в последовательности. Тогда первый байт 0000 0001 попадет на ДИСК 1, второй байт 0000 0010 - на ДИСК 2, а третий по порядку байт - на ДИСК 3. На чет­вертый диск будет записана сумма по модулю 2, равная в данном случае 0000 0000 (рис. 8.14). Вторая строка таблицы, приведенной на рисунке, соответ­ствует следующим трем байтам и их контрольной сумме и т. д. Представим, что ДИСК 2 вышел из строя.

 

 

При поступлении запроса на чтение, например, пятого байта (он выделен жир­ным шрифтом) контроллер дискового массива считывает данные, относящиеся к этой строке со всех трех оставшихся дисков — байты 0000 0100, 0000 0110, 0000 0111 — и вычисляет для них сумму по модулю 2. Значение контрольной суммы 0000 0101 и будет являться восстановленным значением потерянного из-за неисправности пятого байта.

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

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

Минимальное количество дисков, необходимое для создания конфигурации RAID-3, равно трем. В этом случае избыточность достигает максимального значения — 33 %.  . При увеличении числа дисков степень избыточности снижается, так, для 33 дисков она составляет менее 1 %.

Уровень RAID-3 позволяет выполнять одновременное чтение или запись данных на несколько дисков для файлов с длинными записями, однако следует подчерк­нуть, что в каждый момент выполняется только один запрос на ввод-вывод, то есть RAID-3 позволяет распараллеливать ввод-вывод в рамках только одного процесса (рис. 8.15). Таким образом, уровень RAID-3 повышает как надежность, так и ско­рость обмена информацией.

 

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

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

В уровне RAID-5 (рис. 8.16) используется метод, аналогичный RAID-4, но дан­ные о контроле четности распределяются по всем дискам массива. При выполне­нии операции записи требуется в три раза больше оперативной памяти. Каждая команда записи инициирует ту же последовательность «считывание—модифи­кация—запись» в нескольких дисках, как и в методе RAID-4. Наибольший выигрыш в производительности достигается при операциях чтения. Поскольку информация о четности может быть считана и записана на несколько дисков од­новременно, скорость записи по сравнению с уровнем RAID-4 увеличивается, однако она все еще гораздо ниже скорости отдельного диска метода RAID-1 или RAID-3.

 

 

 

Кроме рассмотренных выше имеются еще и другие варианты организации со­вместной работы избыточного набора дисков, среди них можно особо отметить технологию RAID-10, которая представляет собой комбинированный способ, при котором данные «расщепляются» (RAID-0) и зеркально копируются (RAID-1) без вычисления контрольных сумм. Обычно две пары «зеркальных» массивов объединяются и образуют один массив RAID-0. Этот способ целесообразно при­менять при работе с большими файлами.

В табл. 8.2 сведены основные характеристики для некоторых конфигураций из­быточных дисковых массивов.

 

 

 

Обмен данными между процессами и потоками

 

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

Эти средства, так же как и рассмотренные выше средства синхронизации процес­сов, относятся к классу средств межпроцессного взаимодействия, то есть IPC (Inter-Process Communications).

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

Набор средств межпроцессного обмена данными в большинстве современных> ОС выглядит следующим образом:

□   конвейеры (pipes);

□  именованные конвейеры (named pipes);

□   очереди сообщений (message queues);

  разделяемая память (shared memory).

Кроме этого достаточно стандартного набора средств в конкретных ОС часто име­ются и более специфические средства межпроцессного обмена, например средст­ва среды STREAMS для различных версий UNIX или почтовые ящики (mail slots) в ОС Windows.

 

Конвейеры

 

Конвейеры как средство межпроцессного обмена данными впервые появились в операционной системе UNIX. Системный вызов pipe позволяет двум процессам обмениваться неструктурированным потоком байт. Конвейер представляет собой буфер в оперативной памяти, поддерживающий очередь байт по алгоритму FIFO. Для программиста, использующего системный вызов pipe, этот буфер вы­глядит как безымянный файл, в который можно писать и читать, осуществляя тем самым обмен данными.

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

При выполнении системного вызова pipe в процесс возвращаются два дескрип­тора файла, один для записи данных в конвейер, а другой для чтения данных из конвейера. Обычно для выполнения некоторой общей работы ведущий процесс сначала создает конвейер, а затем — несколько процессов-потомков с помощью соответствующего системного вызова. В результате механизм наследования про­цессов копирует для всех процессов-потомков значения дескрипторов, указываю­щих на один и тот же конвейер, так что все кооперирующиеся процессы, вклю­чая процесс-прародитель, могут использовать этот конвейер для обмена данными. Данные читаются из конвейера с помощью системного вызова read с использова­нием первого из возвращенных вызовом pipe дескрипторов файла, а записыва­ются в конвейер с помощью системного вызова write с использованием второго дескриптора. Синтаксис системных вызовов read и write тот же, что и при работе с обычными файлами.

Конвейер обеспечивает автоматическую синхронизацию процессов — если при использовании системного вызова read в буфере конвейера нет данных, то про­цесс, обратившийся к ОС с системным вызовом read, переводится в состояние ожидания и активизируется при появлении данных в буфере.          

Механизм конвейеров доступен не только программистам, но и пользователям большинства современных операционных систем. Именно системные вызовы pipe используются оболочкой (командным процессором) операционной системы для организации конвейера команд, когда выходные данные одной команды поль­зователя становятся входными данными для другой команды. Примером такого конвейера команд может служить показанная ниже строка командного интер­претатора shell ОС UNIX, которая передает выходные данные команды ls (чте­ние списка имен файлов текущего каталога) на вход команды we (подсчет слов) с ключом -l                                                                                           

 

Is | we -l                                                                                            

 

Результатом работы этой командной строки будет количество файлов в текущем каталоге.                                      

 

Именованные конвейеры

 

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

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

Ввиду того что именованные конвейеры основаны на файловой системе, обычные конвейеры, создаваемые системным вызовом pipe, иногда называют программ­ными конвейерами (software-pipes). Следует иметь в виду, что именованные кон­вейеры используют файловую систему только для хранения имени конвейера в каталоге, а данные между процессами передаются через буфер в оперативной па­мяти, как и в случае программного конвейера.

 

Очереди сообщений

 

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

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

Для работы с очередью сообщений процесс должен воспользоваться системным вызовом msgget, указав в качестве параметра значение ключа. Если очередь с дан­ным ключом в настоящий момент не используется ни одним процессом, то для нее резервируется область памяти, а<затем процессу возвращается идентифи­катор очереди, который, как и дескриптор файла, имеет локальное для процесса значение. Если же очередь уже используется, то процессу просто возвращается ее идентификатор. Системный администратор может управлять настройками операционной системы Для изменения максимального объема памяти, отводи­мой очереди, а также максимального размера сообщения.

После открытия очереди процесс может помещать в него сообщения с помощью вызова msgsnd или читать сообщения с помощью вызова msgrsv. Программист мо­жет влиять на то, как ОС будет обрабатывать ситуацию, когда процесс пытается читать сообщения, которые еще не поступили в очередь, то есть на синхрониза­цию процесса с данными. При задании в системных вызовах msgsnd и msgrcv па­раметра IPC_NOWAIT операционная система в любом случае будет возвращать управление в вызывающий процесс, даже если он пытается прочитать несущест­вующее сообщение (в последнем случае в процесс возвращается код ошибки). Без этого параметра процесс при отсутствии данных переводится в состояние ожидания. Параметр IPC_NOWAIT используется не только в очередях сообщений, но и в некоторых других средствах IPC, например в семафорах. При использо­вании параметра IP_CNOWAIT программист должен самостоятельно организовать ожидание данных.

 

Разделяемая память

 

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

ГЛАВА 9

Концепции распределенной обработки в сетевых ОС

 

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

Распределенные приложения обладают рядом потенциальных преимуществ по сравнению с локальными. Среди этих преимуществ — более высокая производи­тельность, отказоустойчивость, масштабируемость и приближение к пользователю.

 

Модели сетевых служб

и распределенных приложений

 

Значительная часть приложений, работающих в компьютерах сети, являются сете­выми, но, конечно, не все. Действительно, ничто не мешает пользователю запус­тить на своем компьютере полностью локальное приложение, не использующее имеющиеся сетевые коммуникационные возможности. Достаточно типичным является сетевое приложение, состоящее из двух частей. Например, одна часть приложения работает на компьютере, хранящем базу данных большого объема, а вторая — на компьютере пользователя, который хочет видеть на экране неко­торые статистические характеристики данных, хранящихся в базе. Первая часть приложения выполняет поиск в базе записей, отвечающих определенным крите­риям, а вторая занимается статистической обработкой этих данных, представ­лением их в графической форме на экране, а также поддерживает диалог с пользователем, принимая от него новые запросы на вычисление тех или иных статистических характеристик. Можно представить себе случаи, когда приложе­ние распределено и между большим числом компьютеров.

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

Целесообразно выделить три основных параметра организации работы приложе­ний в сети. К ним относятся:

□  способ разделения приложения на части, выполняющиеся на разных компью­терах сети;

□  выделение специализированных серверов в сети, на которых выполняются не­которые общие для всех приложений функции;

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

 

Способ разделения приложений на части

 

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

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

□  логика представления данных на экране описывает правила и возможные сце­нарии взаимодействия пользователя с приложением: выбор из системы меню, выбор элемента из списка и т. п.;

□   прикладная логика — набор правил для принятия решений, вычислительные процедуры и операции;

□  логика данных — операции с данными, хранящимися в некоторой базе, кото­рые нужно выполнить для реализации прикладной логики;

□   внутренние операции базы данных — действия СУБД, вызываемые в ответ на выполнение запросов логики данных, такие как поиск записи по определен­ным признакам;

□   файловые операции — стандартные операции над файлами и файловой систе­мой, которые обычно являются функциями операционной системы.

На основе этой модели можно построить несколько схем распределения частей приложения между компьютерами сети.

 

Двухзвенные схемы

 

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

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

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

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

 

 

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

В схеме «файловый сервер» (рис. 9.1, б) на клиентской машине выполняются все части приложения, кроме файловых операций. В сети имеется достаточно мощ­ный компьютер, имеющий дисковую подсистему большого объема, который хра­нит файлы, доступ к которым необходим большому числу пользователей. Этот компьютер играет роль файлового сервера, представляя собой централизованное хранилище данных, находящихся в разделяемом доступе. Распределенное при­ложение в этой схеме мало отличается от полностью локального приложения. Единственным отличием является обращение к удаленным файлам вместо ло­кальных. Для того чтобы в этой схеме можно было использовать локальные при­ложения, в сетевые операционные системы ввели такой компонент сетевой фай­ловой службы, как редиректор, который перехватывает обращения к удаленным файлам (с помощью специальной нотации для сетевых имен, такой, например, как //server1/doc/file1.txt) и направляет запросы в сеть, освобождая приложение от необходимости явно задействовать сетевые системные вызовы.

Файловый сервер представляет собой компонент наиболее популярной сетевой службы — сетевой файловой системы, которая лежит в основе многих распре­деленных приложений и некоторых других сетевых служб. Первые сетевые ОС (NetWare компании Novell, IBM PC LAN Program, Microsoft MS-Net) обычно поддерживали две сетевые службы — файловую службу и службу печати, остав­ляя реализацию остальных функций разработчикам распределенных приложе­ний.

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

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

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

внутренних операций базы данных и файловых операций (рис. 9.1, в). Клиент­ский компьютер при этом выполняет все функции, специфические для данного приложения, а сервер — функции, реализация которых не зависит от специфи­ки приложения, из-за чего эти функции могут быть оформлены в виде сетевых служб. Поскольку функции управления базами данных нужны далеко не всем приложениям, то в отличие от файловой системы они чаще всего не реализуются в виде службы сетевой ОС, а являются независимой распределенной приклад­ной системой. Система управления базами данных (СУБД) является одним из наиболее часто применяемых в сетях распределенных приложений. Не все СУБД являются распределенными, но практически все мощные СУБД, позволяющие поддерживать большое число сетевых пользователей, построены в соответствии с описанной моделью клиент-сервер. Сам термин «клиент-сервер» справедлив для любой двухзвенной схемы распределения функций, но исторически он ока­зался наиболее тесно связанным со схемой, в которой сервер выполняет функ­ции по управлению базами данных (и, конечно, файлами, в которых хранятся эти базы) и часто используется как синоним этой схемы.

 

Трехзвенные схемы

 

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

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

Сервер баз данных, как и в двухзвенной модели, выполняет функции двух по­следних слоев — операции внутри базы данных и файловые операции. Приме­ром такой схемы может служить неоднородная архитектура, включающая кли­ентские компьютеры под управлением Windows 95/98, сервер приложений с монитором транзакций TUXEDO в среде Solaris на компьютере компании Sun Microsystems и сервер баз данных Oracle в среде Windows 2000 на компьютере компании Compaq.

 

                       

 

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

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

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

В крупных сетях для связи клиентских и серверных частей приложений также используется и ряд других средств, относящихся к классу middleware, в том числе:

□  средства асинхронной обработки сообщений (message-oriented middleware, MOM);

□  средства удаленного вызова процедур (Remote Procedure Call, RPC);

□  брокеры запроса объектов (Object Request Broker, ORB), которые находят объекты, хранящиеся на различных компьютерах, и помогают их использо­вать в одном приложении или документе.

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

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

 

Механизм передачи сообщений в распределенных системах

 

Единственным по-настоящему важным отличием распределенных систем от цен­трализованных является способ взаимодействия между процессами. Принципиально межпроцессное взаимодействие может осуществляться одним из двух спо­собов:

□   с помощью совместного использования одних и тех же данных (разделяемая память);

□   путем передачи друг другу данных в виде сообщений.

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

В распределенных системах не существует памяти, непосредственно доступной процессам, работающим на разных компьютерах, поэтому взаимодействие про­цессов (как находящихся в пользовательской фазе, так и в системной, то есть выполняющих код операционной системы) может осуществляться только путем передачи сообщений через сеть. Как было показано в разделе «Сетевые службы и сетевые сервисы» главы 2 «Назначение и функции операционной системы», на основе механизма передачи сообщений работают все сетевые службы, предостав­ляющие пользователям сети разнообразные услуги — доступ к удаленным файлам, принтерам, почтовым ящикам и т. п. В сообщениях переносятся запросы от кли­ентов некоторой службы к соответствующим серверам — например, запрос на просмотр содержимого определенного каталога файловой системы, расположен­ной на сетевом сервере. Сервер возвращает ответ — набор имен файлов и подка­талогов, входящих в данный каталог, также помещая его в сообщение и отправ­ляя его по сети клиенту.

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

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

найти нужный процесс в сети, состоящей из большого количества компью­теров.

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

□   Структурированная информация, состоящая в общем случае из нескольких частей: поля типа данных, поля длины данных и поля значения данных (то есть собственно данных). Поле типа данных определяет, например, что дан­ные являются целым числом или же представляют собой строку символов (это поле может также содержать признак нерезидентности данных, то есть указатель на данные, которые хранятся где-то вне данного сообщения). Вто­рое поле определяет длину передаваемых в сообщении данных (обычно в бай­тах), то есть размер следующего поля сообщения. Сообщение может включать несколько элементов, состоящих из описанных трех полей. В тех случаях, ко­гда сообщение всегда переносит данные одного и того же типа, поле типа может быть опущено. То же касается поля длины данных — для тех типов со­общений, которые переносят данные фиксированного формата, но такая си­туация характерна только для протоколов низкого уровня (например, ATM, имеющего фиксированный размер поля данных в 48 байт).

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

Транспортная подсистема сетевой ОС имеет обычно сложную структуру, отра­жающую структуру семиуровневой модели взаимодействия открытых систем (Open System Interconnection, OSI). Представление сложной задачи сетевого взаимодействия компьютеров в виде иерархии нескольких частных задач позво­ляет организовать это взаимодействие максимально гибким образом. В то же время каждый уровень модели OSI экранирует особенности лежащих под ним уровней от вышележащих уровней, что делает средства взаимодействия компью­теров все более универсальными по мере продвижения вверх по уровням. Таким образом, в процесс выполнения примитивов send и receive вовлекаются средства всех нижележащих коммуникационных протоколов (рис. 9.3).

 

 

Несмотря на концептуальную простоту примитивов send и receive, существуют различные варианты их реализации, от правильного выбора которых зависит эф­фективность работы сети. В частности, эффективность зависит от способа зада­ния адреса получателя. Не менее важны при реализации примитивов передачи сообщений ответы и на другие вопросы. В сети всегда имеется один получатель или их может быть несколько? Требуется ли гарантированная доставка сообще­ний? Должен ли отправитель дождаться ответа на свое сообщение, прежде чем продолжать свою работу? Как отправитель, получатель и подсистема передачи сообщений должны реагировать на отказы узла или коммуникационного канала во время взаимодействия? Что нужно делать, если приемник не готов принять сообщение, нужно ли отбрасывать сообщение или сохранять его в буфере? А если сохранять, то как быть, если буфер уже заполнен? Разрешено ли приемнику из­менять порядок обработки сообщений в соответствии с их важностью? Ответы на подобные вопросы составляют семантику конкретного протокола, передачи сообщений.

 

Синхронизация

 

Центральным вопросом взаимодействия процессов в сети является способ их син­хронизации, который полностью определяется используемыми в операционной

системе коммуникационными примитивами. В этом отношении коммуникационные примитивы делятся на блокирующие {синхронные) и неблокирующие (асинхрон­ные), причем смысл данных терминов в целом соответствует смыслу аналогич­ных терминов, применяемых при описании системных вызовов (см. подраздел «Системные вызовы» раздела «Мультипрограммирование на основе прерываний» в главе 4 «Процессы и потоки») и операций ввода-вывода (см. подраздел «Под­держка синхронных и асинхронных операций ввода-вывода» раздела «Задачи ОС по управлению файлами и устройствами» в главе 7 «Ввод-вывод и файловая система»). В отличие от локальных системных вызовов (а именно такие систем­ные вызовы были рассмотрены в главах 4 и 7) при выполнении коммуникацион­ных примитивов завершение запрошенной операции в общем случае зависит не только от некоторой работы локальной ОС, но и от работы удаленной ОС.

Коммуникационные примитивы могут быть оформлены в операционной системе двумя способами: как внутренние процедуры ядра ОС (в этом случае ими могут использоваться только модули ОС) или как системные вызовы (доступные в этом случае процессам в пользовательском режиме).

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

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

□   Опрос (polling). Этот метод предусматривает наличие еще одного базового при­митива test (проверить), с помощью которого процесс-получатель может ана­лизировать состояние буфера.

□   Прерывание (interrupt). Этот метод использует программное прерывание для уведомления процесса-получателя о том, что сообщение помещено в буфер. Хотя такой метод и очень эффективен (он исключает многократные проверки состояния буфера), у него имеется существенный недостаток — усложненное программирование, связанное с прерываниями пользовательского уровня, то есть прерываниями, по которым вызываются процедуры пользовательского режима (например, вызов процедур АРС в ОС Windows NT по завершении операции ввода-вывода, рассмотренный в главе 8 «Дополнительные возмож­ности файловых систем»).

При использовании блокирующего примитива send может возникнуть ситуация, когда процесс-отправитель блокируется навсегда. Например, если процесс полу­чатель потерпел крах или же отправленное сообщение было утеряно из-за сетевой ошибки. Чтобы предотвратить такую ситуацию, блокирующий примитив send часто использует механизм тайм-аута. То есть определяется интервал времени, после которого операция send завершается со статусом «ошибка». Механизм тайм-аута может использоваться также блокирующим примитивом receive для предотвращения блокировки процесса-получателя на неопределен­ное время, когда процесс-отправитель потерпел крах или сообщение было поте­ряно вследствие сетевой ошибки.

Если при взаимодействии двух процессов оба примитива — send и receive — явля­ются блокирующими, говорят, что процессы взаимодействуют по сети синхронно (рис. 9.4), в противном случае взаимодействие считается асинхронным (рис. 9.5).

 

 

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

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

 

 

 

 

Буферизация в примитивах передачи сообщений

 

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

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

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

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

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

Для всех вариантов обмена сообщениями с помощью асинхронных примитивов необходима буферизация. Поскольку при асинхронном обмене процесс-отправи­тель может посылать сообщение всегда, когда ему это требуется, не дожидаясь подтверждения от процесса-получателя, для исключения потерь сообщений тре­буется буфер неограниченной длины. Так как буфер в реальной системе всегда имеет ограниченный размер, то могут возникать ситуации с переполнением бу­фера и на них нужно каким-то образом реагировать. Для уменьшения вероят­ности потерь сообщений степень асинхронности процесса обмена сообщениями обычно ограничивается механизмом управления потоком сообщений. Управле­ние потоком заключается в том, что при заполнении буфера на принимающей стороне до некоторого опасного порога процесс-передатчик блокируется до тех пор, пока процесс-приемник не обработает часть принятых сообщений и не раз­грузит буфер до безопасной величины. Конечно, вероятность потерь сообщений из-за переполнения буфера все равно сохраняется, например из-за того, что слу­жебное сообщение о необходимости приостановки передачи сообщений может быть потеряно сетью. Асинхронный обмен с управлением потоком — это наибо­лее сложный способ организации обмена сообщениями, так как для повышения эффективности, то есть максимизации скорости обмена и минимизации потерь, он требует применения сложных алгоритмов приостановки и возобновления про­цесс передачи, например таких, которые применяются в протоколе TCP.

Обычно операционная система предоставляет для прикладных процессов специ­альный примитив для создания буферов сообщений. Такого рода примитив, на­зовем его, например, create_buffer (создать буфер), процесс должен использовать перед тем, как отправлять или получать сообщения с помощью примитивов send и receive. При создании буфера его размер может либо устанавливаться по умол­чанию, либо выбираться прикладным процессом. Часто такой буфер носит на­звание порта (port), или почтового ящика (mailbox).

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

узнать, какому процессу адресовано вновь поступившее сообщение, если имеет­ся несколько активных процессов? И как оно узнает, куда его скопировать? Один из вариантов — просто отказаться от сообщения в расчете на то, что отпра­витель после тайм-аута передаст сообщение повторно и к этому времени полу­чатель уже создаст буфер. Этот подход не сложен в реализации, но, к сожале­нию, отправитель (или скорее ядро его компьютера) может сделать несколько таких безуспешных попыток. Еще хуже то, что после достаточно большого числа безуспешных попыток ядро отправителя может сделать неправильный вывод об аварии на машине получателя или о неправильности его адреса. Второй подход к этой проблеме заключается в том, чтобы хранить хотя бы не­которое время поступающие сообщения в ядре получателя в расчете на то, что вскоре будет выполнен соответствующий примитив createbuffer. Каждый раз, когда поступает такое «неожидаемое» сообщение, включается таймер. Если за­данный временной интервал истекает раньше, чем происходит создание буфера, то сообщение теряется.

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

 

Способы адресации

 

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

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

Наибольшее распространение получила система адресации, в которой адрес со­стоит из двух частей, определяющих компьютер и процесс, которому предна­значено сообщение, то есть адрес имеет вид пары числовых идентификаторов: machine_id@loca1_id. В качестве идентификатора компьютера machinejd наиболее употребительным на сегодня является использование IP-адреса, который пред­ставляет собой 32-битовое число, условно записываемое в виде четырех деся­тичных чисел, разделенных точками, например 185.23.123.26. Идентификатором компьютера может служить любой другой тип адреса узла, который воспринимается транспортными средствами сети, например IPX-адрес, ATM-адрес или уже упоминавшийся аппаратный адрес сетевого адаптера, если система передачи сообщений ОС работает только в пределах одной локальной сети.

Для адресации процесса в этом способе применяется числовой идентификатор local_id, имеющий уникальное в"пределах узла machine_id значение. Этот иден­тификатор может однозначно указывать на конкретный процесс, работающий на данном компьютере, то есть являться идентификатором типа process_id. Однако существует и другой подход, функциональный, при котором используется адрес службы, которой пересылается сообщение, при этом идентификатор принимает вид service_id. Последний вариант более удобен для отправителя, так как служ­бы, поддерживаемые сетевыми операционными системами, представляют собой достаточно устойчивый набор (в него входят, как правило, наиболее популяр­ные службы FTP, SMB, NFS, SMTP, HTTP, SNMP) и этим службам можно дать вполне определенные адреса, заранее известные всем отправителям. Такие адре­са называют «хорошо известными» (well-known). Примером хорошо известных адресов служб являются номера портов в протоколах TCP и UDP. Отправитель всегда знает, что, посылая с помощью этих протоколов сообщение на порт 21 не­которого компьютера, он посылает его службе FTP, то есть службе передачи файлов. При этом отправителя не интересует, какой именно процесс (с каким локальным идентификатором) реализует в настоящий момент времени услуги FTP на данном компьютере.

Ввиду повсеместного применения стека протоколов TCP/IP номера портов яв­ляются на сегодня наиболее популярными адресами служб в системах обмена сообщениями сетевых ОС. Порт TCP/UDP является не только абстрактным ад­ресом службы, но и представляет собой нечто более конкретное — для каждого порта операционная система поддерживает буфер в системной памяти, куда по­мещаются отправляемые и получаемые сообщения, адресуемые данному порту. Порт задается в протоколах TCP/UDP двухбайтным адресом, поэтому ОС мо­жет поддерживать до 65 535 портов. Кроме хорошо известных номеров портов, которым отводится диапазон от 1 до 1023, существуют и динамически исполь­зуемые порты со старшими номерами. Значения этих портов не закрепляются за определенными службами, поэтому они часто дополняют хорошо известные порты для обмена в рамках обслуживания некоторой службы сообщениями спе­цифического назначения. Например, клиент FTP всегда начинает взаимодейст­вие с сервером FTP отправкой сообщения на порт 21, а после установления сеан­са обмен данными между клиентом и сервером выполняется уже по порту, номер которого динамически выбирается в процессе установления сеанса.

Описанная схема адресации типа «машина-процесс» или «машина-служба» хо­рошо зарекомендовала себя, работая уже на протяжении многих лет в Интерне­те, а также в корпоративных сетях IP и IPX (в этих сетях также используется адресация службы, а не процесса). Однако эта схема имеет один существенный недостаток — она не гибка и не прозрачна, так как пользователь должен явно указывать адрес машины-получателя. В этом случае, если в один прекрасный день машина, на которой работает некоторая служба, отказывает, то программа, в ко­торой все обращения к данной службе выполняются по жестко заданному адресу, не сможет использовать аналогичную службу, установленную на другой ма­шине.

Основным способом повышения степени прозрачности адресации является исполь­зование символьных имен вместо числовых. Примером такого подхода является характерная для сегодняшнего Интернета нотация URL (Universal Resource Loca­tor, универсальный указатель ресурса), в соответствии с которой адрес состоит из символьного имени узла и символьного имени службы. Например, если в сообщения указан адрес ftp://arc.bestcompany.ru/, то это означает, что оно отправ­лено службе ftp, работающей на компьютере arc.bestcompany.ru.

Использование символьных имен требует создания в сети службы оперативного отображения символьных имен на числовые идентификаторы, поскольку имен­но в таком виде адреса распознаются сетевым оборудованием. Применение сим­вольного имени позволяет разорвать жесткую связь адреса с одним-единственным компьютером, так как символьное имя перед отправкой сообщения в сеть заменяется на числовое, например на IP-адрес. Этап замены позволяет сопоста­вить с символьным именем различные числовые адреса и выбрать тот компью­тер, который в данный момент в наибольшей степени подходит для выполнения запроса, содержащегося в сообщении. Например, отправляя запрос на получение услуг службы Web от компании Microsoft по адресу http://www.microsoft.com/, вы точно не знаете, какой из нескольких серверов этой компании, предоставляющих данный вид услуг и обслуживающих один и тот же символьный адрес, ответит вам.

Для замены символьных адресов на числовые применяются две схемы: широко­вещание и централизованная служба имен. Широковещание удобно в локальных сетях, в которых все сетевые технологии нижнего уровня, такие как Ethernet, Token Ring, FDDI, поддерживают широковещательные адреса в пределах всей сети, а пропускной способности каналов связи достаточно для обслуживания та­ких запросов для сравнительного небольшого количества клиентов и серверов. На широковещании были построены все службы ОС NetWare (до версии 4), став­шие в свое время эталоном прозрачности для пользователей. В этой схеме сервер периодически широковещательно рассылает по сети сообщения о соответствии числовым адресам его имени и имен служб, которые он поддерживает. Клиент также может сделать широковещательный запрос о наличии в сети сервера, под­держивающего определенную службу, и если такой сервер в сети есть, то он от­ветит на запрос своим числовым адресом. После обмена подобными сообщения­ми пользователь должен явно указать в своем запросе имя сервера, к ресурсам которого он обращается, а клиентская ОС заменит это имя на числовой адрес в соответствии с информацией, широковещательно распространенной сервером.

Однако широковещательный механизм разрешения адресов плохо работает в территориальных сетях, так как наличие большого числа клиентов и серверов, а также использование менее скоростных по сравнению с локальными сетями каналов делают широковещательный трафик слишком интенсивным, практиче­ски не оставляющим пропускной способности для передачи пользовательских данных. В территориальных сетях для разрешения символьных имен компьюте­ров применяется другой подход, основанный на специализированных серверах, хранящих базу данных соответствия между символьными именами и числовыми адресами. Эти серверы образуют распределенную службу имен, обрабатывающую запросы многочисленных клиентов. Хорошо известным примером такой службы является служба доменных имен Интернета (Domain Name Service, DNS). Эта служба позволяет обрабатывать в реальном масштабе времени многочисленные запросы пользователей Интернета, обращающихся к ресурсам серверов по составным име­нам, таким как http://www.microsoft.com/ или http://www.gazeta.ru/. Другим при­мером может служить служба каталогов (NetWare Directory Sevices, NDS) ком­пании Novell, которая выполняет в крупной корпоративной сети более общие функции, предоставляя справочную информацию по любым сетевым ресурсам, в том числе и по соответствию символьных имен компьютеров их числовым ад­ресам.

Централизованная служба имен на сегодня считается наиболее перспективным средством повышения прозрачности услуг для пользователей сетей. С такой службой связывают и перспективы дальнейшего повышения прозрачности адре­сации сетевых ресурсов, когда имя ресурса будет полностью независимо от ком­пьютера, предоставляющего этот ресурс в общее пользование. Например, в службе NDS уже сегодня можно использовать такие имена, как имена томов, не указы­вая их точного расположения на том или ином компьютере. При перемещении тома с одного компьютера на другой изменение связи тома с компьютером реги­стрируется в базе службы NDS, так что все обращения к тому после его переме­щения разрешаются корректно путем замены имени адресом нового компьютера.

По пути применения централизованной службы-посредника между клиентами и ресурсами идут и разработчики распределенных приложений, например разра­ботчики технологии CORBA, в которой запросы к программным модулям при­ложений обрабатывает специальный элемент — брокер запросов.

Использование символьных имен вместо числовых адресов несколько повышает прозрачность, но не до той степени, которой хотелось бы достичь приверженцам идеи распределенных операционных систем, главным отличием которых от сете­вых ОС является именно полная прозрачность адресации разделяемых ресурсов. Тем не менее символьные имена — это значительный шаг вперед по сравнению с числовыми.

 

Надежные и ненадежные примитивы

 

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

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

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

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

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

В хорошей подсистеме обмена сообщения должны поддерживаться как ненадеж­ные примитивы, так и надежные. Это позволяет прикладному программисту ис­пользовать тот тип примитивов, который в наибольшей степени подходит для организации взаимодействия в той или иной ситуации. Например, для передачи данных большого объема, транспортируемых по сети в нескольких сообщениях (в сетях обычно существует ограничение на максимальный размер поля данных, из-за чего данные приходится пересылать в нескольких сообщениях), больше под­ходит надежный вид обмена с упорядочиванием сообщений. А вот для взаимо­действия типа «короткий запрос — короткий ответ» предпочтительны ненадеж­ные примитивы. Действительно, вероятность потери отдельного сообщения не так уж велика, а скорость такого обмена будет выше, чем при применении на­дежных примитивов, поскольку на установление необходимого в этом случае со­единения тратится дополнительное время.

Для реализации примитивов с различной степенью надежности передачи сооб­щений система обмена сообщениями ОС использует различные коммуникаци­онные протоколы. Так, если сообщения передаются через IP-сеть, то для надеж­ной передачи сообщений используется протокол транспортного уровня TCP, работающий с установлением соединений, обеспечивающий гарантированную и упорядоченную доставку и управляющий потоком данных при обмене. Если же надежность при передаче сообщений не требуется, то будет использован прото­кол UDP, обеспечивающий быструю доставку небольших сообщений без всяких гарантий. Аналогично при работе через сети Novell для надежной доставки сооб­щений используется протокол SPX, а для дейтаграммной — IPX. В стеке OSI су­ществует один транспортный протокол, но он поддерживает несколько режимов, отличающихся степенью надежности.

 

Механизм Sockets ОС UNIX

 

Механизм сокетов (sockets) впервые появился в версии 4.3 BSD UNIX (Berkeley Software Distribution UNIX — ветвь UNIX, начавшая развиваться в калифорний­ском университете Беркли). Позже он превратился в одну из самых популярных систем сетевого обмена сообщениями. Сегодня этот механизм реализован во мно­гих операционных системах, иногда его по-прежнему называют Berkeley Sockets, отдавая дань уважения его создателям, хотя существует большое количество его реализаций как для различных ОС семейства UNIX, так и для других ОС, на­пример для ОС семейства Windows, где он носит название Windows Sockets (WinSock).

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

□   Независимость от нижележащих сетевых протоколов и технологий. Для это­го используется понятие коммуникационный домен (communication domain). Коммуникационный домен обладает некоторым набором коммуникационных свойств, определяющих способ именования сетевых узлов и ресурсов, харак­теристики сетевых соединений (надежные, дейтаграммные, упорядоченные), способы синхронизации процессов и т. п. Одним из наиболее популярных до­менов является домен Интернета с протоколами стека TCP/IP.

□   Использование абстрактной конечной точки соединения, получившей на­звание сокет (socket — гнездо). Сокет — это точка, через которую сообщения уходят в сеть или принимаются из сети. Сетевое соединение между двумя процессами осуществляется через пару сокетов. Каждый процесс пользуется своим сокетом, при этом сокеты могут находится как на разных компьютерах, так и на одном (в этом случае сетевое межпроцессное взаимодействие сводит­ся к локальному).

□   Сокет может иметь как высокоуровневое символьное имя (адрес), так и низ­коуровневое, отражающее специфику адресации определенного коммуника­ционного домена. Например, в домене Интернета низкоуровневое имя пред­ставлено парой (IP-адрес, порт).

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

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

 

Создание сокета:

 

s = socket(domain, type, protocol)

 

Процесс должен создать сокет перед началом его использования. Системный вызов socket создает новый сокет с параметрами, определяющими коммуни­кационный домен (domain), тип соединения, поддерживаемого сокетом (type), и транспортный протокол (например, TCP или UDP), который будет поддер­живать это соединение. Если транспортный протокол не задан, то система сама выбирает протокол, соответствующий типу сокета. Указание домена опреде­ляет возможные значения остальных двух параметров. Системный вызов socket возвращает дескриптор созданного сокета, который используется как идентификатор сокета в последующих операциях.

 

Связывание сокета с адресом:

 

Bind (s, addr. addrlen)

 

Системный вызов bind связывает созданный сокет с его высокоуровневым име­нем либо с низкоуровневым адресом. Адрес addr относится к тому узлу, на котором расположен сокет. Для низкоуровневого адреса домена Интернета адресом будет пара (IP-адрес, порт). Третий параметр делает адрес доменно-независимым, позволяя задавать адреса различных типов, в том числе сим­вольные. Связывать сокет с адресом необходимо только в том случае, если на данный сокет будут приниматься сообщения.

 

Запрос на установление соединения с удаленным сокетом:

 

Connect (s, server_addr, server_addrlen)

 

Системный вызов connect используется только в том случае, если предполага­ется передавать сообщения в потоковом режиме, который требует установ­ления соединения. Процедура установления несимметрична: один процесс (процесс-сервер) ждет запроса на установление соединения, а второй (про­цесс-клиент) — инициирует соединение, посылая такой запрос. Системный вызов connect является запросом клиента на установление соединения с сер­вером. Второй и третий аргументы вызова указывают адрес сокета сервера, с которым устанавливается соединение. После установления соединения сооб­щения по нему могут передаваться в дуплексном режиме, то есть в любом на­правлении. Системный вызов write, используемый для передачи сообщений в рамках установленного соединения, не требует указания адреса сокета полу­чателя, так как локальный сокет, через который сообщение отправляется, уже соединен с определенным удаленным сокетом. Способ, с помощью которого клиенты узнают адрес сокета сервера, не стандартизован.

 

Ожидание запроса на установление соединения:

 

listen (s. backlog)

 

Системный вызов 1isten используется для организации режима ожидания сер­вером запросов на установление соединения. Система обмена сообщениями после отработки данного системного вызова будет принимать запросы на установление, имеющие адрес сокета s, и передавать их на обработку другому системному вызову — accept, который решает, принимать их или отвергать.

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

 

   Принятие запроса на установление соединения:

 

snew = accept(s. client_addr, client-addrlen)                      

 

Системный вызов accept используется сервером для приема запроса на уста­новление соединения, поступившего от системного вызова listeh через сокет s от клиента с адресом client_addr (если этот аргумент опущен, то принимает­ся запрос от любого клиента). При этом создается новый сокет snew, через ко­торый и устанавливается соединение с данным клиентом. Таким образом, сокет s используется сервером для приема запросов на установление соедине­ния от клиентов, а сокеты snew — для обмена сообщениями с клиентами по индивидуальным соединениям.

 

   Отправка сообщения по установленному соединению:

 

write(s, message, msg_len)

 

Сообщение длиной msgl _en, хранящееся в буфере message, отправляется полу­чателю, с которым предварительно соединен сокет s.

 

  Прием сообщения по установленному соединению:

 

nbytes =read(snew, buffer, amount):

 

Сообщение, поступившее через сокет snew, с которым предварительно соеди­нен отправитель, принимается в буфер buffer размером,amount. Если сообще­ний нет, то процесс-получатель блокируется.

 

   Отправка сообщения без установления соединения:

 

sendto(s. message. receiver_address)

 

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

 

   Прием сообщения без установления соединения:

 

amount = recvfrom(s. message. sender_address)

 

Аналогично предыдущему вызову при приеме без установленного соедине­ния в каждом вызове recvfrom указывается адрес сокета отправителя, от кото­рого нужно принять сообщение. Если сообщений нет, то процесс-получатель блокируется.

Рассмотрим использование системных вызовов механизма сокетов для организа­ции обмена сообщениями между двумя узлами.

Для обмена короткими сообщениями, не требующими надежной доставки и упо­рядоченности, целесообразно воспользоваться системными вызовами, не тре­бующими установления соединения. Фрагмент программы процесса-отправите­ля может выглядеть так:

 

s = socket(AFJNET. SOCK_DGRAM.O):                    

     

 

Константа AF_INET определяет, что обмен ведется в коммуникационном домене Интернета, а константа SOCK_DGRAM задает дейтаграммный режим обмена без уста­новления соединения. Выбор транспортного протокола оставлен на усмотрение системы.

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

Для процесса-клиента:              

   

 

Вызов удаленных процедур

 

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

 

Концепция удаленного вызова процедур

 

Идея вызова удаленных процедур состоит в расширении хорошо известного и понятного механизма передачи управления и данных внутри программы, выпол­няющейся на одной машине, на передачу управления и данных через сеть. Сред­ства удаленного вызова процедур предназначены для облегчения организации распределенных вычислений. Впервые механизм RPC реализовала компания Sun Microsystems, и он хорошо соответствует девизу «Сеть — это компьютер», взято­му этой компанией на вооружение, так как приближает сетевое программирование к локальному. Наибольшая эффективность RPC достигается в тех приложениях, в которых существует интерактивная связь между удаленными компонентами с небольшим временем ответов и относительно малым количеством передаваемых данных. Такие приложения называются RPC-ориентированными.

Характерными чертами вызова локальных процедур являются:

□  асимметричность — одна из взаимодействующих сторон является инициато­ром взаимодействия;

□   синхронность — выполнение вызывающей процедуры блокируется с момента выдачи запроса и возобновляется только после возврата из вызываемой про­цедуры.                                   Реализация удаленных вызовов существенно сложнее реализации вызовов локаль­ных процедур. Начнем с того, что поскольку вызывающая и вызываемая процеду­ры выполняются на разных машинах, то они имеют разные адресные простран­ства и это создает проблемы при передаче параметров и результатов, особенно если машины и их операционные системы не идентичны. Так как RPC не может рассчитывать на разделяемую память, это означает, что параметры RPC не долж­ны содержать указателей на ячейки памяти и что значения параметров должны как-то копироваться с одного компьютера на другой.

Следующим отличием RPC от локального вызова является то, что он обязатель­но использует нижележащую систему обмена сообщениями, однако это не долж­но быть явно видно ни в определении процедур, ни в самих процедурах. Удален­ность вносит дополнительные проблемы. Выполнение вызывающей программы и вызываемой локальной процедуры в одной машине реализуется в рамках еди­ного процесса. Но в реализации RPC участвуют как минимум два процесса — по одному в каждой машине. В случае если один из них аварийно завершится, мо­гут возникнуть следующие ситуации:   

□ при аварии вызывающей процедуры, удаленно вызванные процедуры стано­вятся «осиротевшими»;

□ при аварийном завершении удаленных процедур становятся «обездоленными родителями» вызывающие процедуры, которые будут безрезультатно ожи­дать ответа от удаленных процедур.

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

Рассмотрим, каким образом технология RPC, лежащая в основе многих распре­деленных операционных систем, решает эти проблемы.

Чтобы понять работу RPC, рассмотрим сначала выполнение вызова локальной процедуры в автономном компьютере. Пусть это, например, будет процедура за­писи данных в файл:

 

m - my_write(fd.buf.length):

 

Здесь fd — дескриптор файла, целое число, buf — указатель на массив символов, length — длина массива, целое число.

Чтобы осуществить вызов, вызывающая процедура помещает указанные пара­метры в стек в обратном порядке и передает управление вызываемой процедуре my_write. Эта пользовательская процедура после некоторых манипуляций с дан­ными символьного массива buf выполняет системный вызов write для записи дан­ных в файл, передавая ему параметры тем же способом, то есть помещая их в стек (при реализации системного вызова они копируются в стек системы, а при возврате из него результат помещается в пользовательский стек). После того как процедура my_write выполнена, она помещает возвращаемое значение m в регистр, перемещает адрес возврата и возвращает управление вызывающей процедуре, кото­рая выбирает параметры из стека, возвращая его в исходное состояние. Заметим, что в языке С параметры могут вызываться по ссылке (by name), представляю­щей собой адрес глобальной области памяти, в которой хранится параметр, или по значению (by value), в этом случае параметр копируется из исходной области памяти в локальную память процедуры, располагаемую обычно в стековом сег­менте. В первом случае вызываемая процедура работает с оригинальными значе­ниями параметров и их изменения сразу же видны вызывающей процедуре. Во втором случае вызываемая процедура работает с копиями значений параметров, и их изменения никак не влияют на значение оригиналов этих переменных в вы­зывающей процедуре. Эти обстоятельства весьма существенны для RPC.

Решение о том, какой механизм передачи параметров использовать, принима­ется разработчиками языка. Иногда это зависит от типа передаваемых данных. В языке С, например, целые и другие скалярные данные всегда передаются по значению, а массивы — по ссылке.

Рисунок 9.6 иллюстрирует передачу параметров вызываемой процедуре: стек до выполнения вызова write (а), стек во время выполнения процедуры (б), стек"по­сле возврата в вызывающую программу (в).

                       

 

 


                                  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

Механизм RPC достигает прозрачности следующим образом. Когда вызываемая процедура действительно является удаленной, в библиотеку процедур вместо локальной реализации оригинального кода процедуры помещается другая вер­сия процедуры, называемая клиентским стабом (stub — заглушка). На удаленный компьютер, который выполняет роль сервера процедур, помещается оригиналь­ный код вызываемой процедуры, а также еще один стаб, называемый серверным стабом. Назначение клиентского и серверного стабов - организовать передачу параметров вызываемой процедуры и возврат значения процедуры через сеть, при этом код оригинальной процедуры, помещенной на сервер, должен быть пол­ностью сохранен. Стабы используют для передачи данных через сеть средства подсистемы обмена сообщениями, то есть существующие в ОС примитивы send и receive. Иногда в подсистеме обмена сообщениями выделяется программный модуль, организующий связь стабов с примитивами передачи сообщений, назы­ваемый модулем RPCRuntime.

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

 

Эта операция называется операцией упаковки параметров. После этого клиент­ский стаб обращается к примитиву send для передачи этого сообщения удаленно­му компьютеру, на который помещена реализация оригинальной процедуры. По­лучив из сети сообщение, ядро ОС удаленного компьютера вызывает серверный стаб, который извлекает из сообщения параметры и вызывает обычным образом оригинальную процедуру. Для получения сообщения серверный стаб должен предварительно вызвать примитив receive, чтобы ядро знало, для кого пришло сообщение. Серверный стаб распаковывает параметры вызова, имеющиеся в со­общении, и обычным образом вызывает оригинальную процедуру, передавая ей параметры через стек. После окончания работы процедуры серверный стаб упа­ковывает результат ее работы в новое сообщение и с помощью примитива send передает сообщение по сети клиенткому стабу, а тот возвращает обычным обра­зом результат и управление вызывающей процедуре. Ни вызывающая процеду­ра, ни оригинальная вызываемая процедура не изменились оттого, что они ста­ли работать на разных компьютерах.

 

Генерация стабов

 

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

Автоматический способ основан на применении специального языка определе­ния интерфейса (Interface Definition Language, IDL). С помощью этого языка про­граммист описывает интерфейс между клиентом и сервером RPC. Описание включает список имен процедур, выполнение которых клиент может запросить у сервера, а также список типов аргументов и результатов этих процедур. Инфор­мация, содержащаяся в описании интерфейса, достаточна для выполнения стабами проверки типов аргументов и генерации вызывающей последовательности. Кроме того, описание интерфейса содержит некоторую дополнительную инфор­мацию, полезную для оптимизации взаимодействия стабов, например каждый аргумент помечается как входной, выходной или играющий и ту, и другую роли (входной аргумент передается от клиента серверу, а выходной — в обратном на­правлении). Интерфейс может включать также описание общих для клиента и сервера констант. Необходимо подчеркнуть, что обычно интерфейс RPС вклю­чает не одну, а некоторый набор процедур, выполняющих взаимосвязанные функ­ции, например функции доступа к файлам, функции удаленной печати и т. п. Поэтому при вызове удаленной процедуры обычно необходимо каким-то обра­зом задать нужный интерфейс, а также конкретную процедуру, поддерживаемую этим интерфейсом. Часто интерфейс также называют сервером RPC, например файловый сервер, сервер печати.

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

 

Формат сообщений RPC

 

Механизм RPC оперирует двумя типами сообщений: сообщениями-вызовами, с помощью которых клиент запрашивает у сервера выполнение определенной уда­ленной процедуры и передает ее аргументы; сообщениями-ответами, с помощью которых сервер возвращает результат работы удаленной процедуры клиенту.

С помощью этих сообщений реализуется протокол RPC, определяющий способ взаимодействия клиента с сервером. Протокол RPC обычно не зависит от транспортных протоколов, с помощью которых сообщения RPC доставляются по сети от клиента к серверу и обратно. При использования в сети стека протоколов TCP/IP это могут быть протоколы TCP или UDP, в локальных сетях часто ис­пользуется также NetBEUI/NetBIOS или IPX/SPX.

Типичный формат двух типов сообщений, используемых RPC, показан на рис. 9.8.

 

 

Тип сообщения позволяет отличить сообщения-вызовы от сообщений-ответов. Поле идентификатора удаленной процедуры в сообщении-вызове позволяет сер­веру понять, вызов какой процедуры запрашивает в сообщении клиент (проце­дуры идентифицируются не именами, а номерами, которые при автоматической генерации стабов присваивает им IDL-компилятор, а при ручной — программист). Поле аргументов имеет переменную длину, определяемую количеством и типом аргументов вызываемой процедуры. В поле идентификатора сообщения помеща­ется порядковый номер сообщения, который полезен для обнаружения фактов потерь сообщений или прихода дубликатов сообщений. Кроме того, этот номер позволяет клиенту правильно сопоставить полученный от сервера ответ со сво­им вызовом в том случае, когда ответы приходят не в том порядке, в котором по­сылались вызовы. Идентификатор клиента нужен серверу для того, чтобы знать, какому клиенту нужно отправить результат работы вызываемой процедуры. Это поле может также использоваться в процедурах аутентификации клиента, если эти процедуры предусмотрены протоколом RPC.

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

Для устойчивой работы серверов и клиентов RPC необходимо каким-то образом обрабатывать ситуации, связанные с потерями сообщений, которые происходят

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

 

Связывание клиента с сервером

 

Рассмотрим вопрос о том, как клиент узнает место расположения сервера, кото­рому необходимо послать сообщение-вызов. Процедура, устанавливающая соот­ветствие между клиентом и сервером RPC, носит название связывание (binding). Методы связывания, применяемые в различных реализациях RPC, отличаются:

□   способом задания сервера, с которым хотел бы быть связанным клиент;

□   способом обнаружения сетевого адреса (места расположения) требуемого сер­вера процессом связывания;

□   стадией, на которой происходит связывание.                                                  

Метод связывания тесно связан с принятым методом именования сервера. В наи­более простом случае имя или адрес сервера RPC задается в явной форме, в ка­честве аргумента клиентского стаба или программы-сервера, реализующей ин­терфейс определенного типа. Например, можно использовать в качестве такого аргумента IP-адрес компьютера, на котором работает некоторый RPC-сервер, и номер TCP/UDP порта, через который он принимает сообщения-вызовы своих процедур. Основной недостаток такого подхода — отсутствие гибкости и про­зрачности. При перемещении сервера или при существовании нескольких серве­ров клиентская программа не может автоматически выбрать новый сервер или тот сервер, который в данный момент наименее загружен. Тем не менее во мно­гих случаях такой способ вполне приемлем и ввиду своей простоты часто ис­пользуется на практике. Необходимый сервер часто выбирает пользователь, на­пример путем просмотра списка или графического представления имеющихся в сети разделяемых файловых систем (набор этих файловых систем может быть собран операционной системой клиентского компьютера за счет прослушивания широковещательных объявлений, которые периодически делают серверы). Кро­ме того, пользователь может задать имя требуемого сервера на основании зара­нее известной ему информации об адресе или имени сервера.

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

Динамическое связывание требует изменения способа именования сервера. Наи­более гибким является использование для этой цели имени RPC-интерфейса, со­стоящего из двух частей:

□  типа интерфейса;

□ экземпляра интерфейса.

Тип интерфейса определяет все характеристики интерфейса, кроме его место­расположения. Это те же характеристики, который имеются в описании для IDL-компилятора, например файловая служба определенной версии, включающая процедуры open, close, read, write, и т. п. Часть, описывающая экземпляр интер­фейса, должна точно задавать сетевой адрес сервера, который поддерживает дан­ный интерфейс. Если клиенту безразлично, какой сервер его будет обслуживать, то вторая часть имени интерфейса опускается.

Динамическое связывание иногда называют импортом/экспортом интерфейса: клиент импортирует интерфейс, а сервер его экспортирует.

В том случае, когда для клиента важен только тип интерфейса, процесс обнару­жения требуемого сервера в сети с экземпляром интерфейса определенного типа может быть построен двумя способами:

□  с использованием широковещания;

□  с использованием централизованного агента связывания.

Эти два способа характерны для поиска сетевого ресурса любого типа по его име­ни, они уже рассматривались в общем виде в подразделе «Способы адресации» раздела «Механизм передачи сообщений в распределенных системах». Первый способ основан на широковещательном распространении по сети серверами RPC имени своего интерфейса, включая и адрес экземпляра. Применение этого спосо­ба позволяет автоматически балансировать нагрузку на несколько серверов, под­держивающий один и тот же интерфейс, — клиент просто выбирает первое из подходящих ему объявлений.

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

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

Агент связывания может работать в составе общей централизованной справоч­ной службы сети, такой как NDS, X.500 или LDAP (справочные службы более подробно рассматриваются в следующей главе).

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

Однако у динамического связывания имеются недостатки, например дополни­тельные накладные расходы (временные затраты) на экспорт и импорт интер­фейсов. Величина этих затрат может быть значительна, так как многие клиентские процессы существуют короткое время, а при каждом старте процесса процедура импорта интерфейса должна выполняться заново. Кроме того, в больших рас­пределенных системах может стать узким местом агент связывания, и тогда необходимо использовать распределенную систему агентов, что можно сделать стандартным способом, используя распределенную справочную службу (таким свойством обладают службы NDS, Х.500 и LDAP).

Необходимо отметить, что и в тех случаях, когда используется статическое свя­зывание, такая часть адреса, как порт сервера интерфейса (то есть идентифи­катор процесса, обслуживающего данный интерфейс), определяется клиентом динамически. Эту процедуру поддерживает специальный модуль RPCRuntime, называемый в ОС UNIX модулем отображения портов (portmapper), а в ОС се­мейства Windows локатором RPC (RPC Locator). Этот модуль работает на каждом сетевом узле, поддерживающем механизм RPC, и доступен по хорошо известному порту TCP/UDP. Каждый сервер RPC, обслуживающий определен­ный интерфейс, при старте обращается к такому модулю с запросом о выделении ему для работы номера порта из динамически распределяемой области (то есть с номером, большим 1023). Модуль отображения портов выделяет серверу некото­рый свободный номер порта и запоминает это отображение в своей таблице, свя­зывая порт с типом интерфейса, поддерживаемым сервером. Клиент RPC, выяс­нив каким-либо образом сетевой адрес узла, на котором имеется сервер RPC с нужным интерфейсом, предварительно соединяется с модулем отображения портов по хорошо известному порту и запрашивает номер порта искомого серве­ра. Получив ответ, клиент использует данный номер для отправки сообщений-вызовов удаленных процедур. Механизм очень похож на механизм, лежащий в основе работы агента связывания, но только область его действия ограничивает­ся портом одного компьютера.

 

Особенности реализации RPC на примере

систем Sun RPC и DCE RPC

 

Рассмотрим особенности реализации RPC на примере двух широко распростра­ненных систем удаленного вызова процедур: Sun RPC и DCE RPC. Система Sun

RPC является продуктом компании Sun Microsystems и работает во всех сетевых операционных системах этой компании — SunOS, Solaris, а система DCE RPC — это стандарт консорциума Open Software Foundation для распределенной вычис­лительной среды Distributed Computing Environment (DCE). Реализации DCE RPC доступны сегодня для многих сетевых ОС, кроме того, на основе стандарта DCE RPC разработана система Microsoft RPC, применяющаяся в популярных ОС семейства Windows. К сожалению, реализации Sun RPC и DCE RPC несо­вместимы друг с другом, более того, нет гарантий, что различные реализации RPC, в основе которых лежит стандарт DCE RPC, смогут совместно работать в гетеро­генной сети, так как стандарт DCE определяет только базовые свойства механизма удаленного вызова процедур, а каждая реализация добавляет к стандарту боль­шое количество собственных дополнительных функций.

 

Sun RPC

 

Система Sun RPC позволяет автоматически генерировать клиентский и серверный стабы в том случае, если интерфейс RPC описан на языке IDL, называемом RPC Language (RPCL). Язык RPCL является расширением языка Sun XDR (eXtemal Data Representation), который был разработан для системно-независимого представ­ления внешних данных в гетерогенной среде. XDR-представление данных по умол­чанию используется в Sun RPC при передаче аргументов и результатов между кли­ентом и сервером RPC.

Механизм Sun RPC обладает некоторыми достаточно жесткими ограничениями. Одним из них является ограничение на аргументы и результаты удаленных про­цедур — процедура может иметь только один аргумент и вырабатывать только один результат. Для преодоления этого ограничения в качестве аргумента и ре­зультата обычно используется структура данных.

Следующий пример описания интерфейса файловой службы иллюстрирует эту особенность:

 

 

 

 

 

Интерфейс однозначно идентифицируется номером программы (FILESERVICE2 = = 0x20000000) и номером версии (FILESERVICEVERS - 1), а процедуры внутри ин­терфейса — номерами процедур, READ — номером 1 и WRITE — номером 2.

Структура readargs позволяет передать процедуре READ три аргумента — имя фай­ла, позицию (смещение) в файле, с которой нужно начать чтение данных, и ко­личество считываемых байт. Структура Data позволяет вызывающей процедуре получить результат чтения в массиве buffer и узнать количество реально считан­ных байт с помощью переменной п. Аналогично используются структуры writeargs и Data в процедуре записи WRITE.

Результатом обработки описания интерфейса FILESERVICE2 RPCL-компилятором являются несколько файлов, в том числе файл, описывающий клиентские стабы, а также файл, описывающий серверные стабы. По умолчанию имена стабов процедур образуются путем преобразования имен процедур, указанных в описа­нии интерфейса, в нижний регистр, а также добавлением после нижнего подчер­кивания номера версии интерфейса, то есть стабы в нашем примере будут иметь имена read_l и write_l.

Механизм Sun RPC не поддерживает динамического связывания с сервером с помощью агента связывания. Клиент должен явно указывать сетевой адрес сер­вера, а для получения номера порта он должен обратиться к модулю отобра­жения портов, работающему на узле сервера. Для взаимодействия с этим моду­лем система Sun RPC предоставляет в распоряжение клиента системный вызов clnt_create, имеющий следующий синтаксис:

 

Client_handle = clnt_create (server_host_name. interface_name. interface_version. protocol)

 

Аргумент server_host_name представляет собой имя (символьное DNS-имя или IP-адрес узла, на котором работает сервер RPC), аргументы i nterface_name и i nterf ace_versi on задают номер и версию интерфейса, а аргумент protocol указы­вает на один из двух транспортных протоколов стека TCP/IP — TCP или UDP. Жесткая ориентация только на один стек коммуникационных протоколов, а имен­но стек TCP/IP, — еще одно ограничение Sun RPC. У компании Sun существует также и протокольно-независимая версия системы удаленного вызова процедур — TI-RPC (Transport-Independent RPC), но она менее распространена.

Вызов clnt_create возвращает указатель, который необходимо далее использовать вместо адреса RPC-сервера при последовательных обращениях к удаленным процедурам, обслуживаемым данным сервером. В процессе своего выполнения clnt_create создает сокет, который связывает с адресом сервера, включающим и неявно полученный от модуля отображения портов (службы portmapper) номер порта. В конце сеанса работы с сервером необходимо выполнить системный вы­зов clnt_destroy, который закрывает созданный сокет.

Еще одним ограничением Sun RPC является максимальный размер сообщения в 8 Кбайт при использовании протокола UDP (применение протокола TCP He на­кладывает таких ограничений в силу особенности его интерфейсах вышележа­щими протоколами, который позволяет передавать непрерывный поток байт в течение периода существования ТСР-соединения).

 

DCE RPC

 

Служба DCE RPC обладает рядом функциональных преимуществ по сравнению с Sun RPC. Она поддерживает динамическое связывание клиентов и серверов, для чего используется справочная служба среды DCE — служба Cell Directory Service (CDS). Каждый сервер RPC при старте регистрирует свой сетевой ад­рес и уникальный идентификатор интерфейса на сервере CDS. Кроме того, на каждом узле, поддерживающем RPC, работает процесс rpcd, который выполняет функции по отображению сервера RPC на подходящий локальный адрес процес­са (например, порт TCP/UDP, если сервер работает на компьютере, поддержи­вающем стек TCP/IP). Служба DCE RPC является транспортно-независимой, что позволяет ей работать на разных платформах и в различных сетях.

При описании интерфейса используется язык IDL. Процедуры DCE RPC могут иметь произвольное число аргументов, которые описываются как входные, вы­ходные или входные-выходные.