В этом посте я кратко опишу некоторые из основных понятий, которыми оперирует ядро линукса. Я буду рассказывать не про адреса регистров и номера секторов дискет и прерываний биоса, специфические для какой-то определенной машины, а про общую концепцию работы с устройствами.
Скучная теория из школьного учебника
Драйвер устройства, в общем случае, является каким-то кодом, который преобразовывает абстрактные запросы других частей системы (включить подсветку первого экрана) в аппаратно и архитектурно специфические — записать магическую последовательность байтиков по волшебному адресу.
Что происходит на практике, будем рассматривать на примере драйверов 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 для всех
Оригинальная страница: Модули, драйвера, устройства
Комментариев нет:
Отправить комментарий