Реализация ввода-вывода в системе unix

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

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

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

Система ввода-вывода разделена на два основных компонента: обработку блочных специальных файлов и обработку символьных специальных файлов. Цель той части системы, которая занимается операциями ввода-вывода с блочными специальными файлами (например, дисковым вводом-выводом), заключается в минимизации количества операций переноса данных. Для достижения данной цели в системах UNIX между дисковыми драйверами и файловой системой помещается буферный кэш. Буферный кэш представляет собой таблицу в ядре, в которой хранятся тысячи недавно использованных блоков. Когда файловой системе требуется блок диска (например, блок i-узла, каталога или данных), сначала проверяется буферный кэш. Если нужный блок есть в кэше, он получается оттуда, при этом обращения к диску удается избежать. Буферный кэш значительно улучшает производительность системы. Если же блока нет в буферном кэше, он считывается с диска в кэш, а оттуда копируется туда, куда нужно. Поскольку в буферном кэше есть место только для фиксированного количества блоков, требуется некий алгоритм управления кэшем. Обычно блоки в кэше организуются в связный список. При каждом обращении к блоку он перемещается в начало списка. Если в кэше не хватает места для нового блока, то из него удаляется самый старый блок, находящийся в конце списка. Буферный кэш поддерживает не только операцию чтения с диска, но также и запись на диск. Когда программа пишет блок, этот блок не попадает напрямую на диск, а отправляется в кэш. Только когда кэш наполняется, модифицированные блоки кэша сохраняются на диске. Чтобы модифицированные блоки не хранились в кэше слишком дол-го, их принудительная выгрузка на диск производится каждые 30 с.

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

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

Потоки данных в UNIX

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

Первое решение, реализованное в системе BSD, основано на структурах данных, присутствующих в классических системах UNIX и называемых С-списками. Каждый С-список представляет собой блок размером до 64 символов плюс счетчик и указатель на следующий блок. Символы, поступающие с терминала или любого другого символьного устройства, буферизируются в цепочках таких блоков. Когда пользовательский процесс считывает данные из /dev/tty (то есть из стандартного входного потока), символы не передаются процессу напрямую из С-списков. Вместо этого они пропускаются через процедуру, расположенную в ядре и называемую дисциплиной линии связи. Дисциплина линии связи работает как фильтр, принимая необработанный поток символов от драйвера терминала, обрабатывая его и формируя то, что называется обработанным символьным потоком. В обработанном потоке выполняются операции локального строкового редактирования (например, удаляются отмененные пользователем символы и строки), а также выполняются другие специальные операции обработки. Обработанный поток передается процессу. Однако если процесс желает воспринимать каждый символ, введенный пользователем, он может принимать необработанный поток, минуя дисциплину линии связи. Вывод работает аналогично, заменяя табуляторы пробелами, добавляя символы-заполнители и т. д. Как и входной поток, выходной символьный поток может быть пропущен через дисциплину линии связи (обработанный режим) или миновать ее (необработанный режим). Необработанный режим особенно полезен при отправке двоичных данных на другие машины по линии последовательной передачи или для графических интерфейсов пользователя. Здесь не требуется никакого преобразования.

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

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

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

7.5. Файловые системы UNIX

Основные понятия

Файл в системе UNIX – это последовательность байтов произвольной длины (от 0 до некоторого максимума), содержащая произвольную информацию. Не делается принципиального различия между текстовыми (ASCII) файлами, двоичными файлами и любыми другими файлами. Значение битов в файле целиком определяется владельцем файла. Системе это безразлично. Изначально размер имен файлов был ограничен 14 символами, но в системе Berkeley UNIX этот предел был расширен до 255 символов, что впоследствии было принято в System V, а также в большинстве других версий. В именах файлов разрешается использовать все ASCII-символы, кроме символа NUL.

По соглашению многие программы ожидают, что имена файлов должны состоять из основного имени и расширения, отделяемого от основного имени файла точкой (которая в системе UNIX также считается символом). Эти соглашения никак не регулируются операционной системой, но некоторые компиляторы и другие программы ожидают файлов именно с такими расширениями. Расширения могут иметь произвольную длину, кроме того, файлы могут иметь по нескольку расширений.

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

Существует два способа задания имени файла в системе UNIX, как в оболочке, так и при открытии файла из программы. Первый способ заключается в использовании абсолютного пути, указывающего, как найти файл от корневого каталога. Пример абсолютного пути: /man/labs/operat/numb4. Он сообщает системе, что в корневом каталоге следует найти каталог man, затем в нем найти каталог labs, который содержит каталог operat, а в нем расположен файл numb4.

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

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

Кроме обычных файлов, системой UNIX также поддерживаются символьные специальные файлы и блочные специальные файлы. Символьные специальные файлы используются для моделирования последовательных устройств ввода-вывода, таких как клавиатуры и принтеры. Если процесс откроет файл /dev/tty и прочитает из него, он получит символы, введенные с клавиатуры. Если открыть файл /dev/lp и записать в него данные, то эти данные будут распечатаны на принтере.

Блочные специальные файлы, часто с такими именами, как /dev/hd1, могут использоваться для чтения и записи необработанных дисковых разделов, минуя файловую систему. Таким образом, поиск байта номер k, за которым последует чтение, приведет к чтению k-то байта на соответствующем дисковом разделе, игнорируя i-узел и файловую структуру. Необработанные блочные устройства используются для страничной подкачки и свопинга программами установки файловой системы (например, mkfs) и программами, исправляющими ломаные файловые системы (например, fsck).

При наличии у вычислительной машины нескольких дисков возникает вопрос управления ими. Одно из решений заключается в том, чтобы установить самостоятельную файловую систему на каждый отдельный диск и управлять ими как отдельными файловыми системами. При таком решении пользователь должен помимо каталогов указывать также и устройство, если оно отличается от используемого по умолчанию. Такой подход применяется в операционных системах MS-DOS, Windows 98 и VMS. Решение, применяемое в операционной системе UNIX, заключается в том, чтобы позволить монтировать один диск в дерево файлов другого диска. Например, можно смонтировать дискету в каталог жесткого диска. При этом пользователь будет видеть единое дерево файлов и уже не должен думать о том, какой файл на каком устройстве хранится.

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

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

09 — Операционные системы. Ввод и вывод


Похожие статьи.

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