Прямой доступ к памяти

Предыдущая  Содержание  Следующая V*D*V

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

Обзор передачи данных с прямым доступом к памяти

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

 

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

 

1.Когда процесс вызывает read, метод драйвера выделяет буфер DMA и поручает оборудованию передавать свои данные в этот буфер. Процесс помещается в сон.

2.Оборудование записывает данные в буфер DMA и вызывает прерывание, когда это выполнено.

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

 

Второй случай наступает, когда DMA используется асинхронно. Так происходит, например, с устройством сбора данных, которое выполняет выдаёт данные, даже если никто их не читает. В этом случае драйвер должен содержать буфер, чтобы последующий вызов read вернул все накопленные данные в пространство пользователя. Этапы, связанные с такого рода передачей, немного отличаются:

 

1.Оборудование вызывает прерывание, чтобы объявить, что поступили новые данные.

2.Обработчик прерывания выделяет буфер и сообщает оборудованию, куда передать данные.

3.Периферийное устройство записывает данные в буфер и по завершении вызывает другое прерывание.

4.Обработчик отправляет новые данные, будит любой соответствующий процесс, и заботится о ведении хозяйства.

 

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

 

Обработка шаги во всех этих случаях подчеркнуть, что эффективная обработка DMA опирается на прерывание отчетности. Хотя можно реализовать DMA с опрашивающим драйвером, это не будет иметь смысла, потому что опрашивающий драйвер сведёт на нет преимущества производительности, которые предлагает DMA вместо более простого ввода/вывода, управляемого процессором. (* Конечно, из всего есть исключения; смотрите раздел "Уменьшение числа прерываний" в Главе 17 для демонстрации того, как высокопроизводительные сетевые драйверы лучше реализуются с использованием опроса.)

 

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

Выделение DMA буфера

Этот раздел описывает выделение DMA буферов на низком уровне; в ближайшее время мы введём высокоуровневый интерфейс, но ещё неплохо понимать материал, представленный здесь.

 

Основным вопросом, который остро стоит с DMA буферами является тот, что когда они больше, чем одна страница, они должны занимать смежные страницы в физической памяти, так как устройство передаёт данные с помощью системной шины ISA или PCI, обе из которых имеют физические адреса. Интересно отметить, что это ограничение не распространяется на SBus (смотрите раздел "SBus" в Главе 12), которая использует на периферийной шине виртуальные адреса. Некоторые архитектуры могут также использовать виртуальные адреса на шине PCI, но переносимый драйвер не может рассчитывать на такую возможность.

 

Хотя DMA буферы могут быть выделены либо при загрузке системы или во время выполнения программы, модули могут выделить их буферы только во время выполнения. (эти методы представила Глава 8; раздел "Получение больших буферов" описывает выделение при загрузке системы, а "Как работает kmalloc" и "get_free_page и друзья" описали выделение во время выполнения.) Авторы драйверов должны позаботиться, чтобы выделить верный тип памяти, когда она используется для операций DM; не все зоны памяти подходят для этого. В частности, с DMA на некоторых системах и с некоторыми устройствами может не работать верхняя память - периферийные устройства просто не могут работать с такими большими адресами.

 

Большинство устройств на современных шинах могут обрабатывать 32-х разрядные адреса, это означает, что для них прекрасно работает обычное выделение памяти. Однако, некоторые PCI устройства, не следуют полному стандарту PCI и не могут работать с 32-х разрядными адресами. И конечно, устройства ISA ограничены только 24-х разрядными адресами.

 

Для устройств с такого рода ограничением память должна быть выделена из зоны DMA, добавлением флага GFP_DMA при вызове kmalloc или get_free_pages. Если этот флаг присутствует, выделяется только та память, которая может быть адресована 24-мя разрядами. В качестве альтернативы, вы можете использовать общий слой DMA (который мы обсудим в ближайшее время), чтобы выделить буферы для обхода ограничений вашего устройства.

Самостоятельное выделение

Мы уже видели, как get_free_pages может выделить до нескольких мегабайт (так как порядок может иметь диапазон до MAX_ORDER, в настоящее время 11), но запросы высокого порядка подвержены неудаче даже когда запрошенный буфер гораздо меньше, чем 128 Кб, потому что системная память становится с течением времени фрагментированной. (* Слово фрагментация обычно применяется к дискам, чтобы выразить идею о том, что файлы не хранятся последовательно на магнитном носителе. То же самое относится и к памяти, где каждое виртуальное адресное пространства становится разбросанным по физической памяти и становится трудно получить последовательные свободные страницы при запросе буфера DMA.)

 

Когда ядро не может вернуть запрашиваемый объём памяти или когда необходимо более чем 128 Kб (общее требование, например, для устройств видеозахвата PCI), альтернативой возвращению -ENOMEM является выделение памяти во время загрузки или резервирование  для буфера вершины физической памяти. Мы описали выделение во время загрузки в разделе "Получение больших буферов" в Главе 8, но для модулей это недоступно. Резервирование верхней части ОЗУ осуществляется передачей для ядра во время загрузки аргумента mem=. Например, если у вас есть 256 Мб, аргумент mem=255M предохраняет ядро от использования верхнего мегабайта. Чтобы получить доступ к такой памяти, ваш модуль впоследствии может использовать следующий код:

 

dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);

 

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

 

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

Шинные адреса

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

 

На самом деле, ситуация несколько более сложная, чем эта. Оборудование на основе DMA использует шинные, а не физические адреса. Хотя шинные адреса ISA и PCI на ПК являются просто физическими адресами, это не верно для каждой платформы. Иногда интерфейсная шина подключена через схему моста, который отображает адреса ввода/вывода на другие физические адреса. Некоторые системы даже имеют странично-отображающую схему, которая может сделать так, чтобы произвольные страницы выглядели для периферийной шины непрерывными.

 

На самом низком уровне (опять же, в ближайшее время мы будем рассматривать высокоуровневое решение), ядро Linux обеспечивает переносимое решение экспортируя следующие функции, определённые в <asm/io.h>. Использование этих функций настоятельно не рекомендуется, потому что они работают эффективно только на системах с очень простой архитектурой ввода/вывода; тем не менее, вы можете столкнутся с ними при работе с кодом ядра.

 

unsigned long virt_to_bus(volatile void *address);

void *bus_to_virt(unsigned long address);

 

Эти функции выполняют простые преобразования между логическими адресами ядра и шинными адресами. Они не будут работать в любой ситуации, где устройство управления памятью ввода/вывода должно быть запрограммировано или где должны быть использованы возвратные буферы. Правильным способом выполнения этого преобразования является использование универсального уровня DMA, так что мы теперь перейдём к этой теме.

Универсальный уровень DMA

DMA операции, в конечном счёте, сводятся к выделению буфера и передачи вашему устройству шинных адресов. Тем не менее, задача написания переносимых драйверов, которые выполняют DMA безопасно и корректно на всех архитектурах значительно труднее, чем можно подумать. Разные системы имеют разные представления о том, как должно работать согласование кэша; если вы не решите этот вопрос правильно, ваш драйвер может повредить память. Некоторые системы имеют сложное шинное оборудование, которое может сделать задачу DMA легче или труднее. И не все системы могут выполнять DMA из всех частей памяти. К счастью, ядро предоставляет шинно- и архитектурно-независимый уровень DMA, который скрывает большинство из этих проблем от автора драйвера. Мы настоятельно рекомендуем вам использовать этот уровень для DMA операций в любой драйвере, который вы пишете.

 

Многие из приведённых ниже функций требуют указатель на struct device. Эта структура является низкоуровневым представлением устройства внутри модели устройства Linux. Это не то, с чем драйверам часто приходится работать напрямую, но вам это необходимо, когда используется универсальный уровень DMA. Обычно вы можете найти эту структуру похороненной внутри шинной специфики, описывающей ваше устройство. Например, он может быть найден как поле dev в struct pci_device или struct usb_device. Структура device подробно описана в Главе 14.

 

Драйверы, которые используют следующие функции, должны подключать <linux/dma-mapping.h>.

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

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

 

int dma_set_mask(struct device *dev, u64 mask);

 

mask должна указать биты, которые ваше устройство может адресовать; например, если оно ограничено 24-мя битами, вам бы передали mask как 0x0FFFFFF. Возвращаемое значение отлично от нуля, если с заданной маской DMA возможен; если dma_set_mask возвращает 0, вы не можете использовать DMA операции с этим устройством. Таким образом, код инициализации драйвера для устройства, ограниченного 24-х разрядными DMA операциями, может выглядеть так:

 

if (dma_set_mask (dev, 0xffffff))

    card->use_dma = 1;

else {

    card->use_dma = 0; /* Нам придётся жить без DMA */

    printk (KERN_WARN, "mydev: DMA not supported\n");

}

 

Опять же, если устройство поддерживает нормальные, 32-х разрядные операции DMA, вызывать dma_set_mask необходимости нет.

Отображения DMA

Отображения DMA являются комбинацией выделения буфера DMA и генерации адреса для этого буфера, который доступен устройству. Заманчиво получить такой адрес просто вызвав virt_to_bus, но есть веские основания для избегания такого подхода. Первым из них является то, что соответствующее оборудование поставляется с IOMMU, что обеспечивает для шины набор регистров отображения. IOMMU может организовать появление любой физической памяти в диапазоне адресов, доступных устройству, и это может сделать физически разделённые буферы выглядящими последовательно для устройства. Работа с IOMMU требует использования универсального  уровня DMA; virt_to_bus не подходит для этой задачи.

 

Заметим, что не все архитектуры имеют IOMMU; в частности, популярная платформа x86 не имеет поддержки IOMMU. Тем не менее, должным образом написанному драйверу нет необходимости знать о поддержке ввода/вывода оборудованием, на котором он выполняется.

 

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

 

Отображения DMA также должны обратиться к вопросу о согласовании кэша. Помните, что современные процессоры хранят копии наиболее часто используемых областей памяти в быстром, локальном кэше; без этого кэша приемлемая производительность невозможна. Если ваше устройство изменяет область основной памяти, крайне важно, чтобы любые процессорные кэши, охватывающие эту область, стали недействительными; в противном случае, процессор может работать с неправильным образом основной памяти и в результатом будет повреждение данных. Аналогично, когда ваше устройство использует DMA для чтения данных из основной памяти, любые изменения в этой памяти, находящиеся в кэшах процессора, должны быть прочитаны в первую очередь. Эти вопросы согласованности кэша могут создать  бесконечные непонятные и труднообнаруживаемые ошибки, если программист не осторожен. Некоторые архитектуры управляют согласованием кэша аппаратно, но другие требуют поддержки от программного обеспечения. Универсальный уровень DMA делает многое, чтобы обеспечить, чтобы всё работало корректно на всех архитектурах, но, как мы увидим, надлежащее поведение требует соблюдения небольшого набора правил.

 

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

 

Код PCI различает два типа отображений DMA, в зависимости от ожидания того, как долго будет существовать буфер DMA:

 

Согласованные отображения DMA

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

 

Потоковые отображения DMA

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

 

Эти два типа отображения должны управляться разными способами; настало время взглянуть на подробности.

Создание согласованных отображений DMA

Драйвер может создать согласованное отображение вызовом dma_alloc_coherent:

 

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);

 

Эта функция обрабатывает как выделение, так и отображение буфера. Первыми двумя аргументами являются структура устройства и размер необходимого буфера. Функция возвращает результат отображения DMA в двух местах. Возвращаемым значением функции является виртуальный адрес ядра для буфера, который может быть использован драйвером; между тем, адрес соответствующей шины возвращается в dma_handle. Выделение выполняется в этой функции, так что буфер находится в месте, которое работает с DMA; обычно память выделяется только с помощью get_free_pages (но заметьте, что размер в байтах, а не в значении порядка). Аргумент flag является обычным значением GFP_, описывающим как будет выделена память; как правило, он должен быть GFP_KERNEL (обычно) или GFP_ATOMIC (при работе в атомарном контексте).

 

Когда буфер больше не нужен (обычно во время выгрузки модуля), он должен быть возвращён системе с помощью dma_free_coherent:

 

void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);

 

Обратите внимание, что эта функция, как и многие универсальные функции DMA, требует, чтобы были указаны все аргументы: размер, процессорный адрес и шинный адрес.

Пулы DMA

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

 

Функции пула DMA определены в <linux/dmapool.h>.

 

Пул DMA должен быть создан до использования вызовом:

 

struct dma_pool *dma_pool_create(const char *name, struct device *dev,

                                 size_t size, size_t align,

                                 size_t allocation);

 

Тут, name - это имя для пула, dev - ваше структура устройства, size - размер буферов, которые будут выделены из этого пула, align - необходимое аппаратное выравнивание для выделений из пула (выраженное в байтах) и allocation, если не равно нулю, предел памяти, которую выделения не должны превышать. Если, например, allocation передана как 4096, буферы, выделенные из этого пула не пересекают границы в 4 Кб.

 

Когда вы завершили работу с пулом, он может быть освобождён с помощью:

 

void dma_pool_destroy(struct dma_pool *pool);

 

Вы должны вернуть все выделения в пул до его уничтожения.

 

Выделения обрабатываются с помощью dma_pool_alloc:

 

void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);

 

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

 

void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);

Создание потоковых отображений DMA

Потоковые отображения по ряду причин имеют более сложный интерфейс, чем согласованный вариант. Эти отображения рассчитывают на работу с буфером, который уже был выделен драйвером, и, следовательно, иметь дело с адресами, которые они не выбирают. На некоторых архитектурах потоковые отображения могут также иметь несколько несоприкасающихся страниц и составные буферы "разборки/сборки". По всем этим причинам потоковые отображения имеют свой собственный набор функций отображения.

 

При создании потокового отображения вы должны сообщить ядру, в каком направлении движутся данные. Для этой цели были определены некоторые символы (с типом enum dma_data_direction):

 

DMA_TO_DEVICE

DMA_FROM_DEVICE

Эти два символа должна быть достаточно очевидны. Если данные отправляются в устройство (возможно, в ответ на системный вызов write), следует использовать DMA_TO_DEVICE; данные, идущие в процессор, наоборот, обозначаются DMA_FROM_DEVICE.

 

DMA_BIDIRECTIONAL

Если данные могут двигаться в любом направлении, используйте DMA_BIDIRECTIONAL.

 

DMA_NONE

Этот символ предоставляется только как помощь при отладке. Попытки использовать буферы с этим "направлением" вызовут панику ядра.

 

Может возникнуть желание всегда просто указывать DMA_BIDIRECTIONAL, но авторам драйверов не следует поддаваться этому соблазну. На некоторых архитектурах за такой выбор придётся заплатить падением производительности.

 

Если у вас для передачи есть единственный буфер, отображайте его с помощью dma_map_single:

 

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size,

                          enum dma_data_direction direction);

 

Возвращаемым значением является шинный адрес, который вы можете передать в устройство или NULL, если что-то пойдёт не так.

 

После завершения передачи отображение должно быть удалено с помощью dma_unmap_single:

 

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,

                      enum dma_data_direction direction);

 

Здесь, аргументы size и direction должны соответствовать тем, которые использовались для отображения буфера.

 

Для потокового отображения DMA применяются некоторые важные правила:

 

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

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

Буфер не должен быть отключён во время активности DMA, либо гарантирована серьёзная нестабильность системы.

 

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

 

Во-вторых, рассмотрим, что произойдёт, если буфер отображён в область памяти, которая не доступна для устройства. Некоторые архитектуры в этом случае просто дают ошибку, но другие создают возвратный буфер. Возвратный буфер - это просто отдельная область памяти, доступная для устройства. Если буфер отображается с направлением DMA_TO_DEVICE и требуется возвратный буфер, содержимое оригинального буфера копируется как часть операции отображения. Ясно, что изменения в оригинальном буфере после копирования устройству не видны. Аналогичным образом при DMA_FROM_DEVICE возвратные буферы копируются обратно в исходный буфер функцией dma_unmap_single; данные из устройства не имеют смысла, пока не выполнено это копирование.

 

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

 

Иногда драйверу необходимо получить доступ к содержимому потокового буфера DMA без его отключения. Это делает возможным следующий вызов:

 

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,

                             size_t size, enum dma_data_direction direction);

 

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

 

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,

                                size_t size, enum dma_data_direction direction);

 

Процессор, опять же, не должен обращаться к буферу DMA после того, как был сделан этот вызов.

Одностраничные потоковые отображения

Иногда вы можете захотеть настроить отображение на буфер для которого у вас есть указатель struct page; например, это может произойти с буферами пространства пользователя, отображаемыми с помощью get_user_pages. Чтобы создать и снести потоковые отображения с использованием указателей struct page, используйте следующее:

 

dma_addr_t dma_map_page(struct device *dev, struct page *page,

                        unsigned long offset, size_t size,

                        enum dma_data_direction direction);

 

void dma_unmap_page(struct device *dev, dma_addr_t dma_address,

                    size_t size, enum dma_data_direction direction);

 

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

Преобразования разборки/сборки

Отображения разборки/сборки представляют собой особый тип потокового отображения DMA. Предположим, что у вас есть несколько буферов и все они должны быть переданы в или из устройства. Эта ситуация может произойти несколькими способами, включающими системные вызовы readv или writev, кластерный дисковый запрос ввода/вывода или печать страниц в отображённом буфере ввода/вывода ядра. Можно просто по очереди отображать каждый буфер и выполнять необходимые операции, но в отображении всего списка сразу есть свои преимущества.

 

Многие устройства могут принимать список разборки (scatterlist) из указателей массивов и длин, и передавать их все за одну операцию DMA; например, сетевое "нулевое копирование" (“zero-copy”) становится проще, если пакеты могут быть собраны из множества кусков. Другой причиной отображать списки разборки как целое является использование преимуществ систем, которые имеют в шинном оборудовании регистры отображения. На таких системах с точки зрения устройства физически разрозненные страницы могут быть собраны в единый непрерывный массив. Этот метод работает только когда записи в списке разборки равны по длине размеру страницы (за исключением первого и последнего), но когда он работает, это может обернуться несколькими операциями в одном DMA и, соответственно, ускорить процесс.

 

Наконец, если должен быть использован возвратный буфер, имеет смысл объединять весь список в один буфер (поскольку он всё равно копируется).

 

Итак, теперь вы убеждены, что в некоторых ситуациях отображение списков разборки является стоящим. Первый шаг в отображении списка разборки заключается в создании и заполнении массива struct scatterlist, описывающего буферы для передачи. Эта структура архитектурно-зависима и описана в <asm/scatterlist.h>. Тем не менее, она всегда содержит три поля:

 

struct page *page;

Указатель struct page, соответствующий буферу, используемому в операции разборки/сборки.

 

unsigned int length;

unsigned int offset;

Длина этого буфера и его смещение на странице.

 

Чтобы отобразить операцию разборки/сборки DMA, для каждого буфера, подлежащего передаче, ваш драйвер должен задать в записи struct scatterlist поля page, offset и length. Затем вызовите:

 

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,

               enum dma_data_direction direction)

 

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

 

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

 

Ваш драйвер должен передавать каждый буфер, возвращаемый dma_map_sg. Шинный адрес и длина каждого буфера хранятся в записях struct scatterlist, но их расположение в записях структуры меняется от одной архитектуры к другой. Чтобы можно было писать переносимый код, были определены два макроса:

 

dma_addr_t sg_dma_address(struct scatterlist *sg);

Возвращает шинный (DMA) адрес из этой записи списка разборки.

 

unsigned int sg_dma_len(struct scatterlist *sg);

Возвращает длину этого буфера.

 

Опять же, помните, что адреса и длины буферов передачи могут отличаться от того, что были переданы в dma_map_sg.

 

После завершения передачи, отображение разборки/сборки отключается с помощью вызова dma_unmap_sg:

 

void dma_unmap_sg(struct device *dev, struct scatterlist *list,

                  int nents, enum dma_data_direction direction);

 

Обратите внимание, что nents должно быть количеством записей, которые вы передавали перед этим в dma_map_sg, а не количеством DMA буферов, которые были вам возвращены функцией.

 

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

 

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,

                         int nents, enum dma_data_direction direction);

void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,

                            int nents, enum dma_data_direction direction);

Двухадресный цикл отображения PCI

Обычно, уровень поддержки DMA работает с 32-х разрядными шинными адресами, возможно, ограниченными маской DMA определённого устройства. Шина PCI, однако, также поддерживает 64-х разрядный режим адресации с циклом двойной адресации (double-address cycle, DAC). Универсальный уровень DMA не поддерживает этот режим по ряду причин, первая из которых в том, что это возможность специфична для PCI. Кроме того, во многих реализациях DAC в лучшем случае глючит и поскольку DAC медленнее, чем обычный, 32-х разрядный DMA, это может быть накладным расходом. Тем не менее, существуют приложения, где использование DAC может быть тем, что делать правильно; если вы имеете устройство, которое может работать с очень большими буферами, размещёнными в верхней памяти, вы можете рассмотреть вопрос о реализации поддержки DAC. Эта поддержка доступна только для шины PCI, так что должны быть использованы процедуры, предназначенные для PCI.

 

Для использования DAC ваш драйвер должен подключить <linux/pci.h>. Вы должны установить отдельную маску DMA:

 

int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);

 

Вы можете использовать DAC адресацию только если эта функция возвращает 0.

 

Для DAC отображений используется специальный тип (dma64_addr_t). Чтобы установить одно из таких отображений, вызовите pci_dac_page_to_dma:

 

dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page,

                                 unsigned long offset, int direction);

 

Вы заметите, что DAC отображения могут быть сделаны только из указателей struct page (в конце концов, они должны находиться в верхней памяти, или в их использовании нет смысла); они должны быть созданы одной страницей за раз. Аргумент direction является эквивалентом PCI для enum dma_data_direction, используемого в универсальном уровне DMA; он должен быть PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE или PCI_DMA_BIDIRECTIONAL.

 

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

 

void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev,

                                     dma64_addr_t dma_addr,

                                     size_t len,

                                     int direction);

 

void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev,

                                        dma64_addr_t dma_addr,

                                        size_t len,

                                        int direction);

Простой пример DMA для PCI

В качестве примера того, как могут быть использованы отображения DMA, приведём простой пример кодирования DMA для PCI устройства. Фактический вид операций DMA на шине PCI сильно зависит от управляемого устройства. Таким образом, этот пример не будет применим к какому-то реальному устройству; вместо этого, он является частью гипотетического драйвера, названного dad (DMA Acquisition Device, Устройство, получающее DMA). Драйвер для этого устройства может определить передающую функцию следующим образом:

 

int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count)

{

    dma_addr_t bus_addr;

 

    /* Отобразить буфер для DMA */

    dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);

    dev->dma_size = count;

    bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count, dev->dma_dir);

    dev->dma_addr = bus_addr;

 

    /* Проинициализировать устройство */

 

    writeb(dev->registers.command, DAD_CMD_DISABLEDMA);

    writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);

    writel(dev->registers.addr, cpu_to_le32(bus_addr));

    writel(dev->registers.len, cpu_to_le32(count));

 

    /* Начать операцию */

    writeb(dev->registers.command, DAD_CMD_ENABLEDMA);

    return 0;

}

 

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

 

void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct dad_dev *dev = (struct dad_dev *) dev_id;

 

    /* Убедиться, что это прерывание действительно от нашего устройства */

 

    /* Отключить буфер DMA */

    dma_unmap_single(dev->pci_dev->dev, dev->dma_addr, dev->dma_size, dev->dma_dir);

 

    /* Только теперь обращаться к буферу безопасно, копируем пользователю, и так далее. */

    ...

}

 

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

DMA для устройств ISA

Шина ISA допускает два вида передачи DMA: простой (native) DMA и управление DMA по шине ISA (ISA bus master DMA). Простой DMA использует для управления сигнальными линиями на шине ISA стандартную схему контроллера DMA на материнской плате. С другой стороны, управление DMA по шине ISA обрабатывается полностью за счёт периферийного устройства. Последний тип DMA используется редко и не требует обсуждения здесь, потому что он похож на DMA для устройств PCI, по крайней мере с точки зрения драйвера. Примером управления по шине ISA является SCSI контроллер 1542, драйвером которого в исходных текстах ядра является drivers/scsi/aha1542.c.

 

Что касается простого DMA, есть три объекта, участвующие в передаче данных DMA по шине ISA:

 

DMA контроллер 8237 (DMAC)

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

 

Периферийное устройство

Устройство должно активировать сигнал запроса DMA, когда оно готово к передаче данных. Фактическая передача управляется DMAC; аппаратное устройство последовательно читает и записывает данные по шине, когда контроллер стробирует устройство. Когда передача закончилась, устройство обычно вызывает прерывание.

 

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

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

 

Оригинальный контроллер DMA, используемый в ПК мог управлять четырьмя "каналами", каждый из которых связан с одним набором регистров DMA. В один момент времени в контроллере DMA могли хранить свою информацию четыре устройства. Новые ПК содержат эквивалент двух устройств DMAC: (* Эти схемы теперь являются частью чипсета материнской платы, но несколько лет назад они были двумя отдельными чипами 8237.) второй контроллер (ведущий) подключен к системному процессору, а первый (ведомый) подключается к каналу 0 второго контроллера. (* Оригинальный ПК имел только один контроллер; второй был добавлен в платформах на основе 286. Тем не менее, второй контроллер подключен как ведущий, поскольку он обрабатывает 16-ти разрядные передачи; первый передаёт только восемь бит за раз и существует для обеспечения обратной совместимости.)

 

Каналы нумеруются как 0 - 7: 4 канал не доступен для ISA периферии, поскольку он используется для внутреннего каскадирования ведомого и ведущего контроллера. Таким образом, доступными каналами являются 0 - 3 на ведомом (8-ми разрядные каналы) и 5 - 7 на ведущем (16-ти разрядные каналы). Размер любой передачи DMA, хранящийся в контроллере, представляет собой 16-ти разрядное число, представляющее собой количество шинных циклов. Таким образом, максимальный размер передачи - 64 Кб для ведомого контроллера (поскольку он передаёт восемь бит за один цикл) и 128 Кб для ведущего (который выполняет 16-ти разрядные передачи).

 

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

Регистрация использования DMA

Вы должны привыкнуть к регистрациям в ядре - мы уже видели их для портов ввода/вывода и линий прерывания. Регистрация канала DMA похожа на другие. После того, как был подключен <asm/dma.h>, могут быть использованы следующие функции для получения и освобождения собственности для канала DMA:

 

int request_dma(unsigned int channel, const char *name);

void free_dma(unsigned int channel);

 

Аргумент channel является числом от 0 до 7, или, точнее, положительным числом, меньше MAX_DMA_CHANNELS. На ПК MAX_DMA_CHANNELS определён как 8 для соответствия оборудованию. Аргументом name является строка, идентифицирующая устройство. Указанное имя появляется в файле /proc/dma, который может быть прочитан пользовательской программой.

 

Возвращаемое значение из request_dma равно 0 для успешного выполнения и -EINVAL или -EBUSY, если была ошибка. Первая означает, что запрашиваемый канал не соответствует диапазону, а последняя означает, что канал занят другим устройством.

 

Мы рекомендуем вам быть такими же внимательными с каналами DMA, как и с портами ввода/вывода и линиями прерывания; гораздо лучше запрашивать канал во время открытия, чем запрашивать его в функции инициализации модуля. Задержка запроса позволяет некоторое совместное использование драйверами; например, ваша звуковая карта и ваш аналоговый интерфейс ввода/вывода может делить канал DMA, пока они не будут использоваться одновременно.

 

Мы также рекомендуем вам запрашивать канал DMA после того, как вы запросили линию прерывания и чтобы вы освободили его перед прерыванием. Это является обычным порядком запроса этих двух ресурсов; следующее соглашение позволяет избежать возможных взаимоблокировок. Обратите внимание, что каждому устройству, использующему DMA, необходима также и линия IRQ; в противном случае оно бы не смогло просигнализировать о завершении передачи данных.

 

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

 

int dad_open (struct inode *inode, struct file *filp)

{

    struct dad_device *my_device;

    /* ... */

    if ( (error = request_irq(my_device.irq, dad_interrupt, SA_INTERRUPT, "dad", NULL)) )

        return error; /* или реализовать блокирующее открытие */

 

    if ( (error = request_dma(my_device.dma, "dad")) ) {

        free_irq(my_device.irq, NULL);

        return error; /* или реализовать блокирующее открытие */

    }

    /* ... */

    return 0;

}

 

Реализация close, которая соответствует только что показанному open, выглядит следующим образом:

 

void dad_close (struct inode *inode, struct file *filp)

{

    struct dad_device *my_device;

 

    /* ... */

    free_dma(my_device.dma);

    free_irq(my_device.irq, NULL);

    /* ... */

}

 

Вот как выглядит на системе с установленной звуковой картой файл /proc/dma:

 

merlino% cat /proc/dma

 1: Sound Blaster8

 4: cascade

 

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

Общение с контроллером DMA

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

 

Драйверу необходимо настроить контроллер DMA либо когда вызываются read или write, или при подготовке к асинхронным передачам. Эта последняя задача выполняется либо во время открытия, или в ответ на команду ioctl, в зависимости от драйвера и политики, которую он реализует. Код, приведенный здесь, представляет собой код, который обычно вызывается методами устройства read или write.

 

В этом разделе приводится краткий обзор внутренностей контроллера DMA, чтобы вы поняли приведённый здесь код. Если вы хотите узнать больше, мы бы настоятельно призвали вас прочитать <asm/dma.h> и некоторые руководства к оборудованию с описанием архитектуры ПК. В частности, мы не имеем дело с вопросом о 8-ти разрядных передач данных взамен 16-ти разрядных. Если вы пишете драйверы устройств для плат ISA устройств, для таких устройств вам следует найти соответствующую информацию в руководстве по оборудованию.

 

Контроллер DMA является разделяемым ресурсом и могла бы возникнуть путаница, если бы его попытался запрограммировать одновременно более чем один процессор. По этой причине контроллер защищён спин-блокировкой, называемой dma_spin_lock. Драйверам  не следует манипулировать с этой блокировкой напрямую, однако, чтобы сделать это, для вас предоставлены две функции:

 

unsigned long claim_dma_lock( );

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

 

void release_dma_lock(unsigned long flags);

Возвращает спин-блокировкой DMA и восстанавливает предыдущий статус прерывания.

 

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

 

Информация, которая должна быть загружена в контроллер, состоит из трёх элементов: адрес ОЗУ, число атомарных объектов, которые должны быть переданы (в байтах или словах), и направление передачи. С этой целью <asm/dma.h> экспортирует следующие функции:

 

void set_dma_mode(unsigned int channel, char mode);

Показывает, какой канал должен читать из устройства (DMA_MODE_READ) или записывать в него (DMA_MODE_WRITE). Существует третий режим, DMA_MODE_CASCADE, который используется для освобождения управления шиной. Каскадирование является способом подключения первого контроллера ко второму, но может также использоваться как настоящие устройства управления шиной ISA. Мы не будем обсуждать здесь управление шиной.

 

void set_dma_addr(unsigned int channel, unsigned int addr);

Присваивает адрес буферу DMA. Функция сохраняет 24 младших значащих бит addr в контроллере. Аргумент addr должен быть шинным адресом (смотрите раздел "Шинные адреса" ранее в этой главе).

 

void set_dma_count(unsigned int channel, unsigned int count);

Задаёт количество байт для передачи. Аргумент count представляет число байтов также и для 16-ти разрядных каналов; в этом случае число должно быть четным.

 

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

 

void disable_dma(unsigned int channel);

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

 

void enable_dma(unsigned int channel);

Данная функция сообщает контроллеру, что канал DMA содержит достоверные данные.

 

int get_dma_residue(unsigned int channel);

Иногда драйверу необходимо знать, когда передача DMA завершена. Эта функция возвращает количество байтов, которые ещё не переданы. Возвращаемое значение 0 после успешной передачи и непредсказуемо (но не 0), пока контроллер работает. Непредсказуемость исходит из необходимости получения 16-ти разрядного остатка через две 8-ми разрядные операции ввода.

 

void clear_dma_ff(unsigned int channel);

Эта функция очищает триггер (flip-flop, регистр данных) DMA. Триггер используется для управления доступом к 16-ти разрядным регистрам. Регистры доступны через две последовательные 8-ми разрядные операции, а для выбора младшего байта (когда он очищается), или старшего байта (если он устанавливается) используется триггер. После передачи восьми бит триггер автоматически меняет состояние; перед обращением к регистрам DMA программист должен очистить триггер (чтобы установить его в известное состояние).

 

Используя эти функции, для подготовке к передаче DMA драйвер может реализовать следующую функцию:

 

int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count)

{

    unsigned long flags;

 

    flags = claim_dma_lock( );

    disable_dma(channel);

    clear_dma_ff(channel);

    set_dma_mode(channel, mode);

    set_dma_addr(channel, virt_to_bus(buf));

    set_dma_count(channel, count);

    enable_dma(channel);

    release_dma_lock(flags);

 

    return 0;

}

 

Затем для проверки для успешного завершения DMA используется следующая функция:

 

int dad_dma_isdone(int channel)

{

    int residue;

    unsigned long flags = claim_dma_lock ( );

    residue = get_dma_residue(channel);

    release_dma_lock(flags);

    return (residue == 0);

}

 

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

Предыдущая  Содержание  Следующая