MyTetra Share
Делитесь знаниями!
6. Драйверы символьных устройств
Время создания: 20.09.2022 09:45
Текстовые метки: linux, ядро, модуль, программирование, язык, C, Си, пособие, документация
Раздел: Компьютер - Linux - Ядро - Пособие по программированию модулей ядра Linux
Запись: xintrea/mytetra_syncro/master/base/16636563206b4i519fbg/text.html на raw.github.com

6. Драйверы символьных устройств

▍ 6.1 Структура file_operations

Структура file_operations находится в include/linux/fs.h и содержит указатели на определённые драйвером функции, которые выполняют различные действия с устройством. Каждое поле этой структуры соответствует адресу некой функции, определённой драйвером для обработки операции запроса.

Например, каждый символьный драйвер должен определять функцию, считывающую данные с устройства. Структура file_operations содержит адрес функции модуля, которая выполняет эту операцию. Вот как это определение выглядит в ядре 5.4:

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 (*read_iter) (struct kiocb *, struct iov_iter *);

ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

int (*iopoll)(struct kiocb *kiocb, bool spin);

int (*iterate) (struct file *, struct dir_context *);

int (*iterate_shared) (struct file *, struct dir_context *);

__poll_t (*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 *);

unsigned long mmap_supported_flags;

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, loff_t, loff_t, 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 **, void **);

long (*fallocate)(struct file *file, int mode, loff_t offset,

loff_t len);

void (*show_fdinfo)(struct seq_file *m, struct file *f);

ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,

loff_t, size_t, unsigned int);

loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,

struct file *file_out, loff_t pos_out,

loff_t len, unsigned int remap_flags);

int (*fadvise)(struct file *, loff_t, loff_t, int);

} __randomize_layout;

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

Для компилятора gcc есть расширение, которое упрощает присваивание значений в этой структуре. В современных драйверах оно встречается довольно часто, так что не удивляйтесь, если его увидите. Так выглядит новый способ присваивания значений в структуре:

struct file_operations fops = {

read: device_read,

write: device_write,

open: device_open,

release: device_release

};

Однако присваивать элементам структуры значения можно и в соответствии со стандартом С99, с помощью назначенных инициализаторов. Причём такой способ определённо предпочтительнее, чем применение расширения GNU. Этот синтаксис желательно использовать в случае, когда стоит задача портировать драйвер, так как он обеспечит лучшую совместимость:

struct file_operations fops = {

.read = device_read,

.write = device_write,

.open = device_open,

.release = device_release

};

Смысл ясен, и вам нужно иметь ввиду, что любой член структуры, которому вы не присвоите значение явно, gcc инициализирует с NULL.

Экземпляр struct file_operations, содержащий указатели на функции, используемые для реализации системных вызовов read, write, open и так далее, обычно называется fops.

Начиная с Linux v3.14, операции чтения, записи и поиска гарантированно потокобезопасны за счёт использования специальной блокировки f_pos, которая превращает обновление позиции файла во взаимное исключение. Благодаря этому, можно безопасно реализовывать подобные операции без излишних блокировок.

Начиная с Linux v5.6, была введена структура proc_ops, заменившая использование структуры file_operations при регистрации обработчиков процессов.

▍ 6.2 Структура file

Каждое устройство представлено в ядре структурой file, которая определяется в include/linux/fs.h. Имейте ввиду, что file – это структура уровня ядра, которая никогда не появляется в программе пользовательского пространства. Это не то же самое, что FILE, который определяется glibc и никогда не встречается в функции пространства ядра. Кроме того, само имя структуры может сбивать с толку, так как представляет абстрактный открытый file, а не файл на диске, который представляется структурой inode.

Экземпляр структуры file обычно называется filp. Вы также увидите, что порой её называют структурой file object – пусть это не вводит вас в заблуждение.

Загляните в определение file. Большинство записей здесь, такие как struct dentry, не используются драйверами устройств, и их можно игнорировать. Причина в том, что драйверы не заполняют file непосредственно, а лишь используют содержащиеся в ней структуры, которые создаются где-то ещё.

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

Как уже говорилось, обращение к символьным устройствам происходит через файлы устройств, обычно расположенные в /dev. Тем не менее — при написании драйвера вполне допустимо поместить файл устройства в текущий рабочий каталог с тем условием, что по завершении он будет перенесён в /dev. Старший номер сообщает, какой драйвер какой файл устройства обрабатывает. Младший же номер используется только самим драйвером для определения конкретного устройства, с которым он работает.

Добавление драйвера в систему означает регистрацию его с помощью ядра. Это аналогично присваиванию ему старшего номера во время инициализации модуля и выполняется с помощью функции register_chrdev, определённой в include/linux/fs.h.

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

Здесь unsigned major int является старшим номером, который мы хотим запросить, const char *name – это имя устройства в том виде, в котором оно отобразится в /proc/devices, а struct file_operations *fops – это указатель на таблицу file_operations для вашего драйвера. Отрицательное возвращаемое значение означает, что регистрация провалилась. Заметьте, что мы не передавали в register_chrdev младший номер. Ещё раз напомню, что ядру он не важен, его использует только драйвер.

Следующий вопрос в том, как получить старший номер, не взяв случайно тот, что уже используется? Проще всего заглянуть в Documentation/admin-guide/devices.txt и выбрать свободный. Но это будет не самый удачный способ, поскольку вы никогда не сможете быть уверены, что выбранный вами номер не окажется присвоен где-то позднее. Решением будет попросить ядро присвоить динамический старший номер.

Если передать в register_chrdev старший номер 0, возвратным значением будет его динамически выделяемое значение. Недостаток такого решения в том, что не получится создать файл устройства наперёд, поскольку вы не будете знать, какой ему будет присвоен старший номер.

Выйти из ситуации можно несколькими путями:

  • номер может выводить сам драйвер, и мы будем создавать файл устройства вручную.
  • регистрируемое устройство будет иметь запись в /proc/devices, и мы сможем либо сами создать файл устройства, либо написать для этого специальный скрипт оболочки.
  • можно сделать и так, чтобы наш драйвер сам создавал файл устройства, используя функцию device_create после успешной регистрации, и device_destroy во время вызова cleanup_module.

Однако register_chrdev() будет занимать ряд младших номеров, связанных с заданным старшим. Поэтому с целью уменьшения лишних затрат при регистрации символьного устройства рекомендуется использовать интерфейс cdev.

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

int register_chrdev_region(dev_t from, unsigned count, const char *name);

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

Выбор одной из этих функций будет зависеть от того, известны ли вам старшие номера вашего устройства. Используйте register_chrdev_region, если знаете их, и alloc_chrdev_region, если хотите сделать их выделение динамическим.

Вторым этапом необходимо инициализировать для нашего символьного устройства структуру данных struct cdev и связать её с номерами устройства. Эту инициализацию можно осуществить следующей последовательностью команд:

struct cdev *my_dev = cdev_alloc();

my_cdev->ops = &my_fops;

Тем не менее в стандартном сценарии struct cdev будет встроена в вашу собственную связанную с устройством структуру. В этом случае нам для инициализации необходима cdev_init.

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

По завершении инициализации можно добавить символьное устройства в систему с помощью cdev_add.

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

Пример использования этого интерфейса можно найти в ioctl.c, описанном в разделе 9.

▍ 6.4 Отмена регистрации устройства

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

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

Тем не менее существует счётчик, который отслеживает, сколько процессов используют ваш модуль. Значение этого счётчика можно увидеть в 3 поле вывода команды cat /proc/modules или sudo lsmod. Если это не нуль, значит, rmmod провалится. Имейте в виду, что проверять счётчик в cleanup_module не нужно, так как эта проверка будет выполнена за вас системным вызовом sys_delete_module, определённым в include/linux/syscalls.h. Этот счётчик не требуется использовать непосредственно, но include/linux/module.h содержит функции, которые позволяют вам при необходимости увеличивать, уменьшать и отображать его:

  • try_module_get(THIS_MODULE): инкрементирует число активных обращений к текущему модулю;
  • module_put(THIS_MODULE): декрементирует число активных обращений к текущему модулю;
  • module_refcount(THIS_MODULE): возвращает число активных обращений к текущему модулю.

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

▍ 6.5 chardev.c

Код ниже создаёт символьный драйвер chardev. Можете сделать дамп его файла устройства.

cat /proc/devices

(Либо откройте этот файл программой), и драйвер добавит в него значение, указывающее количество раз, которое он был считан. Запись в этот файл (вроде echo "hi" > /dev/hello) мы не поддерживаем, перехватывая такие попытки и сообщая пользователю, что данная операция недопустима. Не беспокойтесь, если не видите, что мы делаем с данными, которые считываем в буфер – они просто считываются, и выводится сообщение, подтверждающее их получение.

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

Решается она обеспечением индивидуального доступа. Мы используем атомарную инструкцию сравнения с обменом (CAS) для сохранения состояний CDEV_NOT_USED и CDEV_EXCLUSIVE_OPEN, чтобы определять, открыт ли в данный момент файл какой-либо программой. CAS сравнивает содержимое области памяти с ожидаемым значением и только в случае их совпадения изменяет содержимое этой памяти на нужное значение. Подробнее о конкурентности читайте в разделе 12.

Код chardev.c

/*

* chardev.c: создаёт символьное устройство, которое сообщает, сколько

* раз происходило считывание из файла.

*/

#include <linux/cdev.h>

#include <linux/delay.h>

#include <linux/device.h>

#include <linux/fs.h>

#include <linux/init.h>

#include <linux/irq.h>

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/poll.h>

/* Prototypes – обычно помещается в файл .h */

static int device_open(struct inode *, struct file *);

static int device_release(struct inode *, struct file *);

static ssize_t device_read(struct file *, char __user *, size_t, loff_t *);

static ssize_t device_write(struct file *, const char __user *, size_t,

loff_t *);

#define SUCCESS 0

#define DEVICE_NAME "chardev" /* Имя устройства, как оно показано в /proc/devices */

#define BUF_LEN 80 /* Максимальная длина сообщения устройства. */

/* Глобальные переменные объявляются как static, поэтому являются глобальными в пределах файла. */

static int major; /* Старший номер, присвоенный драйверу устройства */

enum {

CDEV_NOT_USED = 0,

CDEV_EXCLUSIVE_OPEN = 1,

};

/* Устройство открыто? Используется для предотвращения множественных обращений к устройству. */

static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED);

static char msg[BUF_LEN]; /* msg, которое устройство будет выдавать при запросе. */

static struct class *cls;

static struct file_operations chardev_fops = {

.read = device_read,

.write = device_write,

.open = device_open,

.release = device_release,

};

static int __init chardev_init(void)

{

major = register_chrdev(0, DEVICE_NAME, &chardev_fops);

if (major < 0) {

pr_alert("Registering char device failed with %d\n", major);

return major;

}

pr_info("I was assigned major number %d.\n", major);

cls = class_create(THIS_MODULE, DEVICE_NAME);

device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);

pr_info("Device created on /dev/%s\n", DEVICE_NAME);

return SUCCESS;

}

static void __exit chardev_exit(void)

{

device_destroy(cls, MKDEV(major, 0));

class_destroy(cls);

/* Отмена регистрации устройства. */

unregister_chrdev(major, DEVICE_NAME);

}

/* Методы. */

/* Вызывается, когда процесс пытается открыть файл устройства, например

* "sudo cat /dev/chardev"

*/

static int device_open(struct inode *inode, struct file *file)

{

static int counter = 0;

if (atomic_cmpxchg(&already_open, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN))

return -EBUSY;

sprintf(msg, "I already told you %d times Hello world!\n", counter++);

try_module_get(THIS_MODULE);

return SUCCESS;

}

/* Вызывается, когда процесс закрывает файл устройства. */

static int device_release(struct inode *inode, struct file *file)

{

/* Теперь можно принимать следующий вызов. */

atomic_set(&already_open, CDEV_NOT_USED);

/* Декрементируйте число использований, иначе, открыв файл, вы уже

* не сможете извлечь модуль.

*/

module_put(THIS_MODULE);

return SUCCESS;

}

/* Вызывается, когда процесс, который уже открыл файл устройства,

* пытается из него считать.

*/

static ssize_t device_read(struct file *filp, /* см. include/linux/fs.h */

char __user *buffer, /* буфер для данных. */

size_t length, /* длина буфера. */

loff_t *offset)

{

/* Количество байт, обычно записываемых в буфер. */

int bytes_read = 0;

const char *msg_ptr = msg;

if (!*(msg_ptr + *offset)) { /* мы находимся в конце сообщения. */

*offset = 0; /* сброс смещения. */

return 0; /* обозначение конца файла. */

}

msg_ptr += *offset;

/* Помещение данных в буфер. */

while (length && *msg_ptr) {

/* Буфер находится в пользовательском сегменте данных, а не в

* сегменте ядра, поэтому присваивание "*" не сработает. Тут 133 * нужно использовать put_user, которая копирует данные из

* сегмента ядра в пользовательский сегмент.

*/

put_user(*(msg_ptr++), buffer++);

length--;

bytes_read++;

}

*offset += bytes_read;

/* Большинство функций чтения возвращают количество байт, помещённых в буфер. */

return bytes_read;

}

/* Вызывается, когда процесс производит запись в файл устройства: echo "hi" > /dev/hello */

static ssize_t device_write(struct file *filp, const char __user *buff,

size_t len, loff_t *off)

{

pr_alert("Sorry, this operation is not supported.\n");

return -EINVAL;

}

module_init(chardev_init);

module_exit(chardev_exit);

MODULE_LICENSE("GPL");

▍ 6.6 Создание модулей для нескольких версий ядра

Системные вызовы, являющиеся основным интерфейсом, который ядро раскрывает процессам, обычно среди разных версий сохраняются. Иногда могут добавляться новые системные вызовы, но старые, как правило, продолжают работать по-прежнему. Это необходимо для обратной совместимости – новая версия ядра не должна нарушать работу стандартных процессов. Файлы устройств в большинстве случаев также остаются неизменными. С другой стороны, внутренние интерфейсы ядра между версиями вполне могут меняться.

Различные версии ядра определённо имеют между собой отличия, и если вам нужна поддержка нескольких версий, то придётся писать дополнительные директивы компиляции. Делается это путём сопоставления макроса LINUX_VERSION_CODE с макросом KERNEL_VERSION. В версии a.b.c ядра значение этого макроса будет 216a + 28b + с.


 
MyTetra Share v.0.59
Яндекс индекс цитирования