четверг, 27 января 2011 г.

Модули, драйвера, устройства

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

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



Скучная теория из школьного учебника



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

Что происходит на практике, будем рассматривать на примере драйверов usb-устройств. Описанные ниже принципы применимы, с определенными уточнениями, и к другим типам устройств. Работу хост-контроллера шины я сейчас не рассматриваю, достаточно того, что он обнаруживает новое устройство, назначает адрес и получает о нем определенную информацию, в случае usb и в контексте этой статьи, нам досточно device class, vendor id и product id.

Терминология



  • модуль — бинарный файл (elf) с расширением .ko, содержащий какой-то бинарный код
  • устройство физическое — какая-то штука, которую можно покрутить в руках
  • устройство логическое, низкоуровневое — объект (структура типа device, usb_device, *_device) в памяти ядра, позволяющая как-то послать устройству волшебные данные, обычно содержит указатели на устройство шины, адреса и прочее
  • устройство логическое, абстрактное — объект (структура типа input_dev, led_classdev, etc) в памяти ядра, дающая доступ к какой-то функциональности используя заранее определенный интерфейс и позволяющая абстрагироваться от деталей реализации этих функций в железе
  • драйвер — объект (структура типа usb_driver, i2c_driver, итп) в памяти ядра, имеющий имя, знающий список поддерживаемых им устройств и содержащий набор магиифункций для превращения одного в другое. живет в модуле или в теле ядра


Идентификаторы устройства



Как правило, для идентификации конкретного устройства, используется последняя пара — vendor id/product id (далее vid/pid). На шине usb, кроме них еще есть device class, если он указан, это обычно означает, что устройству достаточно стандартного драйвера. Два самых ходовых примера таких устройств — usb флешки (mass storage) и устройства ввода (human interface). У флешек могут разные vid/pid, но их соответствие классу UMS однозначно указывает ядру, на использование стандартного драйвера usb-storage.

Небольшое отступление в сторону



Шина usb поддерживает автообнаружение устройств, но это не всегда справедливо для других случаев. I2C и SPI устройства зачастую явно определяются (хардкодятся) в файле описания платформы. Кроме того, для самого контроллера usb-хоста тоже нужен драйвер — в данном случае для устройства явно задается его имя и различные параметры, необходимые для работы с устройством — адреса регистров, номера прерываний итд.

static struct resource tegra_usb1_resources[] = {
        [0] = {
                .start  = TEGRA_USB_BASE,
                .end    = TEGRA_USB_BASE + TEGRA_USB_SIZE - 1,
                .flags  = IORESOURCE_MEM,
        },
        [1] = {
                .start  = INT_USB,
                .end    = INT_USB,
                .flags  = IORESOURCE_IRQ,
        },
};
struct platform_device tegra_ehci1_device = {
        .name   = "tegra-ehci",
        .id     = 0,
        .dev    = {
                .dma_mask       = &tegra_ehci_dmamask,
                .coherent_dma_mask = DMA_BIT_MASK(32),
        },
        .resource = tegra_usb1_resources,
        .num_resources = ARRAY_SIZE(tegra_usb1_resources),
};
platform_device_register(&tegra_ehci1_device);


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

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

Как ядро находит нужный драйвер



Когда у нас есть автоопределение устройств и драйвер хост-контроллера usb замечает нового пассажира, в ядре создается объект usb_device, что отражается в каталоге /sys/bus/usb/devices. На данном этапе с устройством уже можно работать (отправлять и получать данные), но еще никто не знает, как это делать, потомучто с устройством не проассоциирован драйвер. Вы втыкаете флешку и видите, что она появилась в списке usb-устройств (lsusb), но не видите соответствующего блочного устройства (/dev/sd*). Для рассматриваемого в заметке usb, на данном этапе можно пользоваться устройством прямо из юзерспейса через libusb.

Загрузка модуля


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

Если воспользоваться утилитой modinfo или посмотреть в файл /lib/modules/$(uname -r)/modules.alias, можно заметить поле alias:

description:    USBLCD Driver Version 1.05
author:         Georges Toth <g.toth@e-biz.lu>
alias:          usb:v10D2p*d*dc*dsc*dp*ic*isc*ip*
depends:        usbcore
vermagic:       2.6.36-ARCH SMP preempt mod_unload 


Если перевести на человеческий язык, данный модуль нужно загрузить, когда в ядре на шине usb нашлось любое устройство (p* — любой pid) вендора 0x10D2 (v10D2), любого класса (ic*).

Естественно, это информация берется не с потолка, а из самого файла usblcd.ko, а изначально прописывается разработчиком драйвера в исходном коде:

static const struct usb_device_id id_table[] = {
        { .idVendor = 0x10D2, .match_flags = USB_DEVICE_ID_MATCH_VENDOR, },
        { },
}
MODULE_DEVICE_TABLE (usb, id_table);


Регистрация драйвера


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

static struct usb_driver lcd_driver = {
        .name =         "usblcd",
        .probe =        lcd_probe,
        .id_table =     id_table,
};

static int __init usb_lcd_init(void)
{
        return usb_register(&lcd_driver);
 }

module_init(usb_lcd_init);


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

В зависимости от нижележащей шины, будет использоваться другой тип структуры и другая функция регистрации, но общая суть останется той же — «увидел устройство такое-то, свистни в функцию probe такую-то».

Регистрация устройства



Если же подходящий драйвер найден, вызывается его функция __probe, которой и передается информация об устройстве (у нас — структура usb_device). Функция __probe может проделать какие-то операции с устройством — посмотреть на список его ендпоинтов (в случае usb), попробовать переслать ему какие-то данные, и так далее, в зависимости от ситуации. Иногда не получается и probe возвращает ошибку, но при этом драйвер остается загруженным в памяти, на случай нахождения подходящего устройства. При возврате ошибки из __init, драйвер будет выгружен обратно.

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

Абстрактные устройства подсистемы



Кроме проверки устройства, функция __probe регистрирует… устройство, но уже не как «адрес какой-то фигни на шине», а как реализующее некий абстрактный интерфейс и принадлежащее к подсистеме ввода, блочных устройств или чего-то еще. Для подсистемы ввода, соответствющая запись появляется в каталоге /sys/class/input и в виде файла /dev/input/eventN, для блочного устройства — появится /dev/sdX, при получении прерывания из usb ендпоинта номер xx, обработчик скажет подсистеме ввода, что нажали (или отпустили) кнопку KB_POWER, а запрос N-ного сектора блочного устройства странслирует в специфическую для протокола usb mass storage команду и отправит в нужный эндпоинт.

Любители паттернов могут сказать, что «драйвер адаптирует usb-устройство к интерфейсу устройства ввода».

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

Распространенная ситуация с модемами — на одно usb устройство приходится три tty линии, одно звуковое устройство (usb audio class device со стандартным драйвером), да еще и эмуляция компакт-диска, при этом три tty-устройства обрабатываются одним драйвером usb-serial, а звук и эмуляция компакт-диска двумя другими.

При этом, подсистеме ввода все равно, на какой шине висит устройство и существует ли оно вообще (драйвер может быть прослойкой к гипервизору виртуальной машины) — если наблюдать извне, то мы просто видим еще одно «устройство ввода с пятью кнопками», точно так же, как драйверу usb-устрйоства все равно, как реализован usb-хост, висит ли он на PCI, I2C или просто вшит прямо в SoC, ему просто нужен способ передать данные в нужный ендпоинт.

При обращении к правильно реализованному устройству, происходит такая цепочка вызовов (на примере led_class): абстрактная функция led_update_brightness -> *_update_brightness конкретного драйвера, реализующий сопряжение с железом -> абстрактная функция следующего низкоуровневого драйвера (драйвера шины) -> и так далее, пока не дойдем до самого железа. При получении данных от устройства, все идет в обратную сторону.

Существует заблуждение, что драйвера пишут на асемблере — это неправда. Большинство кода драйверов устройств написано на обычном переносимом C, поэтому драйвер usb-вебкамеры прекрасно работает, как на x86, так и на мипсах и на армах, при наличии функционирующего драйвера usb-хоста, если же он написан совсем правильно, то будет работать и на big-endian машине. К сожалению, «профессиональные программисты» из различных гомопипроприетарных контор не стремятся писать код, соответствующий этим требованиям, в отличии от «красноглазых студентов».

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


Источник: Хабрахабр - Linux для всех
Оригинальная страница: Модули, драйвера, устройства

Комментариев нет:

Отправить комментарий