Skip to content

Latest commit

 

History

History
257 lines (153 loc) · 38.8 KB

Input-Output.MD

File metadata and controls

257 lines (153 loc) · 38.8 KB

Ввод-Вывод (IO)

Содержание:


Аппаратная часть

Типы устройств ввода-вывода

Устройства ввода-вывода можно разделить на 2 категории:

  • Блочные устройства
  • Символьные

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

Символьные устройства ввода-вывода принимают или выдают поток символов, а не блоки. К такой информации невозможно применять операции позиционирования и произвольный доступ невозможен, так как данные не адресуются. Например: принтер, мышь (в качестве устройства указателя).

Устройство обычно разделяется на 2 части - само устройство и контроллер устройства. Контроллер находится на системной плате и в него вставляется кабель от самого устройства. Благодаря тому, что определенные группы устройств созданы по принятым стандартам, 1 контроллер на мат. плате может обслуживать множество устройств.

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

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


Прерывания

Прерывание — это событие, при наступлении которого процессор должен приостановить выполнение текущего процесса, сохранить его состояние и начать выполнять другой процесс, называемый обработчиком прерывания (Interrupt Handler или Interrupt Service Routine, ISR). После завершения обработки прерывания состояние предыдущего процесса должно быть восстановлено и работа должна продолжиться с той команды, где она остановилась.

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

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

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

Типы прерываний:

  1. Внешние (аппаратные)
  2. Внутренние - исключения
  3. Программные - не являются на самом деле прерываниями, а инициируются с помощью специальной команды процессора, вызывающей прерывание. Используются для перехода на новую последовательность инструкций.

Почему необходимы программные прерывания?

Программные прерывания используются для реализации системных вызовов, где требуется переход в пространство ядра.

Почему требуется прерывание для переход в режим ядра?

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

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

Общение процессора с контроллером происходит через регистры контроллера. Есть 3 способа общения с ними:

  1. Формируется пространство портов ввода-вывода: так пространства памяти и портов являются отдельными адресными пространствами. Например:

    IN R0,4 - данная команда читает порт с адресом 4 и помещает в регистр процессора R0

    MOV R0,4 - данная команда читает адрес памяти 4 и помещает в регистр процессора R0

  2. Всё адреса портов отображаются на оперативную память

  3. Гибридный вариант - адреса буферов контроллеров находятся в памяти, а адреса портов ввода-вывода находятся в отдельном пространстве портов ввода-вывода


Процесс общения процессора и устройства ввода-вывода

Как только процессору необходимо прочитать слово из памяти или из порта ввода-вывода на контроллере, процессор выставляет адрес на адресной линии шины, а затем выставляет сигнал READ на линии управления шины. Затем, на сигнальной линии шины, указывается, какое именно пространство нужно (память, которое является одним адресным пространством, или же адресное пространство портов ввода-вывода), и запрос обработает разное устройство в зависимости от этого - память или само устройство. Если используется только адресное пространство памяти, то каждое устройство сравнивает адрес на адресной линии шины со своим диапазоном обслуживаемых этим устройством адресов. Если адрес попадает в диапазон адресов памяти, то запрос обслужит память, если адрес попадает в диапазон адресов контроллера, то устройство обслужит запрос. Поскольку адресов, выделенных одновременно и памяти, и устройству, не бывает, то недоразумений не возникает.

Достоинства и недостатки отображения на память:

  • Если для обращения напрямую к пространству портов ввода-вывода требуются специальные вставки кода на ассемблере, что влечёт дополнительные расходы, то обращение к памяти может быть написано полностью на C или C++.
  • Отсутствует необходимость механизма защиты осуществления ввода вывода со стороны пользовательских процессов. Достаточно отделить пространство регистров управления контроллеров от виртуального пространства пользователя.
  • Любая команда, которая работает с адресом памяти, может работать и с регистрации управления контроллера, то есть не требуются специальные команды. Например, команда TEST, которая проверяет слово памяти на равенство нулю (использовалась в алгоритме спинлока, мьютекса), может быть использована и с регистром управления контроллера на проверку занятости устройства. Если бы не было отображения на пространство памяти, то пришлось бы сначала считать регистр контроллера в процессор, и только потом проверить его.

Недостатки данного метода:

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

DMA

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

Рассмотрим, как происходит чтение данных с диска без использования DMA:

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

С использованием DMA:

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

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

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

Кроме того, центральный процессор работает намного быстрее чем DMA контроллер, и зачем заставлять ждать быстрый процессоре окончания работы медленного DMA контроллера.



Программная часть (операционная система)

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

Способы передачи данных

  • Синхронный (блокирующий)
  • Асинхронный (управляемый с помощью прерываний)

Рассмотрим каждый способ передачи данных:

Синхронный

Применение данного способа значительно облегчает разработку программ. Когда программа инициирует чтение данных с помощью read(), поток программы останавливает работу и будет ожидать результата операции.

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

Асинхронный

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


Способы осуществления ввода-вывода

Программный ввод-вывод

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

Ввод-вывод управляемый прерываниями

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

Ввод-вывод с помощью DMA-контроллера

В данном варианте ввода-вывода вся работа по записи данных возлагается с процессора на DMA контроллер и снижается количество прерываний.



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

Самый низший уровень - это аппаратура, а над ней идет программное обеспечение, которого коснулись ранее, а здесь рассмотрим структурированно.

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

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

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


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

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

Соответственно с типами оборудования существуют 2 типа драйверов:

  • Драйвера блочных устройств
  • Драйвера символьных устройств

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


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

Кроме программного обеспечения, которое пишутся под конкретные устройства ввода-вывода (драйвера устройств), требуется и другое программное обеспечение, которое не зависит от конкретных устройств.

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

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

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


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

Это библиотеки, которые предоставляют различные функции ввода-вывода разработчику, которые внутри используют все то же единое API операционной системы. Например, write(), read() в Linux. Также, есть еще и более разнообразные функции языков: например, std::cout, std::cin, printf, scanf, которые еще более расширяют возможности ввода-вывода.

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


Buffered I/O

Buffered I/O - это использование буфера в пользовательском пространстве для хранения результатов I/O операций перед их непосредственным их выполнением.

Данная техника позволяет сильно повысить производительность программы при работе с вводом/выводом за счет того, что осуществляется сильно меньше системных вызовов (syscall, вызов к ядру).

Таким образом, операция write(buf) с применением buffered I/O не будет сразу выполнена, а переданные для записи данные buf сохранится только в буфере в пользовательском пространстве, но не на устройстве, на которое направлена операция (например, диск). Обычно, этот буфер будет записан на диск, когда он будет полностью заполнен. Операция read(&buf) же, прочитает максимально возможное кол-во данных с диска в буфер в памяти, а не только столько, сколько длина исходного буфера buf. Далее, при следующих вызовах не будет происходить сисколлов, так как данные для чтения уже имеются в памяти.

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

Все выше - исключительно методы языка, но не системы. Ниже рассмотрим методы Buffered I/O системы.


Disk buffering, buffer cache

Также, существует внутренняя оптимизация ОСи. Даже если не используется буфер в пользовательском пространстве, и происходят частые сисколлы, система все равно не будет сразу же писать данные на диск. Данная техника называется disk buffering, а память используемая для реализации данной оптимизации - page cache (данный буфер хранится в оперативной памяти).

Система будет хранить в оперативной памяти (на стороне ядра) буфер, который будет сбрасываться на диск только при достижении определенного размера или когда решит система. Буфер сбрасывается на диск только когда он помечен dirty, то есть он обновился, и в нем есть новые данные.

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

Ясно одно, что Buffered I/O увеличивает производительность всех input вызовов, кроме самого первого. Самый первый здесь имеется в виду - что первый вызов, когда буфер был пуст.

Существует возможность миновать данный буфер и писать данные напрямую на диск (write through) - DIRECT_O.