Одна из сложнейших задач в реализации переносимого ядра - найти хорошие абстракции для близких по смыслу операций, часто имеющих сильно отличающиеся машинно-зависимые реализации. Это особенно важно для современных компьютеров, которые часто используют общие архитектурные компоненты, такие как шина PCI.
В этом документе описывается, зачем нужна абстракция для машинно-независимого отображения прямого доступа в память, проектные решения такой абстракции и реализация этой абстракции в ядрах NetBSD/alpha и NetBSD/i386.
NetBSD - современная переносимая операционная система типа Unix, которая в настоящее время работает на восемнадцати платформах на основе девяти процессорных архитектур. Некоторые из этих платформ, включая Alpha и i3862, используют шину PCI в качестве общего архитектурного компонента. Для совместного использования драйверов устройств PCI на разных платформах нужно придумать абстракцию, которая скроет подробности доступа к шине. Есть два вида особенностей, которые нужно скрыть: доступ процессора к устройствам на шине (bus_space) и доступ устройства к памяти компьютера (bus_dma). Здесь мы обсудим только bus_dma. bus_space - отдельная сложная тема, выходящая за рамки этого документа.
Есть два основных вида особенностей прямого доступа в память, которые должны быть скрыты от основной части драйвера устройства. Первый вид - особенности компьютера, включая особенности отображения физической памяти системы (и механизмов прямого доступа в память, используемых для такого отображения) и семантику кэша. Второй вид - особенности шины, включая возможности и ограничения определённой шины, к которой подключено устройство, такие как режим блочной передачи при прямом доступе в память и ограничения адресных линий.
В перечисленных выше примерах платформ есть как минимум три различых механизма, используемых для осуществления прямого доступа в память. Первый используется платформой i386. Этот механизм можно описать как "что видишь, то и получишь": устройство использует для операций прямого доступа в память те же адреса, которые использует при обращении к памяти процессор компьютера.
Второй механизм, задействованный в Alpha, очень похож на первый. При непосредственном отображении адреса, используемые процессором для работы с памятью, и адреса на шине, используемые устройствами для прямого доступа в память, смещены друг относительно друга на некоторую величину.
Третий механизм - это векторное отображение. Он задействует модуль управления памятью, который осуществляет преобразование адресов прямого доступа в физические адреса памяти компьютера. Этот механизм также используется в Alpha, потому что физическое адресное пространство на платформах Alpha иногда бывает значительно больше 32-битного адресного пространства, которое поддерживается существующими в настоящее время устройствами PCI.
В Alpha второй и третий механизмы сочетаются при помощи окон прямого доступа в память. Микросхема, реализующая шину PCI на конкретной платформе, имеет по меньшей мере два таких окна. Каждое окно может быть настроено для непосредственного или векторного отображения. Окна выбираются в зависимости от типа операции прямого доступа, типа шины и физического диапазона адресов памяти компьютера, к которым происходит доступ.
Эти концепции применимы и к платформам, отличным от перечисленных выше, и к шинам, отличным от PCI. Похожие проблемы существуют с шиной TurboChannel на DEC-станциях и ранних системах Alpha, и с шиной Q-bus, используемой на некоторых серверах DEC с процессорами MIPS и серверах на основе VAX.
Устройствам, желающим выполнить оперцию прямого доступа в память, важно учитывать поведение системного кэша компьютера. Некоторые системы способны осуществлять прямой доступ к памяти с одновременным обновлением кэша. На таких системах часто происходит сквозная запись (то есть сохранение данных при записи и в кэш и в память компьютера), или же кэш поддерживает специальную логику прослушивания, которая обнаруживает доступ к области памяти, для которой содержимое кэша становится неактуальным (что автоматически приводит к очистке кэша). Другие системы не способны осуществлять прямой доступ к памяти с одновременным обновлением кэша. На этих системах программное обеспечение должно явным образом вытеснить данные кэша перед операциями прямого доступа память-устройство, а также отмечать данные кэша как недействительные перед операциями прямого доступа устройство-память.
Кроме сокрытия специфичных для платформы особенностей прямого доступа в память одной шины желательно обобщить как можно больше кода драйверов устройств, которые можно подключить к нескольким шинам. Хороший пример - семейство SCSI-адаптеров BusLogic. Это семейство поставляется в вариантах для шин ISA, EISA, VESA и PCI. Хотя некоторые особенности, такие как обнаружение устройств и инициализация прерываний, у разных шин отличаются, подавляющая часть кода драйверов этого семейства устройств идентична.
Семейство SCSI-адаптеров BusLogic - это пример так называемых хозяев шины. При операциях прямого доступа в память устройство само производит все действия с шиной и памятью компьютера без участия третьей стороны. Такие устройства сами выставляют адрес на адресных линиях шины, выполняют операцию чтения или записи, увеличивают адрес и так далее до завершения операции. Поскольку устройство использует адресные линии шины, то диапазон физических адресов компьютера, к которому устройство может получить доступ, ограничен лишь количеством таких линий. На шине PCI, где есть по меньшей мере 32 адресные линии, устройство может обратиться ко всему физическому адресному пространству 32-битной архитектуры, такой как i386. Однако у шины ISA есть лишь 24 адресных линии. Это означает, что устройство сможет получить прямой доступ только к 16 мегабайтам физического адресного пространства.
Обычно для решения проблемы ограниченного количества адресных линий используют промежуточный буфер. Промежуточный буфер - дополнительная область памяти, находящаяся в диапазоне адресов, доступных устройству. При передаче память-устройство процессор копирует данные в промежуточный буфер, после чего начинается операция прямого доступа в память. И наоборот, при передаче устройство-память сначала выполняется операция прямого доступа в память, после чего процессор копирует данные из промежуточного буфера.
Хотя промежуточный буфер прост в реализации, это не самый элегантный способ решения проблемы ограниченного количества адресных линий. Например, на Alpha для преобразования физических адресов памяти в адреса шины, доступные для устройства, можно воспользоваться векторным отображением. Это решение позволяет достичь более высокой производительности за счёт избежания копирования данных, а также менее затратно с точки зрения использования памяти.
Если вернуться к примеру SCSI-адаптеров BusLogic, то было бы нежелательно помещать в основную часть драйвера этих устройств знания об особенностях непосредственного отображения, векторного отображения и о промежуточном буфере. Очевидно нужна абстракция, которая скроет эти особенности и предоставит однородный интерфейс вне зависимости от используемого механизма прямого доступа в память.
Скрыть особенности компьютера и шины довольно просто. Поддержка механизмов прямого соответствия и непосредственного отображения тривиальна. Поддержка векторного отображения также очень проста, если использовать состояние, хранимое на машинно-зависимом уровне кода. Наличие и поведение кэшей также легко учесть при помощи четырёх операций "синхронизации". И как только мы справимся с кэшем, промежуточный буфер оказывается простым, если посмотреть на него как на кэш, не обновляемый при прямом доступе к памяти. К сожалению, хотя каждая из этих операций довольно проста, в традиционных ядрах для них нет достаточно абстрактного интерфейса. А это значит, что драйверы устройств в таких традиционных ядрах должны явным образом учитывать каждый случай.
Кроме предоставления интерфейса к этим операциям исчерпывающий фреймворк для прямого доступа в память должен также уметь работать со структурами буферов данных и управлять памятью, безопасной для прямого доступа.
В ядре BSD есть три структуры буферов данных. Первая - это простой линейный буфер в виртуальном пространстве, выделяемый распределителем памяти ядра общего назначения. Например, это различные буферы и области данных, используемые для реализации буферного кэша файловой системы. Вторая - это цепочка mbuf. Обычно mbuf используется в коде, реализующем межпроцессное и сетевое взаимодействие. Цепочка небольших буферов снижает фрагментацию памяти и позволяет легко добавлять заголовки пакетов. Третья - это структура uio. Эта структура описывает программный вектор адресного пространства ядра или адресного пространства определённого процесса. Чаще всего она используется системными вызовами read(2) и write(2). Хотя драйвер устройства мог бы трактовать две более сложные структуры буферов как несколько простых линейных буферов, это нежелательно с точки зрения лёгкости сопровождения кода. Код для работы с такими структурами буферов данных может быть сложным, особенно с точки зрения обработки ошибок.
Обычно нужно чтобы область прямого доступа отображалась в адресное пространство ядра. Кроме того, в современных операционных системах обычно имеется реализация оптимизированного интерфейса ввода-вывода для пользовательских процессов, предоставляющая возможность отображать области прямого доступа устройства в адресное пространство процесса. Хотя такие средства отчасти доступны для устройств символьного ввода-вывода через двойное отображение пользовательского буфера в адресное пространство ядра, интерфейс недостаточно обобщён и потребляет ресурсы ядра. Что касается структуры uio, то она способна обращаться к буферам в адресном пространстве процесса. Однако для определённых целей желательно было бы использовать альтернативный формат данных, такой как линейный буфер. Для реализации этого фреймворк прямого доступа в память должен иметь доступ к структурам виртуальной памяти процесса.
Также может быть желательно, чтобы буферы прямого доступа не отображались в какое-либо адресное пространство. Очевидный пример - это перехват кадров. Устройствам для захвата видео часто требуются огромные физически непрерывные области памяти, чтобы сохранять в них захваченные изображения. На некоторых архитектурах отображение виртуального адресного пространства обходится дорого. Приложение может пожелать выделить для устройства огромный буфер, позволить ему непрерывно обновлять содержимое этого буфера, а затем в любой момент времени отображать в адресное пространство лишь небольшие области этого буфера. Поскольку не нужно отображать в виртуальное адресное пространство весь буфер, фреймворк прямого доступа в память должен предоставить интерфейс для использования в операциях прямого доступа буферов, не отображённых в какое-либо адресное пространство.
Исчерпывающий фреймворк прямого доступа в память должен также предоставлять определённые средства управления памятью. Самое очевидное из них - это метод выделения (и освобождения) памяти безопасной для прямого доступа. Слова "безопасной для прямого доступа" означают возможность указать набор атрибутов, которым должна соответствовать память. Во-первых, такая память должна располагаться по адресам, доступным шине. Она также должна быть выделенной таким образом, чтобы не превысить количество физических сегментов3, указанных вызывающей стороной.
Чтобы ядро получило доступ к памяти, безопасной для прямого доступа, метод должен отобразить эту память в виртуальное адресное пространство ядра. Сделать это довольно просто, за одним исключением. На некоторых платформах, где кэш не обновляется при операциях прямого доступа в память, вымывание кэша обходится очень дорого. Однако в некоторых случаях возможно отметить виртуальные отображения памяти как не кэшируемые или обращаться к физической памяти через некэшируемый сегмент памяти по адресу непосредственного отображения. Для учёта этих обстоятельств функции отображения памяти можно дать подсказку, которая укажет что пользователь этой памяти желает избежать дорогостоящих операций вымывания данных из кэша.
Чтобы процесс смог оптимизировать ввод-вывод, нужно дать ему возможность отобразить в своё адресное пространство память, безопасную для прямого доступа. Удобнее всего воспользоваться для этого входной точкой mmap() драйвера устройства. Поэтому фреймворк должен взаимодействовать с устройством подкачки4 подсистемы виртуальной памяти для отображения прямого доступа в память.
По возможности фреймворк прямого доступа в память может объединять схожие по смыслу операции или концепции, но архитектура полученного фреймворка должна учитывать все эти требования. В следующем разделе описан интерфейс такого фреймворка.
Машинно-независимый интерфейс NetBSD для доступа к шине часто называют bus.h5. Далее следует описание bus_dma - его части, касающейся прямого доступа в память. Эта часть интерфейса состоит из трёх типов данных, относящихся к прямому доступу, и тринадцати функций. Кроме того интерфейсы bus_dma и bus_space используют два общих типа данных.
Функции интерфейса bus_dma делятся на две категории: функции отображения и функции управления памятью. Сами функции могут быть реализованы в виде макросов cpp(1).
Первый из двух типов данных, который используется также в интерфейсе bus_space, - это тип bus_addr_t. Этот тип представляет адрес шины устройств, который будет использоваться процессором или при прямом доступе и который должен быть достаточно большим, чтобы в нём уместился самый большой адрес системной шины. Второй из двух типов - это тип bus_size_t, который представляет размер диапазона адресов шины.
Реализация прямого доступа в память для заданного сочетания компьютера и шины описывается типом bus_dma_tag_t. Это непрозрачный тип, передаваемый средствам автоконфигурирования машинно-зависимого кода. Слой шины в свою очередь передаёт его драйверам устройств. Первый аргумент каждой функции в этом интерфейсе имеет тип bus_dma_tag_t.
Отдельные сегменты прямого доступа описываются типом bus_dma_segment_t. Этот тип является структурой с двумя публично-доступными полями. Первое поле называется ds_addr, имеет тип bus_addr_t и содержит адрес сегмента прямого доступа. Второе поле называется ds_len, имеет тип bus_size_t и содержащит длину сегмента.
Третий, и пожалуй самый важный, тип данных - это bus_dmamap_t. Этот тип является указателем на структуру, описывающую отдельные отображения прямого доступа. Эта структура содержит три публичных поля. Первое поле называется dm_mapsize, имеет тип bus_size_t и описывает длину отображения, если оно действительно. dm_mapsize со значением 0 указывает, что отображение недействительно. Второе поле называется dm_nsegs, имеет тип int и содержит количество сегментов прямого доступа, принадлежащих отображению. Третье публичное поле называется dm_segs, и является массивом или указателем на массив структур типа bus_dma_segment_t.
Кроме этих типов данных интерфейс bus_dma таже определяет набор флагов, передаваемых некоторым функциям интерфейса. Флаги BUS_DMA_WAITOK и BUS_DMA_NOWAIT указывают функции, что ей разрешено или запрещено ожидание доступности ресурсов6. Есть также четыре флага, начиная с BUS_DMA_BUS1 и до BUS_DMA_BUS4, которые зарезервированы для различных шин и предназначены для обозначения особенностей этой шины. Например, возможность использовать 32-битные адреса прямого доступа для устройств на шине VESA. Хотя ядро считает такие устройства логически подключенными к шине ISA, на них не действует ограничение её адресов. Зарезервированные флаги позволяют обрабатывать особые случаи сочетаний шина-шина.
В интерфейсе bus_dma есть восемь функций для действий над отображениями прямого доступа. Их можно поделить на три подкатегории: для создания и уничтожения, для загрузки и выгрузки и для сихронизации отображений.
Первые две функции делятся на подкатегории создания и уничтожения. Функция bus_dmamap_create() создаёт отображение прямого доступа и инициализирует его в соответствии с переданными параметрами. Параметрами являются максимальный размер операции прямого доступа, который может уместиться в отображение, максимальное количество сегментов прямого доступа, максимальный размер любого из сегментов и любые ограничения пределов прямого доступа. Кроме стандартных флагов bus_dmamap_create() также принимает флаг BUS_DMA_ALLOCNOW, который предписывает выделить при создании отображения все ресурсы, необходимые для операции максимального размера. Этот флаг может пригодиться драйверам, которым необходимо загружать отображение прямого доступа, когда блокировка недопустима, например в контексте прерывания. Функция bus_dmamap_destroy() уничтожает отображение прямого доступа и освобождает все связанные с ним ресурсы.
Следующие пять функций делятся на подкатегории загрузки и выгрузки. Две основных функции - это bus_dmamap_load() и bus_dmamap_unload(). Первая отображает линейный буфер для исходящей или входящей операции прямого доступа. Этот линейный буфер может быть отображён в виртуальное адресное пространство ядра или процесса. Вторая выгружает ранее загруженное отображение. Если при создании отображения был указан флаг BUS_DMA_ALLOCNOW, bus_dmamap_load() не будет блокироваться или сообщать об ошибке выделения ресурса, а при выгрузке ресурсы отображения не будут освобождаться.
Кроме линейных буферов, управляемых основной функцией bus_dmamap_load(), интерфейс может управлять тремя другими структурами буферов данных. Функция bus_dmamap_load_mbuf() работает с цепочками mbuf. Предполагается, что отдельные буферы данных находятся в виртуальном адресном пространстве ядра. Функция bus_dmamap_load_uio() работает со структурами uio, из которых она извлекает информацию об адресном пространстве, в котором находятся данные. Наконец, функция bus_dmamap_load_raw() работает с памятью, которая не отображена в какое-либо виртуальное адресное пространство. Все отображения прямого доступа, загруженные этими функциями, выгружаются с помощью функции bus_dmamap_unload().
Наконец, подкатегория сихронизации отображений содержит одну функцию: bus_dmamap_sync(). Эта функция осуществляет четыре операции синхронизации прямого доступа, необходимые для управления кэшем и промежуточным буфером. Вот эти четыре операции:
Направление указывается с точки зрения памяти компьютера. Другими словами, передача устройство-память - это чтение, а память-устройство - это запись. Операции синхронизации указываются при помощи флагов, поэтому можно сочетать операции READ и WRITE в одном вызове. Это особенно полезно при синхронизации отображений дескрипторов управления устройством. Сочетание операций PRE и POST не допускается.
Кроме аргументов отображения и операции функция bus_dmamap_sync() также принимает смещение и длину. Они используются для частичной синхронизации. Если дескриптор управления устройством доступен через область прямого доступа, то может быть нежелательно синхронизировать всё отображение, т.к. это может быть неэффективно или даже разрушительно для других дескрипторов управления. Для синхронизации всего отображения нужно передать смещение 0 и длину, указанную в поле dm_mapsize отображения.
В интерфейсе bus_dma есть две подкатегории функций для управления памятью, безопасной для прямого доступа: выделения памяти и отображения памяти.
Первая функция в подкатегории выделения памяти - это bus_dmamem_alloc(), которая выделяет память, удовлетворяющую указанным требованиям. Можно указать следующие требования: размер выделяемой области, выравнивание каждого сегмента в выделении, ограничения пределов и максиальное количество сегментов прямого доступа, которые может создать выделение. Функция заполняет предоставленный массив bus_dma_segment_t и указывает количество действительных сегментов в массиве. Память, выделяемая этим интерфейсом, не отображена7 в какое-либо виртуальное адресное пространство. После окончания использования её можно освободить с помощью функции bus_dmamem_free().
Чтобы ядро или пользовательский процесс смогли обратиться к памяти, её нужно отобразить в адресное пространство ядра или процесса. Эти операции производятся функциями из подкатегории управления отображением памяти, безопасной для прямого доступа. Функция bus_dmamem_map() отображает указанную память, безопасную для прямого доступа, в адресное пространство ядра. Адрес отображения записывается в указатель по ссылке. Такое отображение можно отменить при помощи функции bus_dmamem_unmap().
Чтобы отобразить память, безопасную для прямого доступа, в адресное пространство процесса, можно воспользоваться входной точкой mmap() драйвера устройства. Для этого устройство подкачки подсистемы виртуальной памяти повторно обращается к драйверу для отображения каждой из страниц. Драйвер преобразует смещение mmap, указанное пользователем, в смещение памяти прямого доступа и вызывает функцию bus_dmamem_mmap() для преобразования смещения в непрозрачное значение, интерпретируемое модулем pmap8. Устройство подкачки заставляет модуль pmap преобразовать идентификатор отображения в адрес физической страницы, который затем отображается в адресное пространство процесса.
В настоящее время у подсистемы виртуальной памяти и у драйвера устройства (например, если устройство было удалено в процессе работы системы) нет возможности сообщить о необходимости отменить отображение области, созданное при помощи mmap. В широком смысле это ошибка, которая может быть устранена в будущей версии подсистемы виртуальной памяти NetBSD. При появлении такой возможности нужно соответствующим образом изменить интерфейс bus_dma.
В этом разделе описаны реализации bus_dma из двух переносов NetBSD: NetBSD/alpha и NetBSD/i386. Обе реализации представлены в сравнении друг с другом для лучшего понимания особенностей, скрытых за интерфейсом.
В настоящее время NetBSD/alpha поддерживает шесть реализаций шины PCI, каждая из которых реализует прямой доступ по-своему. Для понимания архитектурного подхода к довольно сложной реализации bus_dma в NetBSD/alpha необходимо понимать различия между адаптерами шины. Хотя некоторые из этих адаптеров имеют похожее описание и возможности, программный интерфейс каждого из них совершенно различается. (Кроме шины PCI в NetBSD/alpha также поддерживается две реализация прямого доступа для шины TurboChannel на компьютерах модели DEC 3000. Ради простоты мы ограничимся обсуждением шины PCI и связанных с ней шин.)
Первая реализация PCI, поддерживаемая NetBSD/alpha, - DECchip 21071/21072 (APECS) [1]. Она спроектирована для использования с процессорами DECchip 21064 (EV4) и 21064A (EV45). Этот адаптер шины PCI встречается в системах AlphaStation 200, AlphaStation 400 и AlphaPC 64, а также в некоторых системах AlphaVME. APECS поддерживает до двух окон прямого доступа, которые можно настроить на работу с непосредственными или векторными отображениями и которые хранят таблицу страниц векторов в оперативной памяти компьютера.
Вторая реализация PCI, поддерживаемая NetBSD/alpha, была найдена во встроенном контроллере ввода-вывода в семействе бюджетных процессоров Alpha (LCA - Low Cost Alpha) DECchip 21066 [2] и DECchip 21068. Это семейство процессоров использовалось в системах AXPpci33 и Multia AXP, а также в некоторых системах AlphaVME. Семейство процессоров поддерживает до двух окон прямого доступа, которые можно настроить на работу с непосредственными или векторными отображениями и которые хранят таблицу страниц векторов в оперативной памяти компьютера.
Третья реализация PCI, поддерживаемая NetBSD/alpha, - это DECchip 21171 (ALCOR) [3], 21172 (ALCOR2) и 21174 (Pyxis)9. Эти адаптеры шины PCI найдены в системах на основе процессоров DECchip 21164 (EVS), 21164A (EV56) и 21164PC (PCA56), включая AlphaStation 500, AlphaStation 600, AlphaPC 164 и персональные цифровые рабочие станции Digital Personal Workstation. ALCOR, ALCOR2 и Pyxis поддерживают до четырёх окон прямого доступа, которые можно настроить на работу с непосредственными или векторными отображениями и которые хранят таблицу страниц векторов в оперативной памяти компьютера.
Четвёртая реализация PCI, поддерживаемая NetBSD/alpha, - это Digital DWLPA/DWLPB [4]. Это мост между TurboLaser и PCI10, найденный в системах AlphaServer 8200 и 8400. Мост подключен к системной шине TurboLaser через адаптер ввода-вывода KFTIA (внутренний) или KFTHA (внешний). Первый поддерживает один встроенный и один внешний DWLPx. Второй поддерживает до четырёх внешних DWLPx. На системной шине TurboLaser может присутствовать несколько адаптеров ввода-вывода. Каждый DWLPx поддерживает до четырёх первичных шин PCI и имеет три окна прямого доступа, которые можно настроить на работу с непосредственными или векторыми отображениями. Эти три окна являются общими для всех шин PCI, присоединённых к DWLPx. DWLPx не использует оперативную память компьютера для хранения таблиц страниц векторов. Вместо этого DWLPx использует встроенную статическую оперативную память, которая делится между всеми шинами PCI, присоединёнными к DWLPx. Причина в том, что эти системы сохраняют данные перед дальнейшей передачей, что может вызвать слишком высокие задержки при доступе к таблице страниц прямого доступа. DWLPA снабжён статической памятью таблицы страниц объёмом 32K, а DWLPB - объёмом 128K. Т.к. DWLPx могут подслушивать обращения к таблице страниц в статической оперативной памяти, в этой реализации шины PCI не требуется явной инвалидации вектора TLB.
Пятая реализация PCI, поддерживаемая NetBSD/alpha, - это шина PCI A12C на масштабируемом параллельном процессоре Avalon A12 Scalable Parallel Processor [5]. Эта шина PCI - вторичная шина ввода-вывода11, на которой есть только один слот PCI форм-фактора мезонин и который используется исключительно для ввода-вывода Ethernet. Эта шина PCI не имеет прямого доступа к оперативной памяти компьютера. Вместо этого устройство использует для передачи и приёма буфер статической оперативной памяти объёмом 128K. По сути это аппаратная реализация промежуточного буфера прямого доступа. На неё не действует ограничения архитектуры, предусмотренные назначением системы A12 (она предназначена для параллельных вычислений, взаимодействующих через коммутатор по MPI12).
Шестая реализация PCI, поддерживаемая NetBSD/alpha, - MCPCIA. Это мост из MCBUS в PCI, найденный в системах AlphaServer 4100 (Rawhide). Архитектура Rawhide состоит из "лошади" (центральной панели) и двух "шорников" (главным образом адаптеров шины PCI на одной из сторон панели). Шорники также могут содержать адаптеры шины EISA. У каждого моста MCPCIA есть четыре окна прямого доступа, которые можно настроить на работу с непосредственными или векторными отображениями и которые хранят таблицу страниц векторов в оперативной памяти компьютера.
Платформа i386 резко контрастирует с Alpha и обладает очень простой реализацией шины PCI. Шина PCI способна адресовать всё 32-битное физическое адресное пространство архитектуры PC и в целом все адаптеры шины PCI программно совместимы. Также на платформе i386 используется прямое соответствие адресов прямого доступа, поэтому преобразование окон не требуется. Однако для прямого доступа к шине ISA на платформе i386 нужно использовать промежуточный буфер, т.к. шина ISA ограничена 24-битными адресами, а векторное отображение отсутствует.
Метки прямого доступа, используемые в NetBSD/alpha и NetBSD/i386, очень похожи. Обе содержат тринадцать указателей на функции интерфейса bus_dma. Однако метка прямого доступа NetBSD/alpha также содержит указатель на функцию, используемую для получения меток прямого доступа потомков первичной шины ввода-вывода и непрозрачные данные для методов её низкоуровневой реализации.
Непрозрачные данные, имеющиеся в метке прямого доступа в NetBSD/alpha, являются указателем на статически выделенную информацию о наборе микросхем. Эта информация содержит одну или более структур alpha_sgmap. alpha_sgmap содержит всю информацию о состоянии одного окна прямого доступа для управления векторым отображением, включая указатели на таблицу страниц вектора, карту диапазонов13 для управления таблицей страниц и базу окна прямого доступа.
Структура отображения прямого доступа содержит все параметры, использованные при создании отображения. (Так делают все текущие реализации интерфейса bus_dma.) Кроме параметров создания в двух реализациях есть дополнительные переменные состояния, относящиеся к отдельным особенностям прямого доступа. Например, отображение прямого доступа в NetBSD/alpha содержит несколько переменных состояния, относящихся к прямому доступу с векторным отображением. С другой стороны, в отображении прямого доступа из i386 содержится указатель на непрозрачные данные, относящиеся к отображению. Эти непрозрачные данные содержат информацию о состоянии промежуточного буфера прямого доступа шины ISA. Эти данные хранятся отдельно, потому что промежуточный буфер прямого доступа более характерен для i386, а не для Alpha, где при наличии в системе достаточно большого объёма физической памяти для прямого доступа на шине PCI можно воспользоваться векторным отображением.
В обеих реализациях bus_dma из NetBSD/alpha и NetBSD/i386 структура сегмента прямого доступа содержит только публичные поля, определённые интерфейсом.
В обеих реализациях bus_dma из NetBSD/alpha и NetBSD/i386 используется простая схема наследования для повторного использования кода. Для этого метка прямого доступа собирается из слоёв кода, специфичных для набора микросхем или шины (то есть мастер-слоёв). При сборке метки мастер-слой вставляет свои собственные методы в слот указателей на функции, предназначеные для работы с этим слоем. Методы, не требующие специальной обработки, инициализируются указателями на общий код.
Код bus_dma из Alpha разбит на четыре основные категории: код набора микросхем, код реализации общих операций непосредственного отображения, код реализации общих операций векторного отображения и код реализации операции и непосредственного и векторного отображения. Некоторые из общих функций вызываются не по указателям в метке, а напрямую. Эти функции являются вспомогательными и используются только из функций, относящихся к наборам микросхем. Такими функциями являются, например, общие функции загрузки непосредственного отображения. Кроме аргументов, принимаемых интерфейсными функциями, эти функции также принимают один дополнительный аргумент - базовый адрес окна прямого доступа.
В свою очередь, реализация bus_dma в i386 разбита на три основные категории: общая реализация методов bus_dma, общие вспомогательные функции и интерфейсные функции шины ISA14. Функции с общей реализацией методов интерфейса можно вызывать напрямую из переключателя функций в метке. Этим пользуются метки PCI и EISA, так как они не реализуют методов, специфичных для шины. Если в системе больше 16 мегабайт физической памяти, то для шины ISA используются интерфейсные функции с поддержкой промежуточного буфера прямого доступа. Если в системе 16 мегабайт физической памяти или меньше, то промежуточный буфер прямого доступа не требуется и в качестве интерфейсных функции шины ISA используется общая реализация методов bus_dma.
Система автоконфигурирования ядра NetBSD использует обход узлов (устройств) первого уровня в дереве устройств. Этот процесс запускается машинно-зависимым кодом, сообщающим машинно-независимому фреймворку автоконфигурирования, что он "нашёл" корневую "шину". На двух описанных здесь платформах эта корневая шина называется mainbus. mainbus является виртуальным устройством и не соответствует какой-либо физической шине в системе. Драйвер устройства mainbus реализован в машинно-зависимом коде. Этот драйвер отвечает за первичное конфигурирование шины или шин ввода-вывода.
В NetBSD/alpha слой mainbus считает первичной шиной ввода-вывода ту, которая реализуется набором микросхем. Машинно-зависимый код платформы указывает имя набора микросхем, а драйвер mainbus "находит" и конфигурирует его. Когда драйвер набора микросхем присоединён, он настраивает свои окна прямого доступа и структуры данных. Как только всё готово, он "находит" первичную шину или шины PCI, логически подсоединённые к набору микросхем, и передаёт метку этих шин драйверу шины PCI. В свою очередь этот драйвер находит и конфигурирует каждое устройство на шине PCI и так далее.
Если драйвер шины PCI обнаруживает мост PCI-в-PCI (PPB), метка прямого доступа передаётся драйверу устройства PPB неизменной. Он в свою очередь передаёт её вторичному экземпляру шины PCI, подключенному с другой стороны моста. Однако, если драйвер шины PCI обнаруживает мост другого типа, такой как EISA или ISA, то нужно задействовать машинно-зависимый код. Этим шинам может потребоваться другая метка. По этой причине все драйверы мостов типа PCI-в-<другая шина> (PCxB) реализованы в машинно-зависимом коде. Хотя драйверы PCxB можно реализовать в машинно-независимом коде и воспользоваться машинно-зависимыми функциями для получения меток, такая реализация не используется, т.к. вторичная шина может потребовать особой машинно-зависимой настройки прерываний и маршрутизации. Раз для учёта всех особенностей настройки шины всё равно потребуется прибегать к помощи машинно-зависимых функций, вряд ли стоит прилагать усилия для обобщения итогового кода.
Когда драйвер шины находит устройство и сопоставляет его драйверу устройства, то драйвер устройства получает несколько кусочков информации, требуемой для инициализации и связи с устройством. Один из кусочков этой информации - это метка прямого доступа. Если драйвер собирается выполнять операции прямого доступа в память, он должен запомнить эту метку, которая, как ранее отмечено, используется при каждом обращении к интерфейсу bus_dma.
Хотя процедура конфигурирования шин и устройств в NetBSD/i386 по большому счёту идентична случаю NetBSD/alpha, конфигурирование первичных шин ввода-вывода в корне отличается. Платформа PC спроектирована на базе шины ISA. EISA и PCI с точки зрения драйверов устройств очень похожи на ISA. Все три поддерживают концепцию отображения пространств ввода-вывода15 и памяти. Аппаратное обеспечение и встроенное программное обеспечение PC обычно инициализируют эти шины таким образом, чтобы инициализация адаптеров шины со стороны операционной системы не требовалась. Поэтому с точки зрения автоконфигурирования можно рассматривать PCI, EISA и ISA как первичные шины ввода-вывода.
Драйвер mainbus из NetBSD/i386 конфигурирует первичные шины ввода-вывода в порядке снижения приоритета: сначала PCI, потом EISA и наконец ISA. Драйвер mainbus имеет прямой доступ к меткам каждой из шин. В случае EISA и ISA слой mainbus пытается сконфигурировать эти шины лишь в том случае, если они не были найдены в процессе конфигурирования шины PCI. Из соображений педантичности NetBSD/i386 идентифицирует мосты PCI-в-EISA (PCEB) и PCI-в-ISA (PCIB) и назначает им узлы автоконфигурирования в дереве устройств. Шины EISA и ISA логически подсоединены к этим узлам примерно так же, как в NetBSD/alpha. Драйверы мостов также имеют прямой доступ к меткам шин и передают их вниз соответствующим шинам ввода-вывода.
В этом подразделе описано, что делает машинно-зависимый код, когда интерфейс bus_dma используется в драйвере гипотетического устройства шифрования DES. Хотя это не настоящее применение bus_dma, этот пример гораздо проще понять. Предположим, что это устройство применяется в некоторой высокопроизводительной иерархической системе хранения.
Здесь описаны не все особенности драйвера устройства NetBSD, а лишь те, которые имеют отношение к теме прямого доступа в память.
Представим, что наше устройство поставляется в вариантах для PCI и ISA. А поскольку мы описываем две платформы, то получится четыре сочетания платформ и шин. Обозначим их следующим образом:
Предположим, что платформа [i386/ISA] снабжена более чем 16 мегабайтами оперативной памяти. Если память, безопасная для прямого доступа, не используется явно, то для для операций прямого доступа может потребоваться промежуточный буфер. Предположим также, что окно непосредственного отображения на платформе [Alpha/PCI] способно обращаться ко всей системной оперативной памяти.
Отметим также, что мы будем описывать синхронизацию отображений только в том случае, если для неё требуется какая-то особая обработка. И в случае [Alpha/ISA] и в случае [Alpha/PCI] операции синхронизации заставляют процессор Alpha осушить буфер записи при помощи инструкции mb [6]. В случае [i386/PCI] и в случае [i386/ISA] с памятью, безопасной для прямого доступа, операций синхронизации не требуется.
Устройство является хозяином шины и управляется через командный блок фиксированной длины, который читается из памяти с использованием прямого доступа. Поддерживаются три команды: SET KEY, ENCRYPT и DECRYPT. Команда записывается в командный блок и запускается при записи адреса области прямого доступа командного блока в регистр dmaAddr. Командный блок создержит 6 32-битных слов: cbCommand, cbStatus, cbInAddr, cbInCount, cbOutAddr и cbOutCount. Поля cbInAddr и cbOutAddr являются адресами прямого доступа программных списков векторов, используемых средствами прямого доступа устройства. Поля cbInCount и cbOutCount - это количество записей в списке соответствующего вектора. Каждая запись в векторе содержит 32-битные слова: адрес и длину области прямого доступа.
Для обработки запроса устройство читает командный блок через прямой доступ в память. Затем оно анализирует командый блок, чтобы определить, какое действие нужно выполнить. В случае всех трёх поддерживаемых команд устройство читает по адресу прямого доступа cbInAddr список вектора длиной cbInCount * 8 байт. Затем устройство подаёт входные данные на обработку соответствующим методом. В случае команды SET KEY список вектора используется для прямого доступа к ключу DES, который копируется в оперативную память устройства. В случае остальных команд входные данные подаются на обработчик DES, который переключается в режим шифрования или в режим дешифрования. Обработчик DES читает список вектора для выходных данных, указанный в области по адресу cbOutAddr длиной cbOutCount * 8 байт. Как только обработчик DES получит все адреса прямого доступа, он приступает к циклу ввод-обработка-вывод до тех пор, пока не будут обработаны все данные. По завершении команды в cbStatus записывается слово состояния и инициируется прерывание. Программное обеспечение драйвера должно прочитать это слово, чтобы определить, завершилась ли команда успешно.
Драйвер этого устройства DES предоставляет входные точки open(), close() и ioctl(). Для максимальной производительности драйвер использует прямой доступ в пользовательское адресное пространство. Когда пользователь инициирует запрос через ioctl, соответствующий запрошенной операции, драйвер помещает его в очередь обработки. Системный вызов ioctl() немедленно завершается, позволяя приложению продолжить работу или ожидать сигнала при помощи sigsuspend(). Если устройство сейчас простаивает, драйвер немедленно передаёт команду устройству. После окончания работы устройство инициирует прерывание и драйвер уведомляет пользователя при помощи сигнала SIGIO, что запрос был выполнен. Если в очереди есть другие запросы, то очередной запрос удаляется из очереди и помещается на обработку и так до тех пор, пока в очереди не останется запросов.
При создании (присоединении) экземпляра драйвера, драйвер должен создать и инициализировать необходимые для работы структуры данных. Этот драйвер использует несколько отображений прямого доступа: одно для управляющих структур (блока управления и списков векторов) и множество для данных из запросов пользователей. Отображения для данных из запросов пользователей хранятся в записях очереди задач драйвера.
Далее драйвер должен выделить для управляющих структур память, безопасную для прямого доступа. Драйвер выделяет три страницы памяти при помощи bus_dmamem_alloc(). Для простоты драйвер запрашивает один сегмент памяти. Для всех платформ и шин в этом примере эта операция просто вызывает функцию подсистемы виртуальной памяти, которая выделят память, соответствующую заданным ограничениям. В случае [i386/ISA] слой ISA вставляет себя в граф вызовов, чтобы указать диапазон 0-16 мегабайт. Во всех других случаях просто указывается, что запись должна присутствовать в диапазоне физической памяти.
Маленький кусочек этой памяти будет использоваться для командного блока. Остальная память будет поделена между двумя списками векторов. Эта память затем отображается в виртуальное адресное пространство ядра при помощи bus_dmamem_map() с флагом BUS_DMA_COHERENT, а ядро заполняет указатели на три структуры. Когда память отображается в i386, флаг BUS_DMA_COHERENT вызывает установку флагов подавления кэша в PTE. На Alpha не этот флаг не требует специальной обработки. Однако т.к. в случае с Alpha есть только один сегмент, память отображается через непосредственно отображаемый Alpha сегмент ядра и поэтому использование виртуального адресного пространства ядра не требуется.
Наконец, драйвер загружает отображение структуры управления, передавая виртуальный адрес в памяти ядра в bus_dmamap_load(). Чтобы упростить начало операции, драйвер кэширует адреса прямого доступа различных управляющих структур (добавляя их смещения к адресам памяти прямого доступа). Во всех случаях нижележащая функция загрузки проходится по каждой странице в диапазоне виртуальных адресов, извлекая физический адрес из модуля pmap и по возможности уплотняя сегмены. Как только память выделилась как один сегмент, она отображается на один сегмент памяти прямого доступа.
Давайте представим, что пользователь уже установил ключ и теперь желает использовать его для шифрования буфера данных. Вызывающая программа берёт указатели на входной и выходной буферы, на слово статуса, упаковывает их в запрос и вызывает ioctl с запросом "зашифровать буфер".
После входа в ядро драйвер блокирует пользовательский буфер для предотвращения выгрузки данных в процессе выполнения операции. Выделяется элемент очереди задач, для задачи создаются два отображения прямого доступа: первое для входного буфера и второе для выходного буфера. Во всех случаях выделяется стандартная структура отображения. В случае [i386/ISA] для каждого отображения также выделяется идентификатор ISA.
Как только выделен элемент очереди задач, его нужно инициализировать. Для этого сначала загружаются отображения прямого доступа для входного и выходного буферов. Поскольку этот процесс по сути одинаков и для входного и для выходного буферов, здесь будут описаны только действия для входного буфера.
На [Alpha/PCI] и [i386/PCI] нижележащий код обходит пользовательский буфер, извлекая физический адрес каждой страницы. На [Alpha/PCI] к этому адресу также добавляется базовый адрес окна прямого доступа. Адрес и длина сегмента помещаются в список сегментов отображения. По возможности сегменты соединяются.
На [Alpha/ISA] происходит почти то же самое. Но в этом случае выделяется новое векторно-отображаемое адресное пространство, а физические адреса помещаются не в список сегментов отображения, а в соответствующие записи таблицы страниц. После этого в список сегментов отображения помещается один сегмент прямого доступа, указывающий на начало области векторного отображения.
В случае [i386/ISA] буфер обходится дважды. При первом обходе буфер проверяется на отсутствие страниц выше 16-мегабайтного предела. Если их нет, то процедура идентична случаю [i386/PCI]. Однако представим, что в буфере есть страницы за пределами границы, так что для операции передачи данных нужно использовать промежуточный буфер. Тогда выделяется промежуточный буфер. Поскольку это происходит в контексте процесса, выделение буфера может привести к блокировке. Указатель на промежуточный буфер сохраняется в идентификаторе прямого доступа шины ISA, а физический адрес промежуточного буфера помещается в список сегментов отображения.
Далее либо начинается обработка запроса, либо он помещается в очередь. Для простоты представим, что других задач нет. Сначала нужно проинициализировать блок управления кэшированными адресами списков векторов устройства. В эти списки также помещаются списки сегментов отображения прямого доступа. Перед тем как мы сообщим устройству о необходимости начать передачу данных, нужно синхронизировать отображения прямого доступа.
Сначала нужно синхронизировать отображение входного буфера. Для этого используется операция PREWRITE. В случае [i386/ISA] содержимое пользовательского буфера копируется из пользовательского адресного пространства в промежуточный буфер16. Далее нужно синхронизировать отображение выходного буфера. Для этого используется операция PREREAD. Наконец, нужно синхронизировать отображение блока управления. Поскольку после завершения задачи из блока управления нужно будет прочитать поле состояния, для этого используется операция PREREAD|PREWRITE.
Теперь можно приступать к операции прямого доступа. Устройство начинает запись по кэшированному адресу блока управления в регистре dmaAddr. Драйвер возвращается в пространство пользователя и процесс ожидает сигнал, указывающий на завершение задачи.
По завершении задачи устройство инициирует прерывание. Обработчик прерывания должен завершить операции прямого доступа и уведомить об окончании задачи процесс, который сделал запрос.
Первым делом нужно синхронизировать отображение входного буфера. Для этого используется операция POSTWRITE. Далее нужно синхронизировать отображение выходного буфера. Для этого используется операция POSTREAD. В случае с [i386/ISA] содержимое выходного промежуточного буфера копируется в пользовательский буфер17. Наконец, нужно синхронизировать отображение блока управления. Для этого используется операция POSTREAD|POSTWRITE.
Теперь, когда все отображения синхронизированы, их нужно выгрузить. В случаях [Alpha/PCI] и [i386/PCI] освобождать нечего и отображение просто помечается как недействительное. В случае [Alpha/ISA] освобождаются ресурсы, использовавшиеся векторным отображением. В случае [i386/ISA] освобождается промежуточный буфер.
Поскольку пользовательский буфер больше не используется, драйвер устройства снимает с него блокировку. Теперь процессу можно отправить сигнал о завершении ввода-вывода. Последнее, что нужно сделать - это уничтожить отображения входного и выходного буферов и запись в очереди задач.
Интерфейс bus_dma был введён в ядро NetBSD в версии 1.2G, прямо перед началом цикла выпуска NetBSD 1.3. Когда код был внесён в основной репозиторий NetBSD, вместе с ним на новый интерфейс были переведены некоторые драйверы, в основном драйверы контроллеров SCSI. (Все эти драйверы ранее были переведены на интерфейс bus_space.) Эти драйверы не только являются примерами правильного использования bus_dma, но и предоставляют NetBSD ранее недоступную функциональность: поддержку захвата шины ISA устройствами на PC с оперативной памятью объёмом более 16 мегабайт.
Первой проверкой интерфейса в действии на платформе Alpha стала установка устройства с поддержкой захвата шины ISA (контроллера SCSI Adaptec 1542 SCSI) в компьютер AXPpci33. После исправления небольшой ошибки в реализации bus_dmamap_load() для Alpha устройство заработало безупречно.
При переводе драйверов устройств на использование нового интерфейса разработчики обнаружат, что из каждого переделанного дравера можно удалить значительную часть однотипного кода. Этот был код цикла, строившего программный список вектора. В некоторых случаях драйверы стали заметно лучше, т.к. реализация цикла внутри bus_dmamap_load() более эффективна и позволяет соединять сегменты.
На новый интерфейс было переведено большинство машинно-независимых драйверов, использовавших прямой доступ в память, а также необходимые внутренние интерфейсы были реализованы на большем количестве платформ. Результаты были весьма ободряющими. Едва ли не каждое сочетание устройство/платформа, которое было опробовано, заработало без дополнительных изменений дайвера устройства. Несколько исключений из этого главным образом были связаны с отличиями в обработке порядка байт на компьютере и устройстве и не имеют прямого отношения к прямому доступу в память.
Интерфейс bus_dma также открыл дорогу для машинно-независимых фреймворков автоконфигурации шин, таких как VME. В конечном счёте это позволит поддерживать мосты PCI-в-VME и позволит системам Sun, Motorola и Intel использовать общие драйверы устройств для шины VME.
Мы считаем, что интерфейс bus_dma станет значительным архитектурным достоинством ядра NetBSD, значительно упрощающим процесс переноса ядра на новые платформы, а также значительно упростит разработку переносимых драйверов устройств. Проще говоря, созданная абстракция соответствует своему назначению: является средством поддержки широкого круга платформ при максимальном повторном использовании кода.
По ссылке http://www.netbsd.org/ можно найти дополнительную информацию о NetBSD, включая информацию о том, где можно взять исходные тексты и двоичные файлы самой операционной системы NetBSD.
Периодически обновляемую версию этого документа можно найти по ссылке http://www.netbsd.org/Documentation/research/.
Хотел бы поблагодарить следующих людей за их весьма конструктивный вклад и ценные идеи во время проектирования bus_dma: Криса Деметриу (Chris Demetriou), Чарльза Ханнума (Charles Hannum), Росса Харви (Ross Harvey), Мэттью Джейкоба (Matthew Jacob), Джонатана Стоуна (Jonathan Stone) и Мэтта Томаса (Matt Thomas).
Дополнительно я хотел бы особо поблагодарить Криса Деметриу (Chris Demetriou), Лонхина Ясинского (Lonhyn Jasinskyj), Кевина Лэхи (Kevin Lahey), Ивонну Мэллой (Yvonne Malloy), Дэвида Макнаба (David McNab) и Гарри Уодделла (Harry Waddell) за время, которое они потратили на критику и помощь в полировке этого документа.
Джейсон Р. Торп - инженер сетевых систем в Исследовательском центре средств цифровой аэрокосмической симуляции NASA имени Эймса. Его профессиональные интересы касаются проектирования и реализации переносимых операционных систем, высокоскоростных компьютерных сетей и сетевых протоколов. Кроме поддержки сетевых средств и разработки систем массового хранения NAS в операционной системе NetBSD, он также является активным участником организации Internet Engineering Task Force (IETF). Был вкладчиком в проект NetBSD с середины 1993 года и в то или иное время запускал почти каждый перенос. В настоящее время поддерживает перенос NetBSD hp300 и является членом ключевой группы NetBSD (NetBSD Core Group).
С автором можно связаться по адресу Numerical Aerospace Simulation Facility, Mail Stop 258-5, NASA Ames Research Center, Moffett field, CA 94035 или по электронной почте thorpej@nas.nasa.gov.
Автор перевода на русский язык: Владимир Ступин