Содержание


Разработка модулей ядра Linux

Часть 18. Модуль как драйвер. Теоретические аспекты

Comments

Серия контента:

Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

В предыдущем цикле (части 1-17) модули ядра Linux были подробно рассмотрены с технологической и инструментальной точек зрения. Было показано, как правильно собирать модули, какие инструменты доступны разработчику и как их использовать. В цикле, открываемом данной статьей, мы изменим точку зрения, с которой рассматриваем модули, на утилитарную: как написать модуль, чтобы он мог использоваться в качестве драйвера (главным образом, реального оборудования) в операционной системе Linux.

Модуль и драйвер

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

  • Внешний интерфейс в пространство пользователя. Под внешними интерфейсами модуля будем понимать связи, которые модуль устанавливает с «внешним пространством» Linux, видимым пользователю, и с которыми пользователь может взаимодействовать из своего программного кода или посредством консольных команд системы. Такими интерфейсами-связями служат, например: имена в файловых системах /dev, /proc или /sys, сетевые интерфейсы и сетевые протоколы.
  • Интерфейс, обеспечивающий связь с аппаратной спецификой поддерживаемого оборудования. Сюда отнесём такие механизмы, как техника обработки аппаратных прерываний, обнаружение и работа устройств на шине PCI или обслуживание USB- устройств.
  • Внутренний интерфейс к механизмам ядра - это API ядра, используя который модуль и реализует два интерфейса, указанных выше. Благодаря этому интерфейсу модуль может зарегистрировать новый драйвер устройства так, чтобы он стал известен ядру, или запросить динамическое выделение памяти для размещения буферов обмена этого устройства.

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

Регистрация механизмов взаимодействия со стороны модуля с пространством пользователя - это не программирование в привычном смысле алгоритмов и структур данных, а строго формализованное (регламентированное как по номенклатуре, так и по порядку вызова) использование предоставляемых для этих целей API ядра. Хотя это и не самый интересный этап, но он является первоочередным при проектировании любого модуля драйвера: создание связей, через которые пользователь сможет с ним взаимодействовать.

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

Давайте вспомним философию устройств, общую не только для Linux, но и для всех UNIX/POSIX систем. Каждому устройству в системе соответствует имя этого устройства в каталоге /dev. Каждое такое именованное устройство в Linux однозначно характеризуется двумя (байтовыми: 0...255) номерами: старшим номером (major), отвечающим за отдельный класс устройств, и младшим номером (minor) конкретного экземпляра устройства внутри своего класса. Например, для диска SATA:

$ ls -l /dev/sda* 
brw-rw---- 1 root disk 8, 0 Июн 16 11:03 /dev/sda 
brw-rw---- 1 root disk 8, 1 Июн 16 11:04 /dev/sda1

Как будет показано, основополагающими фундаментальными характеристиками устройства в системе являются именно major и minor номера, а не имя устройства в /dev, как это видится с точки зрения пользователя. Связать модуль с именованным устройством по характеризующей его паре номеров major/minor и означает установить ответственность модуля за операции с этим устройством. В этом качестве модуль и называют драйвером устройства. Распределение номеров устройств между конкретными типами оборудования — это жёстко регламентированная (особенно в отношении старших номеров) связь, описанная в файле Documentation/devices.txt в дереве исходных кодов ядра Linux (больше 100Kb текста, так как этот документ накапливался на протяжении многих лет). В принципе, можно использовать для своего устройства номера и из этого перечня, но при широком внедрении такого устройства не исключается конфликт с оборудованием, уже установленным на некоторых компьютерах, поэтому лучше так не делать.

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

$ ls -l /dev | grep ' 1,' 
...
crw-r-----  1 root kmem        1,   1 Июн 26 09:29 mem 
crw-rw-rw-  1 root root        1,   3 Июн 26 09:29 null 
...
crw-r-----  1 root kmem        1,   4 Июн 26 09:29 port 
brw-rw----  1 root disk        1,   0 Июн 26 09:29 ram0 
...
crw-rw-rw-  1 root root        1,   8 Июн 26 09:29 random 
crw-rw-rw-  1 root root        1,   5 Июн 26 09:29 zero

За годы существования Linux сменилось несколько парадигм присвоения номеров устройствам и их классам. С этим связано и наличие нескольких альтернативных API для связывания устройств с модулем. Самая первая парадигма (унаследованная из ядер версии 2.4, мы её рассмотрим последней) утверждает, что старший major номер присваивается классу устройств, и за все 255 minor номеров отвечает модуль этого класса и только модуль оперирует с этими номерами. При таком подходе не может быть двух классов устройств (модулей ядра), обслуживающих одинаковые major-значения. Позже (ядра 2.6 и 3.0) для модуля (и класса устройств) определили фиксированный диапазон ответственности для устройств с одним major номером, устройства с minor-номером 0...63 могли бы обслуживаться модулем xxx1.ko (и составлять отдельный класс), а устройства с minor-номером 64...127 — другим модулем xxx2.ko (и составлять совершенно другой класс). Ещё позже, когда для статических номеров устройств, определяемых в devices.txt, стало катастрофически не хватать номеров, была создана модель динамического распределения номеров, поддерживающая её файловая система sysfs (/sys) и обеспечивающий работу sysfs в пользовательском пространстве программный проект udev.

Интерфейс символьного устройства

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

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

Практически вся полезная работа модуля в интерфейсе /dev (так же, как и в интерфейсах /proc и /sys, которые будут рассмотрены позже) реализуется через таблицу файловых операций (структуру file_operations), определенную в файле <linux/fs.h> и содержащую указатели на функции драйвера, которые отвечают за выполнение всех операций с устройством. Это большая структура, и перечислены только некоторые её позиции, дающие представление о таблице, из числа тех, которые нам понадобятся при дальнейшем рассмотрении (показано содержимое структуры из ядра 2.6.37).

Листинг 1. Фрагмент структуры file_operations
   struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ...
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        ...
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        ...
   };

Если переопределить в коде своего модуля какую-то из функций таблицы, то эта функция станет обработчиком, вызываемым для обслуживания этой операции. Если же не переопределять операцию, то для большинства операций (llseek(), flush() и др.) будет использоваться обработчик по умолчанию, который может не выполнять вообще никаких действий. Такая ситуация имеет место достаточно часто, например, в отношении операций open() и release() на устройстве, но тем не менее устройства открываются и закрываются. Но для некоторых операций такой обработчик по умолчанию будет всегда возвращать код ошибки (например, mmap()), и поэтому должен быть переопределен.

Ещё одна структура, менее значимая, чем file_operations, но также широко используемая.

Листинг 2. Фрагмент структуры inode_operations
   struct inode_operations {
        int (*create) (struct inode *, struct dentry *, int, struct nameidata *);
   ...     
        int (*mkdir) (struct inode *, struct dentry *,int);
        int (*rmdir) (struct inode *,struct dentry *);
        int (*mknod) (struct inode *, struct dentry *, int, dev_t);
        int (*rename) (struct inode *, struct dentry *,
                       struct inode *, struct dentry *);
   ...
   }

Можно заметить, что структура inode_operations соответствует системным вызовам, которые работают с устройствами по их путевым именам, а структура file_operations — системным вызовам, которые оперируют с таким представлением файла устройства, как файловый дескриптор. Ещё важнее то, что с устройством всегда ассоциируется одно имя, а файловых дескрипторов может быть ассоциировано много. В результате, указатель структуры inode_operations, передаваемый в операцию (например int (*open)(struct inode*, struct file*)) будет всегда один и тот же (до выгрузки модуля), а вот указатель структуры file_operations, передаваемый в ту же операцию, будет меняться при каждом открытии устройства.

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=830370
ArticleTitle=Разработка модулей ядра Linux: Часть 18. Модуль как драйвер. Теоретические аспекты
publish-date=08142012