Архив рубрики: Программирование драйверов


Глава 8. Управление устройствами

    Содержание

  1. Порты ввода/вывода и память ввода/вывода
  2. Использование портов ввода/вывода
  3. Управление цифровыми устройствами ввода/вывода
  4. Использование памяти ввода/вывода
  5. Пример отображения: рисуем из модуля ядра
  6. Вопросы обратной совместимости
  7. Краткий справочник определений

Игра с драйвером scull и похожими "игрушками" позволяет получить хорошее начальное представление о программном
интерфейсе драйверов устройств в Linux. Однако, реализация реального драйвера требует реального устройства. Драйвер
представляет собой абстрактный слой между программными концепциями и электронными схемами. На протяжении семи предыдущих
глав, мы знакомились с программными концепциями. В этой главе мы завершим картину рассказом о том как драйвер может
получить доступ к портам ввода/вывода и памяти ввода/вывода на Linux платформе.

Данная глава продолжает традицию максимально возможной независимости от особенностей специфических устройств при
подготовке примеров программирования. Поэтому, при подготовке примеров требующих работы с устройствами мы будем
использовать то, что уже есть или может оказаться под рукой. Для демонстрации работы инструкций ввода/вывода мы будем
использовать стандартный параллельный порт, а для примеров ввода/вывода отображенного в память (memory-mapped I/O) мы будем
использовать обычный frame-buffer видео памяти.

Цифровой ввод/вывод в параллельный порт является, наверное, простейшим примером программирования портов ввода/вывода.
Параллельный порт Centronics реализует "сырой" ввод/вывод и доступен на большинстве компьютеров. Биты данных,
которые пишутся в устройство напрямую отображаются на выходных контактах параллельного порта. И наоборот, уровень входного
напряжения на этих контактах может быть напрямую прочитан процессором. Если говорить о практической стороне, то все что
нужно сделать, чтобы увидеть результат цифрового вывода в параллельный порт, достаточно подключить к определенным
контактам обычный светодиод. В дальнейшем мы увидим, что управление таким светодиодом представляет собой достаточно простую задачу.

Порты ввода/вывода и память ввода/вывода

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

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

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

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

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

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

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

Регистры ввода/вывода и обычная (conventional) память

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

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

Время доступа к памяти весьма критично для производительности системы, поэтому, при отсутствии побочных эффектов при операциях с памятью, существует несколько способов оптимизации таких операций: значения могут кэшироваться, а для инструкций чтения/записи может быть изменен порядок вызова. Компилятор, например, может выполнить оптимизацию циклов и упрощение арифметическо-логических выражений в тексте восокоуровневой программы, что приведет к совершенно неожидаемой последовательности инструкций ввода/вывода. Кроме того, некоторые виды такой оптимизации могут быть выполнены на уровне арифметико-логического устройства процессора. Часто, такое изменение последовательности исполненяемых инструкций может привести к повышению быстродействия, по сравнению с нормальной последовательностью, описанной в тексте программы. Например, это может предотвратить взаимную блокировку RISC конвейера. На процессорах архитектуры CISC, долгие операции могут выполняться параллельно с более быстрыми операциями.

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

Проблема аппаратного кэширования и изменения последовательности вызова инструкций чтения/записи решается установкой барьеров памяти ("memory barrier") между операциями, которые должны быть видимы аппаратным устройством (или другими процессами) в обычном, записанном в тексте программы, порядке. Linux, для этих целей, предлагает четыре макроса.

Использование портов ввода/вывода

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

Для начала давайте вспомним, что порты ввода/вывода должны быть распределены перед их использованием в драйвере. В разделе
"Порты ввода/вывода и память ввода/вывода" главы 2 "Построение и запуск модулей" мы рассматривали функции, используемые для захвата (распределения) и освобождения портов:

#include <linux/ioport.h>
int check_region(unsigned long start, unsigned long len);
struct resource *request_region(unsigned long start, 
       unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);

После того, как требуемый диапазон портов был запрошен и получен, драйвер может выполнить операции чтения/записи этих портов. Большая часть аппаратных платформ делает различие между 8,16 и 32-битовыми портами. Поэтому, как правило, вы не можете свободно
выбирать разрядность операций ввода/вывода, исходя из удобства программирования, как вы это делаете при обращении к обычной памяти. Здесь все будет определяться возможностями управляемого устройства.

Иногда, порты ввода/вывода представлены схемами адресации, схожими со схемами адресации памяти, и вы можете заменить две последовательные (по адресу) 8-ми битовые операции на одну 16-ти битовую. Например, такая замена применима при работе с видеоадаптерами для PC, но, в общем случае, вы не можете рассчитывать на такую возможность.

По этой причине, программа на языке Си должна вызывать различные функции для доступа к портам разной разрядности. Как уже говорилось в предыдущем разделе, компьютерные архитектуры, которые поддерживают отображение регистров ввода/вывода в память, подменяют отображаемые адреса портов ввода/вывода на адресацию к памяти, и ядро скрывает детали реализации этого механизма от драйвера для обеспечения лучшей портабельности драйверов. В заголовочных файлах ядра Linux (особенно, в архитектурно-зависимом заголовочном файле <asm/io.h>) определены следующие inline-функции для доступа к портам ввода/вывода.

Замечание: В дальнейшем будет использоваться ключевое слово unsigned без дальнейшего уточнения типа. Такая условность будет нести нагрузку аппаратно-зависимых определений, которые могут быть уточнены только для конкретной платформы. Такие функции почти всегда портируемы, т.к. компилятор автоматически выполнит подстановку платформо-зависимого типа данных без соответствующих предупреждений на этапе компиляции. Ответственность за потерю информации о значении числа при таком преобразовании лежит на плечах программиста — необходимо учитывать особенности платформ и использовать корректные значения. За таким соглашением, мы закрепим понятие "нечеткое приведение типа" (incomplete typing), которое будем использовать в оставшейся части данной главы.

unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
Функции чтения и записи в однобайтовые (8-ми битовые) порты. Аргумент port, в зависимости от платформы, может быть определен как unsigned long, unsigned short и т.п. Тип возвращаемого значения для функции inb() также может быть различным на различных платформах.
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
Функции доступа к 16-ти битовым портам (т.е. портам длиною в "слово" (word)). Данные функции недоступны
для платформ M68k и S390, которые поддерживают только байтовый ввод/вывод.
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
Функции доступа к 32-х битовым портам. Величина "длинное слово" (longword) может определяться как unsigned long или unsigned int в зависимости от платформы.

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

Только что описанные функции могут быть использованы не только в адресном пространстве ядра, но и в пространстве пользователя, по крайней мере на компьютерах класса PC. Библиотека GNU Си определяет эти функции в заголовочном файле <sys/io.h>. Для того, чтобы использовать эти функции в программах пространства пользователя необходимо учитывать следующие соглашения:

  • Программа должна быть скомпилирована с опцией компиляции -O для принудительного inline-расширения соответствующих функций.
  • Для получения разрешения на использование операций ввода/вывода необходимо обратиться к системным функциям ioperm() или iopl(). Функция ioperm() дает и снимает разрешение на использование заданного поддиапазона портов в диапазоне от 0 до 0x3FF (первые 1024 порта), в то время как функция iopl() может предоставить доступ ко всему диапазону портов. Обе эти функции являются специфичными для платформы Intel.
  • Программа должна быть запущена от пользователя root для выполнения функций ioperm() или iopl(). Alternatively, one of its ancestors must have gained port access running as root.

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

Если ваша Linux-платформа не поддерживает системные вызовы ioperm() и iopl(), то задачи пространства пользователя могут попробовать получить доступ к портам ввода/вывода используя файл устройства /dev/port. Хотя, необходимо заметить, что значение данного файла специфично для каждой из платформ, и, наиболее вероятно, что данный файл является бесполезным для любой платформы, кроме PC.

В каталоге примеров вы можете посмотреть файлы misc-progs/inp.c и misc-progs/outp.c которые демонстрируют создание простейших инструментов в пространстве пользователя для чтения и записи портов из командной строки. Данные инструменты устанавливаются в систему под различными именами, типа inpb, inpw, inpl и позволяют манипулировать одно, двух и четырехбайтовыми портами, в зависимости от того, какое имя инструмента будет использовано пользователем. Код этих инструментов построен так, что если системный вызов ioperm() не представлен в системе, то доступ к портам осуществляется через файл устройства /dev/port.

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

Строковые операции

В дополнении к описанным выше однобайтовым, двухбайтовым и четырехбайтовым операциям ввода/вывода, некоторые процессоры поддерживают специальные инструкции по передаче последовательности байт, слов (два байта) или двойных слов (четыре байта) в/из заданного порта ввода/вывода. Такие инструкции называются строковыми (string instructions) и выполняются несколько быстрее передачи такого же объема данных через обычный цикл языка Си. Представим макрос, реализующий концепцию строкового ввода/вывода, который либо использует одну машинную инструкцию (при соответствующей поддержки процессором), либо выполняет цикл передачи (если процессор не поддерживает строковые операции ввода/вывода). Данный макрос не определен для платформ M68k и S390. Данные платформы имеют настолько специфическую шинную архитектуру, что написание портируемых драйверов использующих шинный обмен данными для этих и других платформ в принципе невозможно.

Прототипы строковых функций выглядят следующим образом:

void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
Читаем или пишем count байт, начиная с адреса памяти addr из/в порт port.
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
Читаем или пишем count 16-битовых значений в 16-битовый порт.
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
Читаем или пишем count 32-битовых значений в 32-битовый порт.

Ввод/вывод с задержкой

На некоторых платформах — в особенности на i386 — могут возникнуть проблемы, когда процессор пытается передать данные по шине слишком быстро. Такие проблемы могут возникать по причине того, что процессор работает на более высокой частоте, по сравнению, например, с шиной ISA. Проблемы могут проявиться на слишком медленных устройствах. Решением этой проблемы может быть реализация некоторой небольшой задержки между инструкциями ввода/вывода. Если ваше устройство теряет данные, или вы боитесь, что такая потеря может произойти, то, вместо обычных функций ввода/вывода описанных выше, вы можете использовать специальный набор функций, реализующих такую задержку. Имена этих функций отличаются от обычных окончанием "_p". Например, inb_p, outb_p и т.д. Такие функции определены для большинства архитектур, хотя, при компиляции на некоторые платформы, они расширяются на обычные, функции, не использующие временную задержку. Такая замена производится для тех платформ, архитектура которых исполнена на современных шинах, передача данных по которым не требует временных задержек.

Особенности платформ

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

Одной из таких архитектурных особенностей, является тип адреса порта. Вспомните сигнатуру функций ввода/вывода, в которой приведение типа зависит от используемой аппаратной архитектуры. Например, для архитектуры x86, адрес порта является типом unsigned short (процессор поддерживает 64-х килобайтное пространство ввода/вывода), в то время как на других платформах, которые трактуют порты как некую часть общего адресного пространства памяти, это unsigned long.

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

IA-32 (x86)
Архитектура поддерживает все функции, описанные в этой главе. Адреса портов имеют тип unsigned short.
IA-64 (Itanium)
Поддерживаются все функции. Адреса портов имеют тип unsigned long. Реализовано отображение памяти (memory map). Строковые функции реализованы циклами на языке Си.
Alpha
Поддерживаются все функции и отображение памяти (memory map). Реализация портов ввода/вывода различается на различных Alpha-платформах, в зависимости от используемого чипсета. Строковые функции реализованы циклами на языке Си и лежат в файле arch/alpha/lib/io.c. Адреса портов имеют тип unsigned long.
ARM
Поддерживаются все функции и отображение памяти (memory map). Строковые функции реализованы циклами на языке Си. Адреса портов имеют тип unsigned int.
M68k
Порты отображаются в память. Поддерживаются только байтовые функции. Строковые функции не поддерживаются. Адреса портов имеют тип unsigned char*.
MIPS
MIPS64
Поддерживаются все функции. Строковые операции реализованы циклом (процессор не поддерживает строкового ввода/вывода). Порты отображены в память. Адреса портов unsigned int на 32-х разрядных процессорах, и unsigned long на 64-х разрядных.
PowerPC
Поддерживаются все функции. Порты имеют тип unsigned char *.
S390
Также как и M68k, платформа поддерживает только байтовые порты ввода/вывода и не имеет строковых операций. Порты отображены в память. Адреса портов определены как указатели на char.
Super-H
Поддерживаются все функции и отображение памяти (memory map). Адреса портов имеют тип unsigned int.
SPARC
SPARC64
Пространство ввода/вывода отображено в память. Функции ввода/вывода определены для работы с адресами портов unsigned long.

Более полную информацию можно извлечь из файлов io.h для каждой из платформ. В этих файлах определены архитектурно-зависимые функции в дополнение к тем, которые описаны в этой главе. Однако, считаем нужным предупредить, что некоторые из этих файлов прочитать не просто.

Интересно заметить, что за исключением семейства процессоров x86, нигде более не используется отдельное адресное пространство для портов, хотя некоторые из поддерживаемых архитектур используют ISA и PCI слоты, для которых реализован различный механизм ввода/вывода и различные адресные пространства.

Кроме того, некоторые процессоры (в особенности ранние версии Alpha-процессоров) не имели инструкций, которые могли передавать более одного или двух байтов по шине ввода/вывода. По этой причине, используемые на таких платформах чипсеты, эмулируют 8-ми и 16-ти битовую передачу отображением ее в специальный адресный диапазон общего адресного пространства памяти. Таким образом, инструкции inb и inw реализуются двумя 32-х битовыми операциями чтения памяти, которые оперируют в другом адресном пространстве. К счастью, все эти механизмы скрыты от разработчика драйверов с помощью макросов, описанных в заголовочном файле include/asm-alpha/core_lca.h.

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

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

Управление цифровыми устройствами ввода/вывода

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

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

Реализация и программный интерфейс таких устройств ввода/вывода меняется от системы к системе. Наиболее часто, входные и выходные контакты устройства управляется двумя наборами портов (областей памяти, в случае соответствующего отображения). Через один набор производится настройка устройства (выбор контактов используемых для чтения и для записи), а через другой — непосредственно чтение/запись логических значений. В некоторых случаях реализация еще проще — например, все контакты уже аппаратно разведены либо на чтение, либо на запись. Однако, такое устройство уже нельзя назвать устройством общего назначения. Такое устройство как "параллельный порт&qout; реализованное на всех персональных компьютерах является примером такого устройства НЕ общего назначения.

Краткий обзор "Параллельного порта"

Так как, по нашему предположению, большинство читателей используют платформу x86, в реализации называемой PC — "персональный компьютер", мы считаем полезным разъяснить некоторые детали реализации параллельного порта для персонального компьютера. Именно это устройство мы выбрали для управления нашим драйвером. Скорее всего, у читателей есть спецификация параллельного порта, однако, мы остановимся на ключевых моментах его реализации для общего принятия используемой нами терминологии.

Параллельный интерфейс в своей минимальной конфигурации (опустим режимы ECP и EPP) состоит из трех восьми битовых портов, которые, согласно стандарту PC, начинаются с адреса 0x378 для первого параллельного интерфейса, и с адреса 0x278 — для второго. Первый порт реализован как двунаправленный регистра данных, биты которого соответствуют контактам 2-9 физического разъема. Второй порт, доступный только для чтения, связан с регистром статуса. Когда, например, параллельный порт используется для подключения принтера, то через регистр статуса мы можем получить информацию о текущем состоянии принтера. Третий порт, доступный только для записи, связан с управляющим регистром, через который, среди прочего, можно разрешать или запрещать использование прерываний.

Сигнальные уровни, используемые в параллельном интерфейсе являются уровнями стандартной Транзистор-Транзисторную Логики (TTL): 0 и 5 вольт, с порогом около 1.2 вольт. Требования по току выполняются, наверное, для всех реализаций интерфейса, а большинство современных реализаций удовлетворяет как требования по току, так и по напряжению.

ПРЕДУПРЕЖДЕНИЕ: Контакты параллельного порта не изолированы от внутренних цепей компьютера (…, which is useful if you want to connect logic gates directly to the port). Вы должны быть осторожны при подключении вашего устройства к компьютеру — цепи параллельного порта могут быть легко повреждены неправильным подключением из-за отсутствия такой изоляции. You can choose to use plug-in parallel ports if you fear you’ll damage your motherboard.

Описание битов представлено на рисунке 8-1. Вы можете получить доступ к 12 выходным битам и пяти входным, некоторые из которых логически инвертируются на своем сигнальном пути. Только один бит не имеет связанного с ним контакта — это 4-й бит (0x10) 2-го порта. Этот бит разрешает прерывания от параллельного порта. Мы будем использовать этот бит в реализации обработчика прерываний, который мы рассмотрим в главе 9 "Обработка прерываний".

Пример драйвера

Драйвер, который мы хотим вам представить был назван "short" — английский акроним от Simple Hardware Operations and Raw Tests. Все что он делает, это чтение и запись нескольких восьмибитовых портов, начиная с адреса, заданного во время загрузки. По умолчанию, драйвер использует диапазон портов принадлежащий параллельному интерфейсу PC (персонального компьютера). Через файловые интерфейсы к драйверу, в зависимости от младшего номера, мы можем получить доступ к разным портам. Драйвер "short" не делает ничего полезного, кроме предоставления некого дополнительного интерфейса доступа к портам. Вы можете использовать этот драйвер для управления портами, можете замерить время передачи данных в порт через интерфейс драйвера, а можете сделать что-нибудь еще, что вам покажется интересным.

Для того чтобы драйвер "short" работал на вашей системе, он должен иметь свободный доступ к устройству (по умолчанию, это параллельный интерфейс PC). Другими словами, другие драйверы не должны мешать ему в доступе к устройству. Большинство современных дистрибутивов предоставляют драйвер параллельного порта в виде модуля, который загружается при необходимости. Поэтому, можно предположить, что между нашим драйвером и "родным" драйвером параллельного порта не возникнет конфликта. Однако, если вы получите сообщение "can’t get I/O addres" от драйвера "short" (в консоли, из которой драйвер был загружен, или в файле системного лога), то это означает, что какой-то другой драйвер уже занял этот адресный диапазон под свое использование. Как правило, просмотрев файл /proc/ioports вы можете узнать, какой из драйверов удерживает требуемый адрес порта. Описываемый здесь конфликт ресурсов может возникнуть по отношению к любым драйверам устройств и не является прерогативой параллельного интерфейса.

Для упрощения формулировок, начиная с этого момента, мы будем говорить только о "параллельном интерфейсе". Однако, установив параметр модуля base во время загрузки драйвера, вы можете перенаправить работу драйвера "short" на другое устройство ввода вывода. Такая возможность позволяет нашему коду работать на любой Linux-платформе, где возможно получить доступ к цифровому интерфейсу ввода/вывода с помощью команд inb, outb. Вообще, все реальные аппаратные платформы, за исключением платформы x86, имеют отображение портов в память. Наш драйвер может работать и на таких платформах, и позднее, в разделе "Использование памяти ввода/вывода", мы покажем, как драйвер "short" может быть использован для работы на платформах с отображением портов в память.

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

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

Если вы собираетесь просматривать выводимые на параллельный порт данные с помощью спаянного устройства из светодиодов, резисторов и ответной части разъема параллельного порта (D-type connector), то мы надеемся, что вы не задействуете контакты 9 и 10. Мы будем использовать эти контакты позднее для запуска примера из главы 9 "Обработка прерываний". Там нам понадобиться соединить между собой эти два контакта.

Поясним нотацию выбранную для обозначения имен файлов-интерфейсов к нашему драйверу "short". /dev/short0 — пишет и читает из 8-ми битового порта по адресу base = 0x378 адресного пространства ввода/вывода. При загрузке драйвера вы можете сменить базовый адрес. /dev/short1 — пишет в восьмибитовый порт по адресу base+1. То же самое до base+7.

В действительности, операции вывода, выполняемые /dev/short0, реализованы на программном цикле использующем outb. A memory barrier instruction is used to ensure that the output operation actually takes place and is not optimized away.

while (count--) {
    outb(*(ptr++), address);
    wmb();
}

Вы можете зажечь ваши светодиоды с помощью следующей команды:

echo  -n "any string"  > /dev/short0

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

Чтение выполняется похожим способом, только вместо вызова outb() выполняется inb(). Для того, чтобы прочитать что-нибудь значащее из параллельного порта, необходимо подключить к соответствующим контактам параллельного интерфейса какое-нибудь устройство, способное производить сигналы. Если такого устройства не использовать, то мы прочитаем бесконечный поток одинаковых байт. Если вы выполните чтение с выходного порта, то, наиболее вероятно, вы прочитаете последнее записанное в этот порт значение. Так реализован параллельный интерфейс и большинство других цифровых цепей ввода/вывода. Хотя, надо признаться, что реализация возможности прочитать последнее записанное значение, для большинства устройств, скорее инженерная прихоть, нежели жизненная необходимость. В общем, как бы там не было, мы можем попробовать прочитать последнее выводимое в порт значение с помощью, например, следующей команды:

dd if=/dev/short0 bs=1 count=1 | od -t x1

Для демонстрации использования всех инструкций ввода/вывода, мы представляем три варианта интерфейса к драйверу "short" в следующей нотации имен файлов: /dev/short0 выполняет программный цикл, как показано в примере выше, /dev/short0p использует функции с задержкой outb_p() и inb_p() вместо "быстрых" функций, и /dev/short0s использует строковые инструкции. Все это повторяется восемь раз, от short0 до short7. И хотя параллельный интерфейс персонального компьютера имеет только три порта, вам может понадобиться большее количество портов если вы будете использовать другое устройство для работы с нашим тестовым драйвером.

Драйвер "short" выполняет абсолютный минимум действий по управлению устройством, но в нем показаны все инструкции, которые могут быть использованы при при создании реального драйвера. Интересующиеся читатели могут познакомиться с источниками модулей parport и parport_pc, чтобы посмотреть сложность реализации поддержки общепринятого для параллельного интерфейса набора устройств (принтеры, ленточные накопители, сетевые интерфейсы и пр.).

Использование памяти ввода/вывода

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

Памятью ввода/вывода называют область адресов, неотличимую на верхнем уровне с областью ОЗУ (RAM), которую устройство предоставляет процессору через соответствующую шину. Эта память может быть использована по разному — для удержания видеоданных или пакетов протокола Ethernet. также как и реализация регистров устройства, которые ведут себя как порты ввода/вывода (т.е имеют побочные эффекты (side effects) связанные с чтением и записью в них).

Способ, используемый для доступа к памяти ввода/вывода зависит от компьютерной архитектуры, устройства шины, и, конечно же, используемого устройства. Однако, принципы этой реализации везде одинаковы. В этой главе, обсуждая эти общие принципы, мы коснемся в деталях, главным образом, памяти ISA и PCI. В этой главе мы ознакомимся с памятью PCI, но полное обсуждение PCI вы увидите в главе 15 "Обзор периферийных шин".

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

Используется ли, или нет ioremap() для доступа к памяти ввода/вывода, но прямое использование указателей для доступа к этой памяти является нерекомендуемой практикой. Несмотря на то, что память ввода/вывода адресуется, на аппаратном уровне, как обычное ОЗУ (см. "Порты ввода/вывода и память ввода/вывода"), следует избегать использования обычных указателей (см. "Регистры ввода/вывода и обычная память"). Использование оберточных функций для доступа к памяти ввода/вывода безопасно на всех платформах и оптимизировано наилучшим для каждой платформы способом.

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

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

int check_mem_region(unsigned long start, unsigned long len);
void request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);

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

if (check_mem_region(mem_addr, mem_size)) {
    printk("drivername: memory already in use\n");
    return -EBUSY;
}
    request_mem_region(mem_addr, mem_size, "drivername");

    [...]

    release_mem_region(mem_addr, mem_size);

Прямо отображенная память

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

Процессоры MIPS, используемые в PDA (personal digital assistant — персональный цифровой помощник), предлагают интересный пример такой конфигурации. Два адресных диапазона, по 512МБт каждый, прямо отображаются в физические адреса. Любое обращение к памяти любого из этих диапазонов обходит MMU (Memory Management Unit), а обращение к одному из этих диапазонов обходит еще и кэш. Последняя, 512-ти мегабайтная область резервируется для периферийных устройств, и драйвера могут получить доступ к своей памяти ввода/вывода напрямую, не используя механизм кэширования.

Другие платформы имеют другие способы прямого отображения адресных диапазонов. Некоторые имеют специальные адресные пространства для разыменования физических адресов (например, SPARC64 имеет специальный "address space identifier" для этой цели), а другие используют особые виртуальные адреса обходящие кэш.

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

unsigned readb(address);
unsigned readw(address);
unsigned readl(address);

Эти макросы используются для получения 8-ми, 16-ти и 32-х битовых значений данных из памяти ввода/вывода. Преимущество использование макроса состоит в безтиповости аргумента: тип определяется непосредственно перед использованием. Процитируем комментарий из файла asm-alpha/io.h — "если еще не ясно целое это или указатель, то мы примем оба варианта". Ни функции чтения, ни функции записи не проверяют корректность адреса, стараясь получить максимум производительности.

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

Как и вышеописанные функции, эти функции (макросы) используются для записи 8-ми, 16-ти и 32-х битовых данных.

memset_io(address, value, count);

Когда вам необходимо выполнить "memset" над памятью ввода/вывода, вы можете воспользоваться этой функцией. (When you need to call memset on I/O memory, this function does what you need, while keeping the semantics of the original memset.)

memcpy_fromio(dest, source, num);
memcpy_toio(dest, source, num);

Эти функции перемещают блоки данных в/из памяти ввода/вывода и похожи на функцию memcpy() из библиотеки языка Си.

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

Некоторые, 64-разрядные платформы, предлагают еще функции readq() и writeq() для восьмибайтовых операций с памятью на шине PCI. Суффикс "Q" образовался от термина "quad-word", который является историческим пережитком со времен, когда процессоры оперировали 16-ти битовыми словами (Один "word" — шестнадцать бит). Суффикс "L", образованный от "long word" также является некорректным в этом смысле. Однако, переименование суффиксов, используя какую-нибудь более современную нотацию, приведет, наверное, еще к большей путанице.

Использование драйвера "short" для операций с памятью ввода/вывода

Пример модуля short, представленный ранее для доступа к портам ввода/вывода, также, может быть использован для доступа к памяти ввода/вывода. Для этого, вы должны сообщить об этом драйверу во время его загрузки. При этом не забудьте изменить базовый адрес для указания на требуемую область ввода/вывода.

Приведем пример такого использования драйвера short для засветки нашего светодиодного устройства на платформе MIPS:

 mips.root# ./short_load use_mem=1 base=0xb7ffffc0
 mips.root# echo -n 7 > /dev/short0

Использование драйвера short для памяти ввода/вывода и для портов ввода/вывода одинаково, с той лишь разницей, что использование файловых интерфейсов /dev/short0p и /dev/short0s полностью аналогично использованию /dev/short0. Т.е. для памяти ввода/вывода не существует инструкций выполняющих строковую передачу данных, или передачу данных с временными задержками между атомами данных.

Следующий фрагмент кода показывает цикл, используемый драйвером short, для записи в память:

while (count--) {
    writeb(*(ptr++), address);
    wmb();
}

Заметьте, использование в коде операции "write memory barrier — wmb()". Так как, на многих архитектурах, операция writeb() может превратиться в операцию обычного присвоения, использование wmb() гарантирует нам, что записать будет произведена ожидаемым способом.

Программное отображение памяти ввода/вывода

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

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

Для программного доступа к памяти ввода/вывода необходимо назначить устройству виртуальный адрес. Эту роль выполняет функция ioremap(), описанная в разделе "Семейство функций vmalloc" ("vmalloc and Friends". Эта функция, рассмотренная в предыдущей главе посвященной вопросам использования памяти, специально разработана для назначения виртуальных адресов областям памяти ввода/вывода. Кроме того, разработчики ядра реализовали функцию ioremap() таким образом, что она не делает ничего, если применяется к прямо отображенному адресному пространству ввода/вывода.

После применения функции ioremap() (и iounmap()), драйвер устройства может получить доступ к памяти ввода/вывода независимо от того, является ли это отображение в виртуальное адресное пространство прямым, или нет. Однако помните, что прямое разыменование этих адресов на некоторых платформах недопустимо. Вместо прямого разыменования желательно использовать функции семейства readb(). Таким образом, мы можем приспособить драйвер short для работы как с памятью ввода/вывода в архитектуре MIPS, так и с наиболее общим случаем использования памяти устройств ISA/PCI на платформе x86, добавлением в код модуля вызовов ioremap()/iounmap() везде где анализируется параметр use_mem.

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

Функции вызываются согласно следующим определениям:

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);

Прежде всего, обратим ваше внимание на новую функцию ioremap_nocache(). Мы не обсуждали ее в главе 7 "Работа с памятью", потому что ее значение во многом зависит от используемого "железа" Приведем цитату из одного заголовочного файла ядра: "It’s useful if some control registers are in such an area and write combining or read caching is not desirable." В действительности, реализация этой функции идентична реализации ioremap() для большинства компьютерных платформ: если вся память ввода/вывода видна через некэшируемые адреса — нет причины делать отдельную реализацию для некэшируемого варианта ioremap().

Другим важным замечанием по функции ioremap() является ее особое поведение для версии ядра 2.0. Для ядра Linux 2.0 эта функция, называемая тогда vremap(), отказывала в отображении областям памяти, которые не были выравнены по странице. Разумность этого выбора основывалась на том, что процессор работает только в контексте страничных квантов. Однако, иногда возникает необходимость в отображении маленьких областей регистров, чьи физические адреса не выравнены по странице. Поэтому, начиная с версии ядра 2.1.131, ядро может отображать не выравненные по странице адреса.

Для обратной совместимости с версией ядра 2.0, в модуле short, для доступа к невыравненным по странице множествам регистров, используется следующий код, вместо прямого вызова ioremap():

 /* Remap a not (necessarily) aligned port region */
 /* Отображение областей портов (область может быть не выравнена по странице) */
void *short_remap(unsigned long phys_addr)
{
    /* The code comes mainly from arch/any/mm/ioremap.c */
    unsigned long offset, last_addr, size;

    last_addr = phys_addr + SHORT_NR_PORTS - 1;
    offset = phys_addr & ~PAGE_MASK;
    
    /* Adjust the begin and end to remap a full page */
    phys_addr &= PAGE_MASK;
    size = PAGE_ALIGN(last_addr) - phys_addr;
    return ioremap(phys_addr, size) + offset;
}


/* Unmap a region obtained with short_remap */
/* Снятие отображения с области, отображенной функцией short_remap() */
void short_unmap(void *virt_add)
{
    iounmap((void *)((unsigned long)virt_add & PAGE_MASK));
}

Работа с памятью ISA ниже 1МБт

Одной из хорошо известных областей памяти ввода/вывода является память устройств на шине ISA. Это диапазон адресов памяти между 640КБ (0xA0000) и 1МБт (0x100000). Т.е. она расположена, как-бы внутри, обычного системного ОЗУ. Такое позиционирование адресов может показаться немного странным, и в настоящее время является артефактом относящемуся к началу 1980-х годов, когда объем памяти в 640КБ казался заведомо больше того, что может кому-то понадобиться.

Этот диапазон адресов не относится к классу прямо отображаемых адресов памяти. Вы можете прочитать или записать несколько байт этого диапазона памяти используя модуль short, установкой параметра use_mem во время загрузки.

Примечание: В действительности это не совсем верно. Этот диапазон памяти настолько мал и так часто используется, что ядро, во время загрузки, строит таблицы страниц для доступа к этим адресам. Однако, виртуальный адрес, используемый для доступа к этой области, не тоже самое, что физический адрес, и, вызов ioremap(), по прежнему, необходим. Кроме того, версия ядра 2.0 использовала прямое отображение этой области. См. раздел "Вопросы обратной совместимости".

Хотя ISA память ввода/вывода присутствует только в компьютерах класса x86, мы думаем, что имеет смысл, рассказать несколько слов о простом драйвере, использующем эту память.

В этой главе мы не будем обсуждать память PCI — простейший вариант памяти ввода/вывода: если вы знаете физический адрес, то вы должны просто отобразить его для получения к нему доступа. "Проблема" с памятью ввода/вывода PCI заключается в том, что мы не можем использовать ее для примера в этой главе потому, что мы не знаем ни физических адресов вашего PCI-пространства, ни того, насколько безопасным для вашей системы будет использование этих адресов. Таким образом, диапазон адресов памяти ввода/вывода ISA более подходит для запуска нашего демонстрационного кода.

Для демонстрации доступа к памяти ISA, мы будем использовать еще один простенький модуль, который, также, можно найти в каталоге примеров к оригинальному английскому варианту данной книги. Мы назвали этот модуль "silly" — как синоним к акрониму "Simple Tool for Unloading and Printing ISA Data". Примечание переводчика: в английском языке слова "stupid" и "silly" являются синонимами, означающими: глупый, тупой, бестолковый; дурацкий.

Данный модуль реализует функциональность модуля "short", предоставляя доступ ко всему 384-х килобайтному пространству ISA, с использованием различных функций ввода/вывода. Т.е. обращаясь к четырем различным файловым интерфейсам драйвера, мы можем выполнить одну и ту же задачу, используя разные функции передачи данных. Эти четыре файловых интерфейса к драйверу "silly" можно воспринимать как окно в память ввода/вывода, примерно как интерфейс /dev/mem. Используя драйвер, вы можете выполнять операции read, write и lseek в адресном пространстве ввода/вывода.

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

#define ISA_BASE    0xA0000
#define ISA_MAX    0x100000  /* for general memory access */

    /* this line appears in silly_init */
    io_base = ioremap(ISA_BASE, ISA_MAX - ISA_BASE);

Функция ioremap() возвращает указатель, который мы можем использовать для функций семейства readb, рассмотренных в разделе "Прямо отображенная память".

Вернемся к коду нашего модуля для рассмотрения использования этих функций. Через файловый интерфейс /dev/sillyb, имеющий младший номер (номер устройства) равный 0, мы можем получить доступ к памяти ввода/вывода через функции readb() и writeb(). Следующий пример показывает реализацию чтения адресов в диапазоне 0xA0000-0xFFFFF, доступных как виртуальный файл с пространством адресов 0x0-0x5FFFF. Запрос на чтение структурирован в драйвере оператором switch для различных режимов доступа. Вариант для sillyb выглядит следующим образом:

 case M_8: 
  while (count) {
      *ptr = readb(add);
      add++; count--; ptr++;
  }
  break;

Следующие два устройства /dev/sillyw (номер устройства 1) и /dev/sillyl (номер устройства 2) предоставляют доступ к нашему диапазону адресов используя 16-ти и 32-х битовые функции. Приведем в качестве примера реализацию записи для устройства sillyl (вариант оператора switch):

 case M_32: 
  while (count >= 4) {
      writel(*(u32 *)ptr, add);
      add+=4; count-=4; ptr+=4;
  }
  break;

Последнее устройство /dev/sillycp (номер устройства 3) использует memcpy_*io() функции для выполнения тех же самых задач. Приведем пример реализации операции чтения:

 case M_memcpy:
  memcpy_fromio(ptr, add, count);
  break;

Так как для получения доступа к адресному пространству ISA, мы использовали функцию ioremap(), то для снятия этого отображения адресов, при выгрузке модуля необходимо вызвать функцию iounmap():

 iounmap(io_base);

Семейство функций isa_readb

Просматривая источники ядра, мы можем увидеть множество процедур семейства isa_readb. Т.е. каждая из рассмотренных нами функций чтения/записи пространства памяти ввода/вывода имеет свой isa_* эквивалент. Эти функции обеспечивают доступ к памяти ISA без предварительного отображения требуемых адресов через ioremap(). Однако, разработчики ядра предупреждают, что использование этих функций следует избегать в будущем, так как они могут не поддерживаться в следующих версиях ядра, и предназначены были только для экспериментальных целей доступа.

Поиск свободной области в памяти ISA

Хотя большинство современных устройств предпочитают более современные шинные архитектуры (например, PCI), однако, некоторым программистам приходится иметь дело с устройствами ISA, и их памятью ввода/вывода. Поэтому, мы посвятим еще одну страницу работе с этими устройствами. Мы не будем касаться верхней памяти ISA (так называемая "дыра" в физическом адресном пространстве в диапазоне 14-16МБт), так как в настоящее время этот тип памяти крайне редок, и не поддерживается большинством современных материнских плат и ядер операционных систем. Для доступа к этому диапазону памяти вам потребуется изменить последовательность инициализации ядра, что лежит за пределами данной книги.

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

Использование подсистемы управления ресурсом памяти может быть полезна для определения свободных областей памяти, так как она может определить области, которые были заняты другими драйверами. Однако, менеджер ресурсов не может ничего сказать об устройствах, чьи драйвера не были загружены, и о том, какое устройство уже заняло тот диапазон, который вам требуется. Если мы просмотрим карту памяти, то увидим следующее: отображение ОЗУ, отображение ПЗУ (например, VGA BIOS) и свободные области.

Вспомним код нашего драйвера scull. Так как он не имеет прямого отношения к какому-либо физическому устройству, то он просто выводит информацию об используемости диапазона адресов от 640КБ до 1МБт. Код драйвера scull показывает, как может быть выполнен анализ занятости памяти,

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

unsigned char oldval, newval; /* values read from memory   */
                              /* значения прочитанные из памяти */
			      
unsigned long flags;          /* used to hold system flags */
                              /* флаг, используемый для удержания системы */
			      
unsigned long add, i;
void *base;
    
/* Use ioremap to get a handle on our region */
/* Используйте ioremap() для получения доступа к этой области памяти */
base = ioremap(ISA_REGION_BEGIN, ISA_REGION_END - ISA_REGION_BEGIN);
base -= ISA_REGION_BEGIN;  /* Do the offset once */
                           /* Сразу выполним смещение */
    
/* probe all the memory hole in 2-KB steps */
/* проверим всю память кусками по 2КБт */
for (add = ISA_REGION_BEGIN; add < ISA_REGION_END; add += STEP) {
   /*
    * Check for an already allocated region.
    * Возможно, память уже распределена
    */
   if (check_mem_region (add, 2048)) {
          printk(KERN_INFO "%lx: Allocated\n", add);
          continue;
   }
   /*
    * Read and write the beginning of the region and see what happens.
    * Читаем и пишем в начало области, и смотрим, что получится
    */
   save_flags(flags); 
   cli();
   oldval = readb (base + add);  /* Read a byte */
   writeb (oldval^0xff, base + add);
   mb();
   newval = readb (base + add);
   writeb (oldval, base + add);
   restore_flags(flags);

   if ((oldval^newval) == 0xff) {  /* we reread our change: it's RAM */
                                   /* мы прочитали то, что записали, следовательно это ОЗУ */
       printk(KERN_INFO "%lx: RAM\n", add);
       continue;
   }
   if ((oldval^newval) != 0) {  /* random bits changed: it's empty */
                                /* биты случайно изменены, следовательно это пустая область */
       printk(KERN_INFO "%lx: empty\n", add);
       continue;
   }
        
   /*
    * Expansion ROM (executed at boot time by the BIOS)
    * has a signature where the first byte is 0x55, the second 0xaa,
    * and the third byte indicates the size of such ROM
    *
    * Расширение ПЗУ (выполняемое во время загрузки программой BIOS)
    * имеет специальную сигнатуру, в которой первый байт равен 0x55, второй - 0xaa
    * и третий байт показывает размер этого ПЗУ
    */
   if ( (oldval == 0x55) && (readb (base + add + 1) == 0xaa)) {
       int size = 512 * readb (base + add + 2);
       printk(KERN_INFO "%lx: Expansion ROM, %i bytes\n",
              add, size);
       add += (size & ~2048) - 2048; /* skip it */
                                         /* пропустим этот диапазон */
       continue;
   }
        
   /*
    * If the tests above failed, we still don't know if it is ROM or
    * empty. Since empty memory can appear as 0x00, 0xff, or the low
    * address byte, we must probe multiple bytes: if at least one of
    * them is different from these three values, then this is ROM
    * (though not boot ROM).
    *
    * Если предыдущие тесты окончились неудачей, то мы еще не можем быть уверены
    * является ли эта область пустой, или это ПЗУ. Так как пустая область читается
    * либо как 0x00, либо как 0xff, либо значением младшего байта адреса, мы должны
    * проверить несколько байт из этой области. Если хотя бы один байт отличается от
    * этих трех значений, то это ПЗУ (не загрузочное, но ПЗУ).
    */
   printk(KERN_INFO "%lx: ", add);
   for (i=0; i<5; i++) {
       unsigned long radd = add + 57*(i+1);  /* a "random" value */
       unsigned char val = readb (base + radd);
       if (val && val != 0xFF && val != ((unsigned long) radd&0xFF))
          break;
   }    
   printk("%s\n", i==5 ? "empty" : "ROM");
}

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

Пример отображения: рисуем из модуля ядра

Данная глава является полностью дополнением переводчика и отсуствует в оригинальном источнике.

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

Занимаясь построением систем слежения за небольшим количеством датчиков с максимально высокой частотой опроса состояния системы, для последующей обработки и формирования управляющих кодов, я столкнулся с невозможностью передачи управления в другие процессы, в том числе и процессы пространства пользователя, во время исполнения управляющих алгоритмов. Речь идет о некоторых типах машиностроительных станков. Процесс управления может длится несколько минут, а типичная частота управления составляет 15-20 кГц. За время одного атома управления необходимо причитать состояние со всех аналоговых и цифровых датчиков системы, выполнить математическую обработку считанных сигналов, вычислить и выдать следующее управляющее воздействие на механизмы. Фактически, для решения таких задач требуется полное внимание со стороны процессора для алгоритма управления. Поэтому управление выполняется при выключенных прерываниях, и ни о какой передаче управления в процессы пространства пользователя речи быть не может. Однако, система будет гораздо привлекательнее, если за время цикла обработки будет сообщать о текущем состоянии системы в виде каких-нибудь графиков, графических индикаторов и пр. Особенно это необходимо при настройке системы, которая выполняется в таком же жестком временном режиме.

Приведу простейший пример реализации рисования из ядра:

//файл kdraw.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/io.h>

//глобальная переменная - адрес видеобуфера в адресном пространстве ядра
char* buf;

//процедура рисования точки
void draw_point(int x, int y, unsigned char r, unsigned char g, unsigned char b)
{
	//расчет адреса точки в видеобуфере для нормальной карты в режиме 1280x1024 16 бит
	unsigned char lo,hi;
	//для нормальных видеокарт смещение реальной видеообласти от начала видеобуфера равно 0
	int offset=0; //131072 - 128кБт: таким оказалось смещение для интегрированной видеокарты
                      // одной из машин в нашей лаборатории; 
	//расчитаем адрес точки в видеобуфере: на каждую точку два байта, на каждую строку - 1280 точек
	int i = (y*1280+x)*2+offset; 
	//расчитаем байты для цвета (hi-старший, lo-младший) - двухбайтовая цветовая кодировка
	hi = (r & 0xF8) | (g >> 5);
	lo = ((g << 3) & 0xC0) | ((b >> 2) & 0x3E);
	//запишем точку в видеобуфер
	buf[i] = lo;
	buf[i+1] = hi;
}

//точка входа: вызывается при установке драйвера в ядро
int init_module(void)
{
int i,j;
	//перераспределение видеобуфера под указатель buf в пространстве ядра
	//в функцию ioremap передается:
	// 1) - физический адрес видеобуфера (шинный адрес) см. cat /proc/iomem для vesafb
	// 2) - размер видеобуфера (принципиального значения не имеет, т.к. пока вы в разрешенной области,
	// то segmentation fault не вырабатывается (разрешенная - в смысле, принадлежащая видеобуферу)
	//Т.е. вы должны посмотртеть адрес своего видеобуфера в /proc/iomem и поставить первым параметром
	//функции ioremap().
        buf = (char*)ioremap(0xd0000000,0x1000000);
	if (buf==0){
	  //это напишется в /var/log/messages если все плохо!
	  printk("<1>Bad\n");	
	} else {
	  //закрасим область 500x500 точек голубым цветом
	  for (i=0;i<500;i++) {
		  for (j=0;j<500;j++) {
			  draw_point(i,j,0,0,255);
		  }
	  };
	}
	return 0;
}

//точка входа: вызывается при выгрузке драйвера из ядра
void cleanup_module(void)
{
	//освободим указатель
        iounmap((void*)buf);  
}

//без использования этого макроса будет ругаться на отсутствие лицензии при установке в ядро
MODULE_LICENSE("GPL");

Ниже приведен пример makefile для компиляции данного модуля под ядро 2.4.x. В ключе -I определите свой путь к заголовочным файлам ядра.

CC = gcc
FLAGS = -c -Wall -D__KERNEL__ -DLINUX -DMODULE
PARAM = -I/usr/include/linux-2.4.29-std-up/include

kdraw.o: kdraw.c
	$(CC) $(FLAGS) $(PARAM) -o $@ $^


clean:
	rm kdraw.o
	rm *~

Если вы используете X-сервер в режиме 1280×1024 с двухбайтовой глубиной цвета, то при установке такого модуля в ядро часть вашего рабочего стола будет закрашено синим квадратом. На моем домашнем Celeron 1.7ГГц рисование одной точки занимает всего 60нс времени, с редким разбросом в одну наносекунду. Т.е потери таких интервалов времени не критичны для моего примера управления станками, зато процесс обработки становится более информативным.

Конечно, используя такой метод рисования, вам возможно придется задуматься о том, как заставить работать эту процедуру на разных машинах с разными адресами видеобуфера, разным разрешением и глубиной цвета. Разрешение и глубину цвета можно получить воспользовавшись запросами к фукнциям библиотеки XLib. Одним из вариантов получения информации адреса видеобуфера является парсинг файла /proc/iomem. Конечно, было бы проще получить эту информацию обращаясь к какой-нибудь функции ядра, но я, пока, такую функцию не нашел.

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

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

Вопросы обратной совместимости

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

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

Кроме того, не все функции доступа к портам (семейство inb) были поддержаны для всех архитектур в старых ядрах. Строковые функции, например, были забыты. Мы не предложили замену для этих функций в заголовочном файле sysdep.h, так как это не простая задача, реализация которой, скорее всего, не принесет выгоды.

В ядре Linux 2.0 функции ioremap() и iounmap() назывались vremap() и vfree() соответственно. Передаваемые параметры и функциональность совпадали. Таким образом, пара определений, отображающих новые функции на старых двойников, будет вполне достаточно.

К несчастью, в то время как функция vremap() работает также как и ioremap() для обеспечения доступа к верхней ("high") памяти (такой как, например, память на картах PCI), она не сможет отобразить диапазон памяти ISA. В те далекие времена, доступ к этой памяти выполнялся через разыменование прямых указателей, и не было необходимости для отображения этого адресного пространства. Таким образом, наиболее полное решение для реализации ioremap() для ядра Linux 2.0 на платформе x86 выглядит следующим образом:

 extern inline void *ioremap(unsigned long phys_addr, unsigned long size)
{
    if (phys_addr >= 0xA0000 && phys_addr + size <= 0x100000)
        return (void *)phys_addr;
    return vremap(phys_addr, size);
}

extern inline void iounmap(void *addr)
{
    if ((unsigned long)addr &qt;= 0xA0000
            && (unsigned long)addr < 0x100000)
        return;
    vfree(addr);
}

Если вы включите заголовочный файл sysdep.h в код вашего драйвера, то вы сможете использовать функцию ioremap() без проблем, даже для доступа к памяти ISA.

Распределение областей памяти (семейство функций check_mem_region) было введено в ядре 2.3.17. В ядрах 2.0 и 2.2, не было централизованного механизма распределения ресурсов памяти. Включив sysdep.h в код драйвера, вы сможете использовать эти макросы — они аннулируются при компиляции для ядер 2.0 и 2.2.

Краткий справочник определений

В данной главе были представлены следующие символы для управления вводом/выводом:

#include <linux/kernel.h>
void barrier(void)
Такой "программный" барьер памяти заставляет компилятор полагать, что вся память после этой инструкции является "volatile" (изменяемой).
#include <asm/system.h>
void rmb(void);
void wmb(void);
void mb(void);
Аппаратные барьеры памяти. Они требуют от процессора (и компилятора) устанавливать контроль всех операций чтения/записи после этой инструкции.
#include <asm/io.h>
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned doubleword, unsigned port);
Эти функции используются для чтения и записи портов ввода/вывода. Они, также, могут быть вызваны из адресного пространства пользователя, при условии, что имеются права доступа к портам (см. функции ioperm() и iopl()).
unsigned inb_p(unsigned port);
Иногда, при работе с медленной шиной ISA на платформе x86, требуется небольшая временная задержка между операциями ввода/вывода. Для этой цели вы можете использовать шесть специальных функций (двойников функций из предыдущего пункта списка), которые реализуют такую задержку. Такие "pausing functions" (функции реализующие временную задержку между атомами данных во время передачи) имеют имена, заканчивающиеся на _p.
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
Строковые функции оптимизированы для передачи данных из портов в памяти и обратно. Такая передача выполняется чтением/записью порта count раз.
#include <linux/ioport.h>
int check_region(unsigned long start, unsigned long len);
void request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);
Функции, связанные с системой распределения портов ввода/вывода. Функция check_region() возвращает 0 в случае успеха, и значение меньшее нуля в противном случае.
int check_mem_region(unsigned long start, unsigned long len);
void request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);
Функции связанные с системой распределения памяти.
#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void *virt_addr);
Функция ioremap() отображает диапазон физических адресов в виртуальное адресное пространство процессора, делая его доступным для ядра. Функция iounmap() снимает это отображение.
#include <linux/io.h>
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
memset_io(address, value, count);
memcpy_fromio(dest, source, nbytes);
memcpy_toio(dest, source, nbytes);
Эти функции используются для доступа к областям памяти ввода/вывода, как к нижней памяти ISA, так и верхней памяти PCI.