Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Внешние интерфейсы модуля Вперед

Драйверы: интерфейс устройства

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

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

Но прежде, чем перейти к созданию интерфейса устройства, очень коротко вспомним философию устройств, общую не только для 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 
	brw-rw---- 1 root disk 8, 2 Июн 16 11:03 /dev/sda2 
	brw-rw---- 1 root disk 8, 3 Июн 16 11:03 /dev/sda3 

Здесь 8 — это старший номер для любого из дисков SATA в системе, а 2 — это младший номер для 2-го (sda2) раздела 1-го (sda) диска SATA. Связать модуль с именованным устройством и означает установить ответственность модуля за операции с устройством, характеризующимся парой major/minor. В таком качестве модуль называют драйвером устройства. Связь номеров устройств с конкретными типами оборудования — жёстко регламентирована (особенно в отношении старших номеров), и определяется содержимым файла в исходных кодах ядра: Documentation/devices.txt (больше 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 
	brw-rw----  1 root disk        1,  1 Июн 26 09:29 ram1 
	brw-rw----  1 root disk        1, 10 Июн 26 09:29 ram10 
	...
	brw-rw----  1 root disk        1, 15 Июн 26 09:29 ram15 
	brw-rw----  1 root disk        1,  2 Июн 26 09:29 ram2 
	brw-rw----  1 root disk        1,  3 Июн 26 09:29 ram3 
	...
	crw-rw-rw-  1 root root        1,  8 Июн 26 09:29 random 
	crw-rw-rw-  1 root root        1,  9 Июн 26 09:29 urandom 
	crw-rw-rw-  1 root root        1,  5 Июн 26 09:29 zero 

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

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

	   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 *);
	        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	        int (*readdir) (struct file *, void *, filldir_t);
	        unsigned int (*poll) (struct file *, struct poll_table_struct *);
	        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	        int (*mmap) (struct file *, struct vm_area_struct *);
	        int (*open) (struct inode *, struct file *);
	        int (*flush) (struct file *, fl_owner_t id);
	        int (*release) (struct inode *, struct file *);
	        int (*fsync) (struct file *, int datasync);
	        int (*aio_fsync) (struct kiocb *, int datasync);
	        int (*fasync) (int, struct file *, int);
	        int (*lock) (struct file *, int, struct file_lock *);
	        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
	                       unsigned long, unsigned long);
	        int (*check_flags)(int);
	        int (*flock) (struct file *, int, struct file_lock *);
	        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
	                 loff_t *, size_t, unsigned int);
	        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
	                size_t, unsigned int);
	        int (*setlease)(struct file *, long, struct file_lock **);
	   };

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

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

	   struct inode_operations {
	        int (*create) (struct inode *, struct dentry *, int, struct nameidata *);
	        struct dentry * (*lookup) (struct inode *, struct dentry *, struct nameidata *);
	        int (*link) (struct dentry *, struct inode *, struct dentry *);
	        int (*unlink) (struct inode *, struct dentry *);
	        int (*symlink) (struct inode *, struct dentry *, const char *);
	        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, в разное время и для разных целей, было создано несколько альтернативных (во многом замещающих друг друга) техник написания кода. Мы рассмотрим далее некоторые из них:

  1. Новый способ, использующий структуру struct cdev (<linux/cdev.h>), позволяющий динамически выделять старший номер из числа свободных, и увязывать с ним ограниченный диапазон младших номеров.
  2. Способ полностью динамического создания именованных устройств, так называемая техника misc (misccellaneous) drivers.
  3. Старый способ (использующий register_chrdev()), статически связывающий модуль со старшим номером, тем самым отдавая под контроль модуля весь диапазон допустимых младших номеров; название способа как старый не отменяет его актуальность и на сегодня.

Предыдущий раздел: Оглавление Следующий раздел:
Внешние интерфейсы модуля   Драйверы: примеры реализации