ЗАНЯТИЕ 20. «ВВОД И ВЫВОД ИНФОРМАЦИИ»

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

17.1. Основы аппаратного обеспечения ввода-вывода

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

Устройства ввода-вывода

Устройства ввода-вывода можно условно разделить на две категории: блочные устройства и символьные устройства. К блоч­ным относятся такие устройства, которые хранят информацию в блоках фиксированной длины, у каждого из которых есть соб­ственный адрес. Обычно размеры блоков варьируются от 512 до 65 536 байт. Вся передача данных ведется пакетами из одного или не­скольких целых (последовательных) блоков. Важным свойством блочного устройства является то, что оно способно читать или за­писывать каждый блок независимо от всех других блоков. Среди наиболее распространенных блочных устройств жесткие диски, приводы Blu-ray-дисков и флеш накопители USB.

Если приглядеться, то граница между устройствами с адре­суемыми блоками и устройствами, не обладающими таким свой­ством, не имеет четкого определения. Каждый согласен, что диск является устройством с адресуемыми блоками, поскольку, где бы в данный момент ни находился блок головок, всегда есть возмож­ность переместиться к другому цилиндру, а затем дождаться, пока нужный блок не подойдет под головку. Теперь рассмотрим устаре­вающий накопитель на магнитной ленте, иногда все еще использу­емый для создания резервной копии диска (по причине дешивизны ленты). Ленты содержат последовательность блоков. Если накопи­тель получает команду считать блок N, он всегда может перемотать ленту назад и запустить рабочий ход вперед до тех пор, пока не до­берется до блока N. Эта операция аналогична операции позициони­рования головок на нужную дорожку на диске, за исключением того, что на нее затрачивается гораздо больше времени. Также накопитель может иметь, а может и не иметь возможность перепи­сать один блок в середине ленты. Даже если имеется возможность использовать накопители на магнитной ленте в качестве блочных устройств произвольного доступа, считать их таковыми будет не­которым преувеличением: как правило, они в этом качестве не ис­пользуются.

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

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

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

Таблица 8. Скорости передачи данных некоторых наиболее распро­страненных устройств

Устройство Скорость обмена данными
Клавиатура 10 байт/с
Мышь 100 байт/с
Модем 56 К 7 Кбайт/с
Сканер с разрешением 300 dpi 1 Мбайт/с
Цифровая камера 3,5 Мбайт/с
Blu-ray-диск 4x 18 Мбайт/с
Беспроводная сеть стандарта 802.11n 37,5 Мбайт/с
USB 2.0 60 Мбайт/с
FireWire 800 100 Мбайт/с
Сеть стандарта Gigabit Ethernet 125 Мбайт/с
Диск SATA 3 600 Мбайт/с
USB 3.0 625 Мбайт/с
Диск SCSI Ultra 5 640 Мбайт/с
Одна дорожка шины PCIe 3.0 985 Мбайт/с
Шина Thunderbolt 2 2,5 Гбайт/с
Сеть SONET OC-768 5 Гбайт/с

Контроллеры устройств

Устройства ввода-вывода зачастую состоят из механиче­ской и электронной составляющих. Зачастую эти две составляющие удается разделить, чтобы получить модульную конструкцию и при­дать устройству более общий вид. Электронный компонент называ­ется контроллером устройства, или адаптером. На персональных компьютерах он часто присутствует в виде микросхемы на систем­ной плате или печатной платы, вставляемой в слот расширения (PCIe). Механический компонент представлен самим устройством. Именно такой порядок показан на рис. 46

На плате контроллера обычно имеется разъем, к которому может быть подключен кабель, ведущий непосредственно к самому устройству. Многие контроллеры способны управлять двумя, че­тырьмя или даже восемью одинаковыми устройствами. Если ин­терфейс между контроллером и устройством подпадает под какой-нибудь стандарт, будь то один из официальных стандартов ANSI, IEEE или ISO или же один из ставших де-факто стандартов, то компании могут производить контроллеры или устройства, соот­ветствующие этому интерфейсу. К примеру, многие компании про­изводят дисковые приводы, соответствующие интерфейсу SATA, SCSI, USB, Thunderbolt или FireWire (IEEE 1394).

Интерфейс между контроллером и устройством зачастую относится к интерфейсу очень низкого уровня. Например, какой-нибудь жесткий диск может быть отформатирован на 2 000 000 сек¬торов на дорожку с размером сектора 512 байт. Но на самом деле с привода поступает последовательный поток битов, начинающийся с заголовка сектора (преамбулы), затем следуют 4096 бит, имею¬щиеся в секторе, и в завершение следует контрольная сумма, также называемая кодом коррекции ошибок (Errorr Correcting Code (ECC)). Заголовок сектора записывается на диск во время формати¬рования и содержит номера цилиндра и сектора, размер сектора и тому подобные данные, а также информацию о синхронизации.
Задача контроллера состоит в преобразовании последова¬тельного потока битов в блок байтов и коррекции ошибок в случае необходимости. Блок байтов обычно проходит первоначальную побитовую сборку в буфере, входящем в состав контроллера. После проверки контрольной суммы блока и объявления его не содержа¬щим ошибок он может быть скопирован в оперативную память.
Контроллер монитора на базе жидкокристаллического дис¬плея также работает как побитовое последовательное устройство на таком же низком уровне. Он считывает байты, содержащие сим¬волы, которые должны быть отображены из памяти, и генерирует сигналы, используемые для изменения поляризации подсветки со¬ответствующих пикселов для записи их на экране. Если бы кон¬троллер дисплея этим не занимался, то программисту операцион-ной системы пришлось бы явным образом программировать элек¬трические поля всех пикселов. При наличии контроллера операци¬онная система инициализирует его с помощью нескольких пара¬метров, среди которых количество символов или пикселов в строке и количество строк на экране, а заботу об управлении электриче¬скими полями возлагает на контроллер. В самое ближайшее время жидкокристаллические экраны полностью заменят старые мони¬торы на основе электронно-лучевой трубки (ЭЛТ). Электронно-лу¬чевая трубка испускает электронный луч на флюоресцентный экран. С помощью магнитных полей система способна искривлять луч и рисовать пикселы на экране. По сравнению с жидкокристал-лическими экранами электронно-лучевые мониторы очень гро¬моздкие, потребляющие много энергии и хрупкие. Более того, раз¬решение современных жидкокристаллических дисплеев (с техноло¬гией Retina) настолько высоко, что человеческий глаз не в состоя¬нии различить отдельные пикселы. Сегодня трудно представить, что в прошлом ноутбуки поставлялись с небольшими ЭЛТ-экра¬нами, из-за которых они имели глубину 20 см и вполне подходя¬щий для физических тренировок вес, составлявший 12 кг.

Ввод-вывод, отображаемый на пространство памяти
У каждого контроллера для связи с центральным процессо¬ром имеется несколько регистров. Путем записи в эти регистры операционная система может давать устройству команды на предо¬ставление данных, принятие данных, включение, выключение или выполнение каких-нибудь других действий. Считывая данные из этих регистров, операционная система может узнать о текущем со¬стоянии устройства, о том, готово ли оно принять новую команду, и т. д.
В дополнение к регистрам управления у многих устройств имеется буфер данных, из которого операционная система может считывать данные и в который она может их записывать. Напри¬мер, наиболее распространенный способ отображения компьюте¬рами пикселов на экране предусматривает наличие видеопамяти, которая по сути является буфером данных, куда программы или операционная система могут вести запись.
Но тут возникает вопрос: как центральный процессор обме¬нивается данными с регистрами управления и буферами данных устройств? Есть два альтернативных варианта. В первом из них каждому регистру управления назначается номер порта ввода-вы¬вода, являющийся 8- или 16-разрядным целым числом. Набор всех портов ввода-вывода формирует пространство портов ввода-вы¬вода, которое защищено от доступа со стороны обычных пользова¬тельских программ (доступ к нему имеет только операционная си¬стема). Используя специальные команды ввода-вывода, например
IN REG,PORT
центральный процессор может считать данные из регистра управ¬ления PORT и сохранить полученный результат в своем регистре REG. Аналогично этому, используя команду
OUT PORT,REG
центральный процессор может записать содержимое своего регистра REG в регистр управления PORT. Многие компьютеры первых поколений, включая практически все универсальные ком¬пьютеры, такие как IBM 360 и все его преемники, работали именно таким образом.
На рис. 47, а показано, что при использовании этой схемы для оперативной памяти и для ввода-вывода используются совер¬шенно разные адресные пространства. При такой конструкции ко¬манды
IN R0,4
и
MOV R0,4
полностью отличаются друг от друга. Первая команда читает со¬держимое порта ввода-вывода 4 и помещает его в регистр R0, а вторая команда читает содержимое слова памяти 4 и помещает его в тот же регистр R0. Таким образом, четверки в этих примерах ссы¬лаются на различные не связанные друг с другом адресные про¬странства.

Второй вариант, появившийся на машинах PDP-11, преду¬сматривает отображение всех регистров управления на простран¬ство памяти (рис. 47, б). Каждому регистру управления выделен уникальный адрес в памяти, который не распределяется в опера¬тивной памяти. Эта система называется отображаемым на адрес¬ное пространство памяти вводом-выводом. В большинстве си¬стем выделяемые адреса находятся в верхней части адресного про¬странства или возле нее. На рис. 47, в показан также гибридный вариант, в котором имеются буферы данных ввода-вывода, отобра¬жаемые на пространство памяти, и отдельные порты ввода-вывода для регистров управления. Такая архитектура используется в се¬мействе машин x86, у которых по аналогии с IBM PC адресное про¬странство оперативной памяти от 640 K до 1 M – 1 зарезервировано для буферов данных различных устройств вдобавок к портам ввода-вывода, имеющим номера от 0 до 64 K – 1.
Как же работают все эти схемы? Как только центральному процессору необходимо считать слово либо из памяти, либо из порта ввода-вывода, он выставляет нужный ему адрес на адресных линиях шины, а затем выставляет сигнал READ на линии управле¬ния шины. Для сообщения о том, какое именно пространство ему нужно, ввода-вывода или памяти, используется другая сигнальная линия. Если нужно пространство памяти, то на запрос отвечает па¬мять, если же нужно пространство ввода-вывода, то на запрос отве¬чает устройство ввода-вывода. Если имеется только пространство памяти (как на рис. 47, б), то каждый модуль памяти и каждое устройство ввода-вывода сравнивают адрес, выставленный на ад¬ресной линии с диапазоном обслуживаемых ими адресов. Если ад¬рес попадает в его диапазон, то этот модуль или это устройство от¬вечает на запрос. Поскольку адресов, выделенных одновременно и памяти, и устройству ввода — вывода, не существует, то никаких недоразумений и конфликтов не возникает.
Эти две схемы обращения к контроллерам имеют свои до¬стоинства и недостатки. Начнем с достоинств ввода-вывода, отоб¬ражаемого на пространство памяти. Во-первых, если для операций чтения и записи в регистры управления устройств требуются спе¬циальные команды ввода-вывода, для доступа к этим командам требуется ассемблерный код, поскольку в С или С++ не существует способов непосредственного выполнения команд IN и OUT, только с применением вставок на ассемблере или с использованием напи¬санных на ассемблере подпрограмм, что влечет дополнительные накладные расходы. При использовании же ввода-вывода, отобра¬жаемого на память, регистры управления внешними устройствами могут рассматриваться как обычные переменные в памяти, что поз¬воляет написать драйвер соответствующего устройства полностью на языке C. Когда не используется ввод-вывод, отображаемый на пространство памяти, возникает необходимость использования кода на ассемблере.
Во-вторых, при использовании ввода-вывода, отображае¬мого на пространство памяти, отпадает необходимость в специаль¬ном механизме защиты от осуществления ввода — вывода со сто¬роны пользовательских процессов. Операционной системе нужно лишь воздержаться от помещения той части адресного простран¬ства, в которой содержатся регистры управления, в какие-либо вир¬туальные адресные пространства пользователей. Еще лучше, если у каждого устройства его регистры управления будут находиться на отдельной странице адресного пространства — тогда операционная система может предоставить одному пользователю в отличие от других пользователей возможность управления определенными устройствами, просто включив нужные страницы в его таблицу страниц. Такая схема может позволить различным драйверам устройств размещаться в разных адресных пространствах памяти, не только сокращая при этом размер пространства ядра, но и обере¬гая один драйвер от помех со стороны других драйверов.
В-третьих, при вводе-выводе, отображаемом на простран¬ство памяти, любая команда, которая может обращаться к памяти, может также обращаться и к регистрам управления. Например, если есть команда TEST, проверяющая значение слова памяти на равен¬ство нулю, она может быть применена также для проверки на ра¬венство нулю значения в регистре управления, что может быть сиг¬налом незанятости устройства и возможности приема им новой ко¬манды. При этом код на ассемблере может иметь следующий вид:
LOOP: TEST PORT_4 // проверка того, содержит ли порт 4
значение 0
BEQ READY // если он содержит 0, переход к метке ready
BRANCH LOOP // если нет, продолжение проверки
READY:
При отсутствии отображения регистров ввода-вывода на пространство памяти регистр управления сначала должен быть счи¬тан в центральный процессор, а затем проверен, для чего потребу¬ются две команды вместо одной. В случае использования показан¬ного ранее цикла нужно будет добавить еще четыре команды, что слегка замедлит отклик устройства, проверяемого на незанятость.
При конструировании компьютеров практически всегда приходится идти на компромиссы, и данный случай не исключение. У ввода-вывода, отображаемого на пространство памяти, есть и свои недостатки. Во-первых, большинство современных компьюте¬ров используют ту или иную разновидность кэширования слов па¬мяти. Кэширование регистров управления устройством может при¬вести к пагубным последствиям. Посмотрим, что получится с цик¬лом в представленном ранее ассемблерном коде при использовании кэширования. Первое обращение к PORT_4 приведет к тому, что это слово попадет в кэш. При последующих обращениях значение будет браться прямо из кэша без запроса самого устройства. А ко¬гда устройство наконец-то освободится, программа просто не смо¬жет это определить и цикл продолжится до бесконечности.
Чтобы предотвратить подобную ситуацию при использова¬нии ввода-вывода, отображаемого на пространство памяти, аппара¬тура должна иметь возможность выборочного отключения кэширо¬вания, например на постраничной основе. Это свойство приводит к дополнительному усложнению как аппаратного обеспечения, так и операционной системы, которым приходится управлять избира¬тельным кэшированием.
Во-вторых, если используется только одно адресное про¬странство, то все модули памяти и все устройства ввода-вывода должны проверять все обращения к памяти, чтобы понять, кому из них следует отвечать. Если у компьютера, как показано на рис. 48,а, используется одна общая шина, то заставить всех рассматривать каждый адрес несложно.
Но в современных персональных компьютерах наблюдается тенденция использования выделенной высокоскоростной шины па¬мяти (рис. 48, б). Эта шина специально приспособлена для оптими¬зации производительности памяти, чтобы не идти на компромисс ради медлительных устройств ввода-вывода. У машин семейства x86 могут быть несколько шин (шины памяти, шины PCIe, SCSI и USB), показанных на рис. 49.

Рис. 49. Структура большой системы семейства x86

Проблема, возникающая при использовании отдельной шины памяти на машинах с отображением регистров ввода-вывода на память, состоит в том, что устройства ввода-вывода не могут увидеть адреса памяти, выставляемые процессором на эту шину, следовательно, они не могут реагировать на эти адреса. Поэтому, чтобы заставить отображаемый на память ввод-вывод работать на системе с несколькими шинами, нужно предпринять какие-то спе¬циальные меры. Одним из возможных вариантов является первона-чальное направление всех обращений к пространству памяти по высокоскоростной шине напрямую к модулям памяти. Если эти мо¬дули не смогут ответить, центральный процессор попробует вос¬пользоваться другими шинами. Такая конструкция вполне жизне¬способна, но требует дополнительного усложнения аппаратного обеспечения.
Второе возможное решение предусматривает установку на шину памяти специального отслеживающего устройства, передаю¬щего все адреса потенциально заинтересованным устройствам ввода-вывода. При этом возникает проблема, связанная с неспособ¬ностью устройств ввода-вывода обрабатывать запросы так же быстро, как это делают модули памяти.
Третий вариант, который как раз и используется в приве¬денной на рис. 49 конфигурации компьютера, основан на фильтра¬ции адресов в контроллере памяти. В данном случае микросхема контроллера памяти содержит регистры диапазона, заполняемые в процессе загрузки системы. К примеру, диапазон от 640 К до 1 M – 1 может быть помечен как не принадлежащий пространству опера¬тивной памяти и содержащий адреса, ведущие не к памяти, а к устройствам. Недостаток этой схемы состоит в необходимости обо¬значить в процессе загрузки системы те адреса, которые в действи¬тельности не будут относиться к памяти. Получается, что у каждой схемы есть аргументы за и против ее использования, поэтому без компромиссов не обойтись.

Прямой доступ к памяти
Независимо от наличия или отсутствия у центрального про¬цессора ввода-вывода, отображаемого на пространство памяти, ему необходимо обращаться к контроллерам устройств, чтобы осу¬ществлять с ними обмен данными. Центральный процессор может запрашивать данные у контроллера ввода-вывода побайтно, но при этом будет нерационально расходоваться его рабочее время, по¬этому чаще всего используется другая схема, которая называется прямым доступом к памяти (Direct Memory Access (DMA)). Чтобы не усложнять объяснение, предполагается, что центральный про¬цессор обращается ко всем устройствам и к памяти посредством единой системной шины, соединяющей центральный процессор, память и устройства ввода-вывода (рис. 50). Нам уже известно, что реальная организация в современных системах намного сложнее, но все принципы одинаковы. Операционная система может исполь¬зовать DMA только при наличии аппаратного DMA-контроллера, присутствующего у большинства систем. Иногда этот контроллер встроен в контроллеры дисков и другие контроллеры, но такая кон¬струкция требует отдельного DMA-контроллера для каждого устройства. Чаще всего для упорядочения обмена данными с не-сколькими устройствами, проводимого нередко в параллельном режиме, доступен только один DMA-контроллер (размещенный, к примеру, на системной плате).

На рис. 50 показано, что где бы DMA-контроллер ни находился физически, он имеет доступ к системной шине независимо от центрального процессора. В нем имеется несколько регистров, доступных центральному процессору для чтения и записи. В их число входят регистр адреса памяти, регистр счетчика байтов и один или несколько регистров управления. В регистрах управления указываются используемый порт ввода- вывода, направление передачи данных (чтение из устройства ввода-вывода или запись в него), единица передаваемой информации (побайтовая или пословная передача), а также количество байтов, передаваемых в одном пакете.
Чтобы объяснить принцип работы DMA, рассмотрим сначала, как осуществляется чтение диска, когда DMA не используется. Сначала контроллер диска последовательно побитно считывает блок (один или несколько секторов) с диска, пока весь блок не окажется во внутреннем буфере контроллера. Затем он вычисляет контрольную сумму, чтобы убедиться в отсутствии ошибок чтения. Затем контроллер инициирует прерывание. Когда операционная система приступает к работе, она может в цикле побайтно или пословно считать дисковый блок из буфера контроллера, считывая при каждом проходе цикла один байт или слово из регистра контроллера устройства и сохраняя его в оперативной памяти.
При использовании DMA все происходит по-другому. Сначала центральный процессор программирует DMA-контроллер, устанавливая значения его регистров таким образом чтобы он знал, что и куда нужно передать (шаг 1 на рис. 50). Он также выдает команду контроллеру диска на чтение данных с диска во внутренний буфер контроллера и на проверку контрольной суммы. После того как в буфере контроллера окажутся достоверные данные, к работе может приступать DMA.
DMA-контроллер инициирует передачу данных, выдавая по шине контроллеру диска запрос на чтение (шаг 2). Этот запрос на чтение выглядит так же, как и любой другой запрос на чтение, и контроллер диска не знает и даже не интересуется, откуда он пришел — от центрального процессора или от DMA-контроллера. Обычно адрес памяти, куда нужно вести запись, выставлен на адресных линиях шины, поэтому, когда контроллер диска извлекает очередное слово из своего внутреннего буфера, он знает, куда его следует записать. Запись в память — это еще один стандартный цикл шины (шаг 3). Когда запись завершается, контроллер диска также по шине посылает подтверждающий сигнал DMA-контроллеру (шаг 4). Затем DMA-контроллер дает приращение используемому адресу памяти и уменьшает значение счетчика байтов. Если счетчик байтов все еще больше нуля, то шаги со 2-го по 4-й повторяются до тех пор, пока значение счетчика не станет равно нулю. Как только это произойдет, DMA-контроллер выставляет прерывание, чтобы центральный процессор узнал о завершении передачи данных. И когда к работе приступает операционная система, ей уже не нужно копировать дисковый блок в память, потому что он уже там.
Контроллеры DMA существенно различаются по степени сложности. Самые простые из них, как описано ранее, обслужи¬вают одновременно только одну операцию передачи данных. Более сложные контроллеры могут быть запрограммированы на одновре¬менную обработку нескольких таких операций. У таких контролле¬ров есть несколько наборов внутренних регистров, по одному для каждого канала. Центральный процессор начинает с того, что за¬гружает каждый набор соответствующими параметрами для пере¬дачи данных по определенному каналу. После показанной на рис. 50 передачи каждого слова (шаги со 2-го по 4-й), DMA-контроллер решает, какое из устройств обслуживать следующим. Он может быть настроен на использование алгоритма кругового обслужива¬ния, или же у него может быть система приоритетов, дающая пре¬имущество одним устройствам над другими. Одновременно могут рассматриваться сразу несколько запросов к различным контролле¬рам устройств при условии, что есть однозначный способ обособ¬ленной выдачи сигнала подтверждения. Поэтому довольно часто для каждого DMA-канала на шине используется отдельная линия подтверждения.
Многие шины могут работать в двух режимах: пословном и поблочном. Некоторые DMA-контроллеры могут также работать в обоих режимах. В первом режиме осуществляются операции, опи¬санные ранее: DMA-контроллер запрашивает передачу одного слова и получает его. Если центральному процессору также нужна шина, то он вынужден ждать. Такой механизм называется захва¬том цикла, поскольку контроллер устройства ненадолго украдкой перехватывает у центрального процессора первый попавшийся цикл шины, слегка замедляя его работу. В блочном режиме DMA-контроллер предписывает устройству занять шину, осуществить серию пересылок данных, а затем освободить шину. Такой образ действий называется пакетным режимом. Он более эффективен, чем захват цикла, поскольку, чтобы занять шину, требуется опреде¬ленное время, а тут это время затрачивается на передачу сразу не¬скольких слов только один раз. Недостаток пакетного режима за¬ключается в том, что если будет передаваться довольно длинный пакет данных, то он может заблокировать центральный процессор и другие устройства на весьма существенный период времени.
Рассмотренную нами модель иногда называют сквозным режимом, так как DMA-контроллер предписывает контроллеру устройства осуществить передачу данных непосредственно в опе¬ративную память. Альтернативный режим, который используется некоторыми DMA-контроллерами, предусматривает принуждение контроллера устройства на передачу слова DMA-контроллеру, ко¬торый затем выставляет на шине дополнительный запрос на запись слова по месту его предназначения. Эта схема требует дополни¬тельного цикла шины на каждое передаваемое слово, но она обла¬дает большей гибкостью, поскольку может копировать из устрой¬ства в устройство и даже из одного места памяти в другое (выпол¬няя сначала чтение из памяти, а затем запись в память по другому адресу).
Большинство DMA-контроллеров используют для передачи данных физические адреса памяти. Для этого операционная си¬стема должна преобразовать виртуальный адрес намеченного бу¬фера памяти в физический адрес и записать этот физический адрес в адресный регистр контроллера DMA. В отдельных DMA-кон¬троллерах вместо этого используется альтернативная схема, при которой в контроллер записывается виртуальный адрес. Затем для осуществления преобразования виртуального адреса в физический DMA-контроллер должен воспользоваться блоком управления па¬мятью (MMU). Виртуальные адреса могут выставляться на шину только в том случае, если MMU является частью памяти (что встречается довольно редко), а не частью центрального процессора.
Как уже упоминалось, еще до начала работы DMA диск считывает данные в свой внутренний буфер. Может вызвать удив¬ление, почему контроллер, получив данные с диска, не сохраняет байты сразу в оперативной памяти. Иными словами, зачем ему ну¬жен внутренний буфер? На это есть две причины. Во-первых, осу¬ществляя внутреннюю буферизацию, контроллер диска может про¬верять контрольную сумму перед тем, как приступать к передаче данных. Если контрольная сумма не сходится, выставляется ошибка и данные не передаются.
Во-вторых, как только начинается передача данных с диска, биты поступают с диска с постоянной скоростью независимо от того, готов контроллер их принимать или нет. Если бы контроллер попытался записывать данные непосредственно в память, то для передачи каждого слова ему пришлось бы обращаться к системной шине. Если бы шина была занята обслуживанием какого-нибудь другого устройства (например, при пакетном режиме работы), то контроллеру пришлось бы ждать. Если бы следующее слово, про¬читанное с диска, поступило еще до того, как было сохранено предыдущее слово, контроллер вынужден был бы где-нибудь его сохранить. Если бы шина была слишком занята, то контроллеру пришлось бы заняться хранением довольно большого количества слов и решать массу административных задач. А когда осуществля¬ется внутренняя буферизация блока, надобности в использовании шины не возникает вплоть до начала работы DMA, поэтому кон-струкция контроллера может быть упрощена, поскольку передача данных в память с помощью DMA не критична по времени. (Неко¬торые более старые контроллеры действительно имели непосред¬ственное обращение к памяти, располагая совсем небольшой по объему внутренней буферизацией, но когда шина была слишком занята, передача должна была прерываться из-за ошибки перепол¬нения.)
DMA используется не во всех компьютерах. Аргументом против его использования является то, что центральный процессор зачастую намного быстрее DMA-контроллера и может выполнять эту работу намного быстрее (когда ограничивающим фактором не является скорость работы устройства ввода-вывода). Если для него нет никакой другой работы, то заставлять быстрый центральный процессор ждать, пока медленный DMA-контроллер завершит свою работу, абсолютно бессмысленно. Кроме того, если избавиться от контроллера DMA и возложить на центральный процессор всю ра¬боту в программном режиме, то можно сэкономить средства, что является важным фактором для дешевых встраиваемых компьюте¬ров.

Еще раз о прерываниях
В типичной персональной компьютерной системе присут¬ствует структура прерываний, показанная на рис. 51. На аппарат¬ном уровне прерывания работают следующим образом. Когда устройство ввода-вывода завершает порученную ему работу, оно инициирует прерывание (при условии, что прерывания разрешены операционной системой). Это делается путем выставления сигнала на специально выделенной линии шины. Микросхема контроллера прерываний, расположенная на системной плате, обнаруживает этот сигнал и принимает решение о характере дальнейших дей¬ствий.
Если не было никаких других отложенных прерываний, контроллер прерываний немедленно обрабатывает инициированное прерывание. Но если он находится в процессе обработки другого прерывания или какие-нибудь другое устройство в то же самое время выставило на шине запрос на прерывание более высокого приоритетного уровня, то устройство на некоторое время просто игнорируется. В таком случае оно продолжает выставлять на шину сигнал на прерывание до тех пор, пока оно не будет обслужено центральным процессором.

Для обработки прерывания контроллер помещает номер на адресные линии, указывая, какое устройство требует к себе внима¬ния, и выставляет сигнал на прерывание работы центрального про¬цессора.
Сигнал на прерывание приводит к тому, что центральный процессор прерывает работу над тем, чем он занимался, и присту¬пает к другой работе. Номер на адресных линиях используется в качестве индекса в таблице, называемой вектором прерываний, из которой извлекается новое значение счетчика команд. Этот счетчик команд указывает на начало соответствующей процедуры обра¬ботки прерывания. Как правило, с этого момента системные и обычные прерывания используют один и тот же механизм и до¬вольно часто используют один и тот же вектор прерывания. Место¬положение вектора прерываний может быть «зашито» в самой ма¬шине или находиться где-нибудь в памяти, в том месте, на которое указывает регистр центрального процессора (загружаемый опера¬ционной системой).
Практически сразу после запуска процедура обработки пре¬рывания подтверждает получение прерывания, записывая опреде¬ленное значение в один из портов ввода-вывода контроллера пре¬рываний. Это подтверждение сообщает контроллеру, что он может выдавать новое прерывание. За счет задержки этого подтверждения центральным процессором до тех пор, пока он не будет готов к об¬работке нового прерывания, может быть устранена конкуренция, связанная с наличием нескольких (почти одновременно выдавае¬мых) прерываний. Между прочим, у некоторых устаревших ком¬пьютеров отсутствует централизованный контроллер прерываний, поэтому каждый контроллер устройства выставляет собственные прерывания.
Перед запуском процедуры обслуживания аппаратура все¬гда сохраняет некую информацию. Сохраняемая информация и ме¬сто ее хранения довольно широко варьируются от процессора к процессору. Должен быть сохранен как минимум счетчик команд, чтобы можно было возобновить прерванный процесс. Другой край¬ностью будет сохранение всех программно доступных регистров и большого количества внутренних регистров центрального процес¬сора.
Возникает вопрос: где следует хранить эту информацию? Можно поместить ее во внутренние регистры, значение которых операционная система может считывать по мере надобности. Но при этом возникает проблема, не позволяющая дать подтверждение контроллеру прерываний до тех пор, пока не будет считана вся по¬тенциально важная информация, поскольку следующее прерывание при сохранении состояния перепишет все внутренние регистры. Такая стратегия приводит к продолжительным простоям, в течение которых запрещены прерывания, и к возможным потерям сигналов прерываний и потерям данных.
Именно поэтому большинство центральных процессоров сохраняют информацию в стеке. Но этот подход также имеет свои проблемы. Сначала возникает вопрос: в чьем стеке хранить дан¬ные? Если использовать текущий стек, то он может быть стеком пользовательского процесса. Указатель стека может даже содер¬жать недопустимое значение, что приведет к фатальной ошибке при попытке оборудования записать несколько слов по адресу, на кото¬рый он указывает. Он также может указывать на конец страницы. После нескольких записей может произойти выход за ее пределы, и будет сгенерирована ошибка отсутствия страницы. Возникнове¬ние этой ошибки во время обработки аппаратного прерывания со¬здает весьма серьезную проблему: где сохранить состояние, чтобы обработать ошибку отсутствия страницы?
При использовании стека ядра возникает намного большая вероятность того, что указатель стека содержит допустимое значе¬ние и указывает на фиксированную страницу. Но переключение в режим ядра может потребовать изменения контекста MMU и, веро¬ятно, сделает недействительной большую часть или даже все со¬держимое кэша и TLB. Их статическая или динамическая переза¬грузка увеличит время обработки прерывания и приведет к пустой трате времени центрального процессора.

17.2. Принципы создания программного обеспечения ввода-вывода

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

Задачи, стоящие перед программным обеспечением ввода-вывода

Ключевая концепция разработки программного обеспечения ввода-вывода такова: независимость от конкретных устройств. Ее смысл заключается в предоставлении возможности создания программ, способных получить доступ к любому устройству ввода-вывода без необходимости предварительного определения устрой¬ства. К примеру, программа, считывающая входной файл, должна иметь возможность читать его с жесткого диска, DVD или флеш-накопителя USB без изменения программы под каждое конкретное устройство. То есть любой пользователь должен получить возмож¬ность набрать команду типа
sort output
и убедиться, что она работает с входными данными, поступающими с диска любого типа или с клавиатуры, и с выходными данными, отправляемыми на диск любого типа или на экран. Решение всех проблем, связанных с разнородностью этих устройств и с тем, что для них требуются существенно отличающиеся друг от друга по¬следовательности команд для чтения или записи, возлагается на операционную систему.
С независимостью от конкретного устройства тесно связана задача однообразного именования. Имя файла или устройства должно быть просто строкой или целым числом и никоим образом не зависеть от устройства. В UNIX все диски могут быть произ¬вольным образом сгруппированы в иерархию файловой системы, поэтому пользователь не должен знать, какое имя какому устрой¬ству соответствует. Например, флеш-накопитель USB может быть подключен (смонтирован) к каталогу /usr/ast/backup, чтобы при копировании файла в каталог /usr/ast/backup/monday происходило его копирование в каталог monday на этом флеш-накопителе. Таким образом, все файлы и устройства адресуются одинаково: по пол-ному имени, включающему путь в файловой системе.
Другим важным аспектом программного обеспечения ввода-вывода является обработка ошибок. В общем-то обработка ошибок должна осуществляться как можно ближе к аппаратуре. Если контроллер обнаружил ошибку чтения, он должен попы¬таться, если это возможно, исправить ее самостоятельно. Если он не в состоянии с ней справиться, то ее должен обработать драйвер устройства, возможно, путем повторной попытки чтения блока. Многие ошибки носят случайный характер, как, например, ошибки чтения, вызванные пылинками на головке чтения, и зачастую исче¬зают при повторе операции. Верхние уровни должны осведом¬ляться о возникшей проблеме только в том случае, если с ней не удалось справиться на нижних уровнях. Во многих случаях устра¬нение ошибки может быть выполнено на нижних уровнях неза¬метно, так, что верхние уровни даже не узнают о ее существовании.
Еще один важный вопрос — какой способ применить для передачи данных, синхронный (блокирующий) или асинхронный (управляемый с помощью прерываний). Большинство физических операций ввода-вывода осуществляется в асинхронном режиме: центральный процессор инициирует передачу данных и устраня¬ется от нее для выполнения каких-нибудь других задач до тех пор, пока не поступит прерывание. А вот прикладные программы значи¬тельно легче писать, если операции ввода-вывода осуществляются в блокирующем режиме: после системного вызова read программа автоматически приостанавливается до тех пор, пока данные не по¬ступят в буфер. На операционную систему возлагается задача, чтобы фактически управляемые с помощью прерываний операции выглядели для пользовательской программы как блокирующие. Но некоторым приложениям с весьма высокой производительностью необходимо управление всеми деталями ввода-вывода, поэтому ряд операционных систем делают для них доступным асинхронный ввод-вывод.
Следующей задачей программного обеспечения ввода-вы¬вода является буферизация. Часто данные, поступающие из устрой¬ства, не могут быть сохранены непосредственно в конечном пункте своего назначения. К примеру, когда пакет данных приходит по сети, операционная система не знает, куда его поместить, пока где-нибудь его не сохранит и не проанализирует. К тому же некоторые устройства (к примеру, цифровые аудиоустройства) предъявляют жесткие требования к работе в реальном времени, поэтому данные должны быть помещены в выходной буфер заранее, чтобы скорость получения данных из буфера не зависела от скорости наполнения буфера, что позволит избежать его опустошения. Буферизация вле¬чет за собой многочисленные операции копирования данных и ча¬сто оказывает существенное влияние на производительность опе¬раций ввода-вывода.
И последними из упоминаемых здесь понятий будут устройства совместного использования и выделенные устройства. Некоторые устройства ввода-вывода, например диски, могут ис¬пользоваться многими пользователями одновременно. Когда мно¬гочисленные пользователи работают с открытыми файлами на од¬ном и том же диске в одно и то же время, проблем не возникает. А вот другие устройства, к примеру принтеры, должны быть выде-лены только одному пользователю до тех пор, пока он не завершит свою работу с этим устройством. После этого принтер может полу¬чить другой пользователь. Если два или более пользователей станут записывать символы, перемешанные в случайном порядке на одной и той же странице, то из этого ничего не получится. Применение выделенных (монопольно используемых) устройств создает массу разнообразных проблем, в том числе взаимоблокировки. И опять операционная система должна справляться как с общими, так и с выделенными устройствами, избегая различных проблем.

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

Затем пользовательский процесс запрашивает принтер для записи, совершая системный вызов для того, чтобы его открыть. Если принтер в данный момент задействован другим процессом, этот вызов потерпит неудачу и вернет код ошибки или заблокирует пользовательский процесс до тех пор, пока принтер не станет до¬ступен, в зависимости от используемой операционной системы и параметров вызова. Как только пользовательский процесс получит принтер, он совершит системный вызов, предписывающий опера¬ционной системе осуществить распечатку строки на принтере.
Обычно операционная система копирует буфер со строкой в массив, скажем, p, в пространстве ядра, где ей будет проще полу¬чить доступ к нему (поскольку ядру, чтобы получить доступ к про¬странству пользователя, может потребоваться изменить карту па-мяти). Затем она проверяет принтер на доступность. Если принтер недоступен, она ждет, пока он освободится. Как только принтер станет доступен, операционная система копирует первый символ в регистр данных принтера, используя в данном примере ввод-вывод, отображаемые на пространство памяти. Это действие активирует принтер. Но символ может сразу же не появиться, поскольку неко¬торые принтеры перед тем, как что-нибудь печатать, собирают в буфере строку или страницу. Но на рис. 52, б мы видим, что первый символ был напечатан и система пометила «B» как следующий символ, выводимый на печать.
Как только первый символ будет скопирован на принтер, операционная система проводит проверку принтера на готовность к получению следующего символа. Как правило, у принтера имеется второй регистр, через который передается его состояние. Запись в регистр данных приводит принтер в состояние занятости. Когда контроллер принтера обработает текущий символ, он обозначает свою доступность, устанавливая определенный бит в своем реги¬стре состояния или помещая в него некоторое значение.
Теперь операционная система ждет, когда принтер снова придет в состояние готовности. Как только это произойдет, она по¬сылает на печать следующий символ, что и показано на рис. 52, в. Этот цикл продолжается до тех пор, пока не будет распечатана вся строка. Затем управление возвращается пользовательскому про¬цессу.
Действия, выполняемые операционной системой, сведены в листинг 2. Сначала данные копируются в ядро. Затем операционная система входит в цикл, выводя на печать по одному символу. Ос¬новное проявление программного ввода-вывода, ярко проиллю-стрированное в этом листинге, состоит в том, что после вывода символа центральный процессор постоянно опрашивает устройство на готовность приема следующего символа. Такое поведение часто называют опросом или активным ожиданием.
Листинг 2. Запись строки в принтер с использованием программного ввода-вывода
copy_from_user(buffer, p, count); /* p — буфер ядра / for (i = 0; i < count; i++) { / цикл для каждого символа / while (printer_status_reg != READY); /* цикл до готовности */
*printer_data_register = p[i]; /* вывод одного символа */
}
return_to_user();
Программный ввод-вывод прост в реализации, но имеет не¬достаток связывания центрального процессора ожиданием на все время, пока не будет завершена операция ввода-вывода. Если на «печать» символа уходит очень мало времени (поскольку принтер только копирует новый символ во внутренний буфер), то активное ожидание будет вполне подходящим решением. Активное ожида¬ние имеет смысл также во встроенных системах, где центральному процессору больше нечем заняться. Но для более сложных систем, где у центрального процессора есть еще и другая работа, активное ожидание не подойдет. Им нужен более эффективный способ ввода-вывода.

Ввод-вывод, управляемый прерываниями
Теперь рассмотрим случай распечатки на принтере без бу¬феризации символов, печатающем символы по мере их поступле¬ния. Если принтер может печатать, скажем, 100 символов в се¬кунду, то на печать каждого символа уходит 10 мс. Значит, после записи каждого символа в регистр данных принтера центральный процессор будет находиться в пустом цикле 10 мс, ожидая разре¬шения на вывод следующего символа. Этого времени более чем достаточно для переключения контекста и запуска какого-нибудь другого процесса, в противном случае эти 10 мс будут потрачены впустую. Разрешить центральному процессору заниматься чем-ни¬будь другим на время ожидания готовности принтера позволяет использование прерываний. Когда системный вызов на распечатку строки уже сделан, то, как уже было показано, буфер копируется в пространство ядра и первый символ копируется в принтер, как только он пожелает его принять. В этот момент центральный про-цессор обращается к планировщику и запускается какой-нибудь другой процесс. Процесс, запросивший распечатку строки, блоки¬руется до тех пор, пока не будет распечатана вся строка. Работа, выполняемая при системном вызове, показана на рис. 53, а.

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

Ввод-вывод с использованием DMA

Очевидным недостатком ввода-вывода, управляемого с по¬мощью прерываний, является то, что прерывания выдаются на каж¬дый символ. На прерывания требуется некоторое время, поэтому данная схема приводит к пустой трате определенного количества времени центрального процессора. Решение проблемы заключается в использовании контроллера DMA для посимвольной передачи строки принтеру без участия центрального процессора. По сути DMA-метод является тем же вводом-выводом, управляемым с по¬мощью прерываний, только вместо центрального процессора всю работу делает контроллер DMA. Примерный код программы пока¬зан на рис. 54

Большим преимуществом DMA является сокращение коли¬чества прерываний с одного на каждый символ до одного на каж¬дый распечатываемый буфер. При большом количестве символов и медленной обработке прерываний это может стать существенным улучшением. В то же время контроллер DMA обычно работает намного медленнее, чем центральный процессор. Если DMA-кон¬троллер не способен управлять устройством на полной скорости или центральному процессору нечего делать в ожидании прерыва¬ния от DMA, то, может быть, больше подойдет ввод-вывод, управ¬ляемый с помощью прерываний, или даже программный ввод-вы¬вод. Но в большинстве случаев DMA себя вполне оправдывает.

17.3 Уровни программного обеспечения ввода-вывода

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

Обработчики прерываний

Бывают случаи, когда вполне можно обойтись и программ¬ным вводом-выводом, однако все же для большинства операций ввода-вывода прерывания являются не чем иным, как суровой необходимостью. Они должны быть спрятаны как можно глубже в недрах операционной системы, чтобы о них было известно как можно меньшей части этой системы. Лучший способ их спрятать — это заблокировать тот драйвер, который начал операцию ввода-вывода, до тех пор пока не будет завершен ввод-вывод и не будет получено прерывание. Драйвер может заблокировать себя сам, вы¬полнив, к примеру, процедуру down на семафоре (семафор (semaphore) — тип переменной переменой. Значение семафора может быть равно 0, что будет свидетельствовать об отсутствии сохранен¬ных активизаций, или иметь какое-нибудь положительное значе¬ние, если ожидается не менее одной активизации.), или процедуру wait на переменной состояния, или процедуру receive на сообще¬нии, или еще что-либо подобное.
Когда происходит прерывание, то все необходимое для его обработки делает процедура — обработчик прерывания. Затем она может разблокировать ожидавший данного прерывания драйвер. В ряде случаев она просто выполняет процедуру up на семафоре. В других случаях вызывает процедуру signal для переменной условия в мониторе. Во всех остальных случаях она посылает заблокиро¬ванному драйверу сообщение. В любом случае заключительным действием прерывания будет предоставление возможности возоб¬новления работы ранее заблокированному драйверу. Такая модель лучше всего работает в том случае, если драйверы структурно от¬носятся к процессам ядра, имея собственные состояния, стеки и счетчики команд.
Разумеется, в действительности не все так просто. Обра¬ботка прерывания не ограничивается только его перехватом, вызо¬вом процедуры up для некого семафора, а затем выполнением ко¬манды IRET для возвращения из прерывания к предыдущему про¬цессу. На операционную систему возлагается куда более суще¬ственный объем работы. Сейчас будет дано лишь краткое описание этой работы в виде последовательности шагов, которые должны быть выполнены программным обеспечением после завершения аппаратного прерывания. Следует заметить, что подробности этой работы сильно зависят от конкретной операционной системы, по¬этому некоторые из перечисленных далее шагов конкретной ма¬шине могут и не понадобиться, в отличие от шагов, не попавших в этот перечень. Кроме того, реальные шаги на некоторых машинах могут следовать в ином порядке.

  1. Сохранить все регистры (включая PSW), которые еще не были сохранены аппаратурой, вызвавшей прерывание.
  2. Установить контекст для процедуры обработки прерыва¬ния. Здесь может быть задействована установка TLB, MMU и таблицы страниц.
  3. Установить стек для процедуры обработки прерывания.
  4. Послать подтверждение контроллеру прерываний. В отсутствие централизованного контроллера прерываний разрешить прерывания.
  5. Скопировать регистры из того места, где они были сохранены (возможно, из какого-нибудь стека), в таб¬лицу процессов.
  6. Запустить процедуру обработки прерывания, которая из¬влечет информацию из регистров контроллера устрой¬ства, вызвавшего прерывание.
  7. Выбрать следующий запускаемый процесс. Если прерывание привело к готовности какого-то ранее забло¬кированного процесса, имеющего высокий уровень при-оритета, то теперь может быть выбран запуск именно этого процесса.
  8. Установить контекст MMU для следующего запускае¬мого процесса. Могут потребоваться и некоторые уста¬новки TLB.
  9. Загрузить регистры нового процесса, включая его PSW.
  10. Запустить выполнение нового процесса.

Из всего этого можно понять, что обработка прерываний — весьма непростой процесс. К тому же она состоит из существенного количества команд центрального процессора, особенно на машинах с виртуальной памятью, которым необходимы установка странич¬ных таблиц или сохранение состояния MMU (например, битов R и M). На некоторых машинах при переключениях между пользова¬тельским режимом и режимом ядра необходимо также управлять TLB и кэшем центрального процессора, для чего также нужны до¬полнительные машинные циклы.

Драйверы устройств

Мы уже знаем, что у каждого контроллера есть ряд реги¬стров устройства, предназначенных для отправки ему команд, или ряд регистров устройства, используемых для считывания его состо¬яния, или и те и другие регистры. Число регистров устройства и характер команд очень сильно различаются в зависимости от кон¬кретного устройства. Например, драйвер мыши должен восприни¬мать информацию от мыши, сообщающую, насколько далеко она была перемещена и какие кнопки в данный момент были отпу¬щены. В отличие от этого драйвер диска должен знать все о секто¬рах, дорожках, цилиндрах, головках, перемещениях блока головок, электроприводах, временных показателях стабилизации головки и обо всех остальных механизмах, обеспечивающих нормальную ра¬боту диска. Несомненно, эти драйверы будут сильно отличаться друг от друга.
Поэтому для управления каждым подключенным к компью¬теру устройством ввода-вывода требуется специальная программа, учитывающая его особенности. Эта программа называется драйве¬ром устройства. Обычно она создается производителем устрой¬ства и поставляется вместе с этим устройством. Поскольку для каждой операционной системы нужны собственные драйверы, про¬изводитель устройства обычно поставляет драйверы для несколь¬ких наиболее популярных операционных систем.
Каждый драйвер устройства обычно управляет одним типом устройства или как максимум одним классом родственных устройств. Например, драйвер SCSI-диска обычно может управлять несколькими SCSI-дисками разного объема и разной скорости и, может быть, приводом Blu-ray-диска со SCSI-интерфейсом. А вот мыши и джойстики настолько отличаются друг от друга, что для них, как правило, требуются разные драйверы. Тем не менее техни¬чески вполне возможно создание одного драйвера устройства, управляющего несколькими разнородными устройствами. Но это в большинстве случаев не самая лучшая идея.
Но иногда совершенно разные устройства основаны на од¬ной и той же базовой технологии. Наверное, наиболее известным примером может послужить USB — технология последовательной шины, которая не зря называется универсальной. К USB-устрой¬ствам относятся диски, карты памяти, фотоаппараты, мыши, клави¬атуры, мини-вентиляторы, беспроводные сетевые карты, роботы, считыватели кредитных карт, аккумуляторные бритвы, измельчи¬тели бумаг, сканеры штрих-кодов и портативные термометры. Все они используют USB, хотя занимаются совершенно разными ве¬щами. Характерная особенность заключается в том, что из USB-драйверов обычно формируется стек, подобный TCP/IP-стеку в се¬тях. Внизу, как правило, в оборудовании, находится уровень USB-ссылок (последовательного ввода-вывода), обрабатывающий все, что касается аппаратуры, например сигнализацию и декодирование потоков сигналов в USB-пакеты. Он используется для более высо¬ких уровней, работающих с пакетами и функциями USB, общими для большинства устройств. И наконец, наверху над всем этим находятся высокоуровневые API-интерфейсы, например интер¬фейсы для накопителей, камер и т. д. Таким образом, у нас по-прежнему имеются отдельные драйверы устройств, несмотря на то что ими совместно используются части стека протокола.
Чтобы получить доступ к аппаратной части устройства, то есть к регистрам контроллера, драйвер устройства, как правило, должен быть частью ядра операционной системы, по крайней мере в существующих на сегодняшний день архитектурах. Но вообще-то можно создавать и драйверы, работающие в пространстве пользо¬вателя, используя при этом системные вызовы для чтения и записи регистров устройств. Такое решение позволит изолировать ядро от драйверов и драйверы друг от друга, устранив при этом основной источник системных сбоев — «сырые» драйверы, тем или иным образом мешающие работе ядра. Несомненно, это хороший выход из положения при создании высоконадежных систем. Примером системы, в которой драйверы устройств запускаются в качестве процессов пользователя, может послужить MINIX 3. Но поскольку большинство других операционных систем для настольных компь¬ютеров предполагают запуск драйверов в ядре, рассматриваться будет именно такая модель.
Так как разработчики любых операционных систем знают, что эти фрагменты программного кода (драйверы), созданные дру¬гими разработчиками, будут устанавливаться в их систему, им нужна такая архитектура, которая позволит подобную установку. А это значит, что должна быть вполне определенная модель того, чем занимается драйвер и как он взаимодействует со всей операцион¬ной системой. Как показано на рис. 56, драйверы устройств обычно размещаются ниже остальных компонентов операционной системы.

Обычно операционная система относит драйверы к одной из немногочисленных категорий. Самые распространенные категории — это драйверы блочных устройств, к ним относятся драйверы дисков, содержащих множество блоков данных, к которым можно обращаться независимо от всех остальных блоков, и драйверы сим¬вольных устройств, к которым относятся драйверы клавиатур и принтеров — устройств, которые генерируют или воспринимают поток символов.
Для многих операционных систем определены стандартный интерфейс, который должен поддерживаться всеми драйверами блочных устройств, и еще один стандартный интерфейс, который должен поддерживаться всеми драйверами символьных устройств. Эти интерфейсы состоят из нескольких процедур, которые могут вызываться всей остальной операционной системой для обращения к драйверу. К этим процедурам относятся, например, процедуры чтения блока (в блочных устройствах) или записи строки символов (в символьных устройствах).
В некоторых системах операционная система представляет собой единую программу в двоичных кодах, в которой содержатся все необходимые ей скомпилированные драйверы. Такая схема долгие годы была нормой для систем семейства UNIX, поскольку они работали в компьютерных центрах, где устройства ввода-вы¬вода менялись очень редко. При добавлении нового устройства си¬стемный администратор просто перекомпилировал ядро с новым драйвером для создания нового двоичного кода.
С наступлением эры персональных компьютеров с несмет¬ным количеством устройств ввода-вывода эта модель уже не рабо¬тает. Лишь немногие пользователи способны перекомпилировать или перекомпоновать ядро, даже если у них будут исходные коды или объектные модули, что случается довольно редко. Вместо этого операционные системы, начиная с MS-DOS, перешли к модели, в которой драйверы стали динамически загружаться в систему в про¬цессе работы. Управление загрузкой драйверов ведется в разных системах по-разному.
На драйвер устройства возлагается несколько функций. Наиболее очевидные из них — восприятие абстрактных запросов на чтение и запись от независимого от конкретных устройств про¬граммного обеспечения, находящегося выше них по уровню, и от¬слеживание порядка их выполнения. Но на них возлагается также ряд других функций. Например, драйвер должен при необходимо¬сти инициализировать устройство. Он может понадобиться также для управления энергопотреблением устройства и регистрации со¬бытий.
Многие драйверы устройств имеют сходную общую струк¬туру. Типичный драйвер начинает свою работу с проверки прием¬лемости входных параметров. Если они неприемлемы, возвраща¬ется сообщение об ошибке. Если с параметрами все в порядке, мо-жет понадобиться перевод абстрактных понятий в конкретные. Для драйвера диска это может означать преобразование обычного но¬мера блока в номера головки, дорожки, сектора и цилиндра, отно¬сящихся к геометрии диска.
Затем драйвер может проверить, используется ли устрой¬ство в данный момент. Если оно используется, запрос будет по¬ставлен в очередь для последующей обработки. Если устройство простаивает, проверяется состояние аппаратуры, чтобы определить, может ли запрос быть обработан. Перед началом передачи данных может понадобиться включить устройство или запустить его двига¬тель. Как только устройство включится и будет готово к работе, им можно будет управлять.
Управление устройством означает выдачу в его адрес по¬следовательности команд. Именно драйвер определяет последова¬тельность команд в зависимости от того, что должно быть сделано. После того как драйвер поймет, какие команды он собирается вы¬дать, он начнет записывать их в регистры контроллера устройства. После записи каждой команды в контроллер может потребоваться проверка того, принял ли контроллер команду и готов ли к приему следующей команды. Эта последовательность повторяется до тех пор, пока не будут выданы все команды. Некоторым контроллерам можно указывать на связанный список команд (в памяти) и предпи¬сывать самостоятельное чтение и обработку этих команд без даль¬нейшей помощи со стороны операционной системы.
После того как команды были выданы, может сложиться одна из двух ситуаций. В большинстве случаев драйвер должен ждать, пока контроллер не сделает в его интересах какую-нибудь работу, поэтому он самоблокируется до тех пор, пока не поступит прерывание на его разблокировку. Но в других случаях операция завершается без задержки и драйверу не нужно блокироваться. В качестве примера последней ситуации можно привести прокрутку экрана в символьном режиме, требующую лишь записи нескольких байтов в регистры контроллера. Для этого не нужно никаких меха¬нических перемещений, поэтому вся операция может быть завер¬шена за несколько наносекунд.
В первом случае заблокированный драйвер будет активизи¬рован прерыванием. Во втором случае он никогда не будет перехо¬дить в неактивное состояние. В любом случае по завершении опе¬рации драйвер должен провести проверку на отсутствие ошибок. Если все в порядке, драйвер может получить данные (например, только что считанный блок) для передачи программному обеспече¬нию, не зависящему от применяемого устройства. И наконец, он возвращает вызывавшей его программе определенную информацию о состоянии устройства, наличии или отсутствии ошибок. Если в очереди были какие-нибудь другие запросы, то теперь один из них может быть выбран и запущен на выполнение. Если запросов в очереди не было, драйвер блокируется в ожидании следующего за¬проса.
Эта упрощенная модель дает весьма приблизительное поня¬тие о реальной работе. На самом деле программный код из-за мно¬жества различных факторов куда сложнее. Прежде всего, устрой¬ство ввода-вывода может завершить свою работу и прервать работу драйвера. Прерывание может стать причиной запуска драйвера. Между прочим, оно может вызвать запуск текущего драйвера. К примеру, при обработке сетевым драйвером входящего пакета мо¬жет прибыть еще один пакет. Следовательно, драйверы должны быть реентерабельными, то есть работающий драйвер еще до за¬вершения первого вызова должен ожидать повторного вызова.
В системе, допускающей горячее подключение, устройство может быть добавлено или удалено во время работы компьютера. В результате, пока драйвер занят чтением с какого-нибудь устрой¬ства, система может ему сообщить, что пользователь внезапно уда¬лил это устройство из системы. Драйверу придется не только пре¬рвать текущую передачу данных, не повредив при этом каких-либо структур данных ядра, но и умудриться элегантно удалить из си¬стемы все отложенные запросы для только что удаленного устрой¬ства и сообщить плохие новости пославшим их программам. Более того, неожиданное добавление новых устройств может заставить ядро перераспределить ресурсы (например, линии запроса преры¬ваний), забирая у драйвера старые и предоставляя вместо них но¬вые.
Драйверам не разрешается делать системные вызовы, но ча¬сто возникает необходимость взаимодействовать с остальной ча¬стью ядра. Обычно им разрешается вызывать определенные проце¬дуры ядра. Например, для использования в качестве буферов выде¬ленных аппаратуре страниц памяти драйверы вызывают проце¬дуры, которые занимаются их выделением и освобождением. Дру¬гие полезные вызовы нужны для управления MMU, таймерами, контроллером DMA, контроллером прерываний и т. д.

Программное обеспечение ввода-вывода, не зависящее от конкретных устройств

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

Таблица 9. Функции программного обеспечения, не зависящего от конкретных устройств

Предоставление унифицированного интерфейса для драйверов устройств
Буферизация
Сообщение об ошибках
Распределение и освобождение выделенных устройств
Предоставление размера блока, не зависящего от конкретных устройств

Предоставление унифицированного интерфейса для драйверов устройств

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

В противоположность этому на рис. 57, б показана другая конструкция, в которой у всех драйверов имеется одинаковый ин¬терфейс. Теперь стало намного проще подключить новый драйвер, обеспечив его соответствие интерфейсу драйверов. Также это озна¬чает, что создатели драйверов знают, чего от них ожидают. Факти¬чески не все устройства абсолютно одинаковы, но обычно прихо¬дится иметь дело лишь с небольшим количеством типов устройств, и даже они в целом практически одинаковы.
Все это работает следующим образом. Для каждого класса устройств, таких как диски или принтеры, операционной системой определяется набор функций, которые драйвер должен поддержи¬вать. Для диска в этот набор будут входить не только чтение и за¬пись, но и включение и выключение электропитания, форматиро¬вание и другие присущие диску операции. Зачастую драйвер со¬держит таблицу с указателями на эти функции. При загрузке драй¬вера операционная система записывает адрес таблицы указателей на функции, чтобы, когда потребуется вызвать одну из этих функ¬ций, она могла выполнить опосредованный вызов через таблицу. Таблица указателей на функции определяет интерфейс между драй-вером и всей остальной операционной системой. Все устройства определенного класса (диски, принтеры и т. д.) должны соответ¬ствовать этому условию.
Другим аспектом использования унифицированного интер¬фейса является способ присвоения имен устройствам ввода-вывода. Независимое от устройств программное обеспечение берет на себя отображение символических имен устройств на соответствующие драйверы. Например, в системе UNIX имя устройства, такое как /dev/disk0, однозначно определяет i-узел для специального файла, и этот i-узел содержит старший номер устройства, который использу¬ется для определения соответствующего драйвера. В i-узле содер¬жится также младший номер устройства, который передается в ка¬честве параметра драйверу, чтобы определить конкретное устрой¬ство для проведения операции чтения или записи. У всех устройств есть старший и младший номера, и доступ ко всем драйверам осу-ществляется с использованием старшего номера, по которому про¬исходит выбор драйвера.
С присвоением имен тесно связан вопрос защиты. Как си¬стема препятствует доступу пользователей к тем устройствам, к которым они не имеют права доступа? Как в UNIX, так и в Windows устройства появляются в файловой системе в виде поиме¬нованных объектов, что означает распространение обычных правил защиты файлов также на устройства ввода-вывода. Системный ад¬министратор может в таком случае установить соответствующие права доступа для каждого устройства.

Буферизация

Буферизация по многим причинам также является актуаль¬ным вопросом как для блочных, так и для символьных устройств. Чтобы понять, в чем состоит одна из таких причин, рассмотрим процесс, которому необходимо прочитать данные, получаемые от ADSL-модема, который многие используют дома для связи с Ин¬тернетом. По одной из возможных стратегий работы с поступаю¬щими символами нужно заставить пользовательский процесс осу¬ществить системный вызов read и заблокироваться в ожидании од¬ного символа. При этом прерывание возникает по случаю поступ¬ления каждого символа. Процедура обработки прерывания передает символ пользовательскому процессу и снимает с него блокировку. Поместив куда-нибудь символ, процесс переходит к чтению следу¬ющего символа и снова блокируется. Эта модель показана на рис. 58, а.

Проблема реализации такого способа заключается в том, что пользовательский процесс должен возобновляться для каждого поступающего символа. Из-за низкой эффективности многократ¬ных краткосрочных запусков процесса это далеко не самая лучшая модель.
Улучшенный вариант показан на рис. 58, б. Здесь пользова¬тельский процесс предоставляет буфер объемом n символов и вы¬полняет чтение такого же количества символов. Процедура обра¬ботки прерывания помещает поступающие символы в этот буфер до тех пор, пока он не заполнится. Затем она возобновляет работу пользовательского процесса. Эта схема работает намного эффек¬тивнее предыдущей, но у нее есть один недостаток. Что получится, если буфер выйдет за границу страницы при поступлении очеред¬ного символа? Буфер будет зафиксирован в памяти, но если множе¬ство процессов начнет фиксировать страницы в памяти, то запас доступных страниц сократится и производительность резко сни¬зится.
Другой подход предусматривает создание буфера внутри ядра и возложение на обработчик прерывания обязанности поме¬щать символы в этот буфер (рис. 58, в). Когда этот буфер запол¬нится, то вводится, если нужно, страница с буфером пользователя и буфер копируется в нее за одну операцию. Эта схема работает еще более эффективно.
Но даже эта улучшенная схема не обходится без проблемы. Что произойдет с символами, которые поступят в тот момент, когда страница с пользовательским буфером будет извлекаться с диска? Поскольку буфер заполнен, его будет некуда поместить. Выходом из положения может стать второй буфер ядра. Как показано на рис. 58, г, после заполнения первого буфера, но перед его опустошением используется второй буфер. Когда второй буфер заполнится, его можно будет скопировать в буфер пользователя (если предполо¬жить, что пользователь просил об этом). Пока второй буфер будет копироваться в пространство пользователя, для новых символов может использоваться первый буфер. Таким образом, два буфера работают по очереди: пока один из них копируется в пространство пользователя, другой аккумулирует новые поступления. Эта схема называется двойной буферизацией.
Другой широко распространенной формой буферизации яв¬ляется использование кольцевого буфера. Он состоит из области памяти и двух указателей, один из которых указывает на следую¬щее свободное слово, в которое можно поместить новые данные, а другой — на первое слово тех данных в буфере, которые еще не были из него выведены. Во многих случаях аппаратура по мере до¬бавления данных (например, только что поступивших из сети) пе¬редвигает вперед первый указатель; операционная система, по мере того как она выводит из буфера и обрабатывает данные, переме¬щает вперед второй указатель. Оба указателя ходят по кругу, пере¬ходя обратно к нижним адресам буфера, как только достигнут его верхних адресов.
Буферизация играет важную роль и при выводе данных. Рассмотрим, к примеру, как осуществляется вывод данных на мо¬дем без буферизации с использованием модели, показанной на рис. 58, б. Пользовательский процесс, чтобы вывести n символов, осу-ществляет системный вызов write. В этот момент у системы есть два варианта выбора. Она может заблокировать пользовательский процесс до тех пор, пока не будут записаны все символы, но при использовании телефонной линии это займет очень много времени. Система может также немедленно освободить пользовательский процесс и заняться операциями ввода-вывода, пока этот процесс будет заниматься какими-то другими вычислениями, но это приве¬дет к еще более серьезной проблеме. Как пользовательский процесс узнает, что вывод данных был завершен и он может опять восполь¬зоваться буфером? Система может выставить сигнал или про¬граммное прерывание, но такой стиль программирования слишком сложен и склонен к созданию состязательных условий. Намного более удачным решением будет копирование ядром данных в бу¬фер ядра аналогично варианту, показанному на рис. 58, в (но в об¬ратную сторону), и сразу после этого разблокирование вызываю¬щего процесса. Теперь неважно, когда фактически завершится про¬цесс ввода-вывода. Пользовательский процесс с момента разблоки¬рования может использовать буфер повторно.
Буферизация является широко используемой технологией, но у нее имеются и недостатки. Если данные будут подвергаться буферизации слишком часто, упадет производительность. Рассмот¬рим, к примеру, сеть, показанную на рис. 59. Здесь пользователь¬ский процесс осуществляет системный вызов для записи данных по сети. Ядро копирует пакет данных в буфер ядра, позволяя пользо¬вательскому процессу немедленно возобновить работу (шаг 1). Те¬перь пользовательская программа может использовать буфер по¬вторно.
Когда вызывается драйвер, он копирует пакет в контроллер для его последующего вывода (шаг 2). Причина, по которой он не осуществляет вывод в сеть непосредственно из памяти ядра, со¬стоит в том, что как только будет запущена передача пакета, она должна продолжаться на постоянной скорости. Драйвер не может гарантировать, что он будет получать доступ к памяти на постоян¬ной скорости, поскольку множество циклов обращения к шине мо¬гут отвлекать на себя каналы DMA и другие устройства ввода-вы¬вода. Неудача при своевременном получении слова приведет к порче пакета. Эту проблему можно устранить за счет буферизации пакета внутри контроллера.

После того как пакет будет скопирован во внутренний бу­фер контроллера, он копируется в сеть (шаг 3). Биты поступают получателю вскоре после их отправки, поэтому сразу же после от­правки последнего бита этот бит поступает получателю, у которого пакет попадает в буфер контроллера. Затем пакет копируется в бу­фер ядра получателя (шаг 4). И наконец он копируется в буфер процесса получателя (шаг 5). Обычно после этого получатель по­сылает подтверждение. Когда отправитель получает подтвержде­ние, он имеет возможность послать следующий пакет. Но при этом следует понимать, что операции копирования существенно сни­жают скорость передачи данных, поскольку шаги должны осу­ществляться последовательно.

Распределение и высвобождение выделенных устройств

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

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

Предоставление размера блока, не зависящего от конкретных устройств

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

Программное обеспечение ввода-вывода, работающее в пространстве пользователя

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

count = write(fd, buffer, nbytes);

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

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

printf(«Квадрат числа %3d равен %6d\n», i, i*i);

Он форматирует 14-символьную строку «Квадрат числа», за кото­рой следует значение i в виде 3-символьной строки, затем 7-сим­вольная строка «равен», за ней — i2 в виде шести символов и, наконец, символ перевода строки.

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

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

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

Спулинг используется не только при работе с принтерами, но и в других ситуациях применения операций ввода-вывода. Например, при передаче файла по сети часто задействуется сетевой демон. Чтобы куда-нибудь отправить файл, пользователь помещает его в сетевой каталог спулинга. Чуть позже сетевой демон извле­кает этот файл и передает его по сети. Конкретным примером си­стемы, в которой файлы передаются с помощью спулинга, может послужить USENET News (стала частью Google Groups). Эта сеть состоит из нескольких миллионов машин по всему миру, обмени­вающихся данными через Интернет. Существуют тысячи новост­ных групп, затрагивающих обширную тематику. Для публикации нового сообщения пользователь вызывает программу новостей, ко­торая принимает публикуемое сообщение, а затем помещает его в каталог спулинга для последующей отправки в адрес других ма­шин. И вся новостная система работает вне операционной си­стемы1.

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

Оставить комментарий

avatar
  Подписаться  
Уведомление о