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

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

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

Назначение портов ввода/вывода

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

 

#include <linux/ioport.h>

struct resource *request_region(unsigned long first, unsigned long n, const char *name);

 

Эта функция сообщает ядру, что вы хотели бы использовать n портов, начиная с first. Параметр name должен быть именем вашего устройства. Возвращаемое значение не NULL, если выделение успешно. Если от request_region вы получаете обратно NULL, вы не сможете использовать желаемые порты.

 

Все назначенные порты показываются в proc/ioports. Если вы не в состоянии получить необходимый набор портов, то оно является местом для поиска, чтобы узнать, кто получил их первым.

 

Когда вы закончите работу с набором портов ввода/вывода (возможно, при выгрузке модулей), они должна быть возвращены системе:

 

void release_region(unsigned long start, unsigned long n);

 

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

 

int check_region(unsigned long first, unsigned long n);

 

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

Управление портами ввода/вывода

После того, как драйвер запросил диапазон портов ввода/вывода, необходимых ему для своей деятельности, он должен читать и/или писать в эти порты. В этом в большинстве аппаратуры имеются различия между 8-ми разрядными, 16-ти разрядными и 32-х разрядными портами. Обычно вы не можете смешивать их, как обычно это делается с системой доступа к памяти. (* Иногда порты ввода/вывода организованы как память, и вы можете (к примеру) связать две 8-ми разрядных операции записи в одну 16-ти разрядную операцию. Это относится, например, к платам видео ПК. Но, как правило, вы не можете рассчитывать на эту особенность.)

 

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

 

unsigned inb(unsigned port);

void outb(unsigned char byte, unsigned port);

Чтение и запись байтовых портов (шириной восемь бит). Для одних платформ аргумент port определяется как unsigned long и unsigned short для других. Тип, возвращаемый inb, также различен между архитектурами.

 

unsigned inw(unsigned port);

void outw(unsigned short word, unsigned port);

Эти функции осуществляют доступ к 16-ти разрядным портам (шириной в одно слово); они не доступны при компиляции для платформы S390, которая поддерживает только байтовый ввод/вывод.

 

unsigned inl(unsigned port);

void outl(unsigned longword, unsigned port);

Эти функции осуществляют доступ к 32-х разрядным портам. longword объявляется либо как unsigned long, или как unsigned int, в зависимости от платформы. Как и ввод/вывод по словам, "длинный" ввод/вывод не доступен на S390.

 

Совет

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

 

Обратите внимание, что для портов ввода/вывода нет 64-х разрядных операций. Даже на 64-х разрядных архитектурах адресное пространство портов использует (максимум) 32-х разрядные данные.

Доступ к портам ввода/вывода из пользовательского пространства

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

 

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

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

Для вызова ioperm или iopl программа должна запускаться с правами суперпользователя. (* Технически, он должен иметь  разрешение CAP_SYS_RAWIO, но на большинстве существующих систем это то же самое, как запуск с правами суперпользователя.) Либо один из её предков уже должен иметь доступ к порту, запускаясь с правами суперпользователя.

 

Если данная платформа не имеет системных вызовов ioperm и iopl, пространство пользователя всё же может получить доступ к портам ввода/вывода с помощью файла устройства /dev/port. Однако, следует отметить, что содержание файла очень зависит от платформы и вряд ли полезно где-то, кроме ПК.

 

Исходники примеров misc-progs/inp.c и misc-progs/outp.c являются минимальным инструментом для чтения и записи портов из командной строки в пространстве пользователя. Ожидается, что они установлены под разными именами (например, inb, inw и inl манипулируют байтами, пословными, или "длинными" портами в зависимости от того, какое имя вызвано пользователем). Под x86 они используют ioperm или iopl, на других платформах - /dev/port.

 

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

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

В дополнение к однократным операциям ввода и вывода некоторые процессоры реализуют специальные инструкции для передачи последовательности байтов, слов или двойных слов в и из одного порта ввода/вывода такой же размерности. Это так называемые string instructions (строковые операции) и они выполняют задачу быстрее, чем это может сделать цикл языка Си. Следующие макросы реализуют такую концепцию строчного ввода/вывода либо с помощью одной машинной команды, либо выполняя плотный цикл, если целевой процессор не имеет инструкции для выполнения строчного ввода/вывода. При компиляции для платформы 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);

Чтение или запись 16-ти разрядных значений в один 16-ти разрядный порт.

 

void insl(unsigned port, void *addr, unsigned long count);

void outsl(unsigned port, void *addr, unsigned long count);

Чтение или запись 32-х разрядных значений в один 32-х разрядный порт.

 

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

Пауза ввода/вывода

Некоторые платформы, прежде всего i386, могут иметь проблемы, когда процессор пытается передать данные слишком быстро в или из шины. Проблемы могут возникнуть, когда процессор разогнан по отношению к периферийной шине (подумайте здесь об ISA) и могут появляться, когда плата устройства является слишком медленной. Решением является вставить небольшую задержку после каждой инструкции ввода/вывода, если за ней следует другая такая же инструкция. На x86 пауза достигается выполнением инструкции outb в порт 0x80 (обычно, но не всегда неиспользуемый), или активным ожиданием. Смотрите для подробностей файл io.h в подкаталоге asm вашей платформы.

 

Если ваше устройство пропускает некоторые данные, или если вы опасаетесь, что можете пропустить некоторые, вместо обычных можно воспользоваться функциями с паузой. Функции с паузой точно такие же, как перечисленные выше, но их имена заканчиваются на _p; они называются inb_p, outb_p и так далее. Эти функции определены для большинства поддерживаемых архитектур, хотя они часто преобразуются в тот же код, как и ввод/вывод без паузы, поскольку нет необходимости для дополнительной паузы, если архитектура работает с достаточно современной периферийной шиной.

Зависимости от платформы

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

 

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

 

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

 

IA-32 (x86)

x86_64

Эта архитектура поддерживает все функции, описанные в этой главе. Номера портов имеет тип unsigned short.

 

IA-64 (Itanium)

Поддерживаются все функции; порты являются unsigned long (и отображены на память). Строковые функции являются реализованными на Си.

 

Alpha

Поддерживаются все функции и порты являются отображёнными на память. Реализация портов ввода/вывода является различной в разных платформах Alpha, в зависимости от используемого чипсета. Строковые функции реализованы на Си и определены в arch/alpha/lib/io.c. Порты являются unsigned long.

 

ARM

Порты являются отображёнными на память и поддерживаются все функции; строковые функции реализованы на Си. Порты имеют тип unsigned int.

 

Cris

Эта архитектура не поддерживает абстракции порта ввода/вывода даже в режиме эмуляции; различные операции с портами определены как вообще ничего не делающие.

 

M68k

M68k-nommu

Порты являются отображёнными на память. Поддерживаются строковые функции и типом порта является unsigned char *.

 

MIPS

MIPS64

Порт MIPS поддерживает все функции. Строковые операции реализованы жёстким ассемблерным циклом, потому что процессор не имеет строкового ввода/вывода на машинном уровне. Порты являются отображёнными в память; они являются unsigned long.

 

PA-RISC

Поддерживаются все функции; порты являются int на PCI-базирующихся системах и unsigned short на системах EISA, за исключением строковых операций, которые используют номера портов unsigned long.

 

PowerPC

PowerPC64

Поддерживаются все функции; порты имеют тип unsigned char * на 32-х разрядных системах и unsigned long на 64-х разрядных системах.

 

S390

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

 

Super-H

Порты являются unsigned int (отображены на память) и поддерживаются все функции.

 

SPARC

SPARC64

Вновь, пространство ввода/вывода является отображённым на память. Версии функций портов определены для работы с портами типа unsigned long.

 

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

 

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

 

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

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