Дополнительные возможности файловых систем
Особая роль файловой системы, связанная с долговременным хранением информации, в том числе критически важных программ и данных пользователей и самой ОС, порождает повышенные требования к ее надежности и отказоустойчивости. Эти важные свойства обеспечиваются за счет применения восстанавливаемых файловых систем и отказоустойчивых дисковых массивов. Модели файла и файловых операций, применяемые первоначально к хранимым на дисках данным, оказались удобным средством работы с данными любой природы, поэтому со временем они нашли применение и в других областях, таких как управление устройствами ввода-вывода и обмен данными между процессами. С другой стороны, и на классическую файловую систему оказало влияние развитие других подсистем ОС, в частности подсистемы управления памятью, за счет средств которой стало возможным отображение файлов в оперативную память и работа с дисковыми данными как с обычными переменными.
Специальные файлы и аппаратные драйверы
Специальные файлы как универсальный интерфейс
Понятие «специальный файл» появилось в операционной системе 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.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 Locator, универсальный указатель ресурса), в соответствии с которой адрес состоит из символьного имени узла и символьного имени службы. Например, если в сообщения указан адрес 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 могут иметь произвольное число аргументов, которые описываются как входные, выходные или входные-выходные.