- Драйверы устройств в Linux
- Часть 5: Файлы символьных устройств – создание файлов и операции с ними
- Автоматическое создание файлов устройств
- Операции с файлами
- null — драйвер
- Подведем итог
- brenns10 / Makefile
- This comment has been minimized.
- cirosantilli commented Jun 15, 2017
- This comment has been minimized.
- agusalex commented Mar 25, 2019
- Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы…
- Введение в Character device. Теория
- Введение в Character device. Практика
- User interface
- Введение в sysfs. Теория
- Заключение
Драйверы устройств в Linux
Часть 5: Файлы символьных устройств – создание файлов и операции с ними
Эта статья является продолжением серии статей о драйверах устройств в Linux. В ней обсуждаются вопросы, касающиеся символьных драйверов и их реализации.
В моей предыдущей статье я упоминал, что даже при регистрации диапазона устройств , файлы устройств в директории /dev не создаются — Светлана должна была создать их вручную с помощью команды mknod . Но при дальнейшем изучении Светлана выяснила, что файлы устройств можно создавать автоматически с помощью демона udev . Она также узнала о втором шаге подключения файла устройства к драйверу устройства — связывание операций над файлом устройства с функциями драйвера устройства. Вот что она узнала.
Автоматическое создание файлов устройств
Ранее, в ядре 2.4, автоматическое создание файлов устройств выполнялось самим ядром в devfs с помощью вызова соответствующего API. Однако, по мере того, как ядро развивалось, разработчики ядра поняли, что файлы устройств больше связаны с пользовательским пространством и, следовательно, они должны быть именно там, а не в ядре. Исходя из этого принципа, теперь для рассматриваемого устройства в ядре в /sys только заполняется соответствующая информация о классе устройства и об устройстве. Затем в пользовательском пространстве эту информацию необходимо проинтерпретировать и выполнить соответствующее действие. В большинстве настольных систем Linux эту информацию собирает демон udev, и создает, соответственно, файлы устройств.
Демон udev можно с помощью его конфигурационных файлов настроить дополнительно и точно указать имена файлов устройств, права доступа к ним, их типы и т. д. Так что касается драйвера, требуется с помощью API моделей устройств Linux, объявленных в
, заполнить в /sys соответствующие записи. Все остальное делается с помощью udev . Класс устройства создается следующим образом:
Затем в этот класс информация об устройстве ( ) заносится следующим образом:
Здесь, в качестве first указывается dev_t . Соответственно, дополняющими или обратными вызовами, которые должны вызыватся в хронологически обратном порядке, являются:
Посмотрите на рис.1 на записи /sys , созданные с помощью chardrv — запись ( ) и с помощью mynull — запись ( ). Здесь также показан файл устройства, созданный с помощью udev по записи : , находящейся в файле dev .
Рис.1: Автоматическое создание файла устройства
В случае, если указаны несколько младших номеров minor, API device_create() и device_destroy() могут вызываться в цикле и в этом случае окажется полезной строка ( ). Например, вызов функции device_create() в цикле с использованием индекса i будет иметь следующий вид:
Операции с файлами
Независимо от того, что системные вызовы (или, в общем случае, операции с файлами), о которых мы рассказываем, применяются к обычным файлам, их также можно использовать и с файлами устройств. Т.е. мы можем сказать: если смотреть из пользовательского пространства, то в Linux почти все является файлами. Различие — в пространстве ядра, где виртуальная файловая система (VFS) определяет тип файла и пересылает файловые операции в соответствующий канал, например, в случае обычного файла или директория — в модуль файловой системы, или в соответствующий драйвер устройства в случае использования файла устройства. Мы будем рассматривать второй случай.
Теперь, чтобы VFS передала операции над файлом устройства в драйвер, ее следует об этом проинформировать. И это то, что называется регистрацией драйвером в VFS файловых операций. Регистрация состоит из двух этапов. (Код, указываемый в скобках, взят из кода «null -драйвера», который приведен ниже).
Во-первых, давайте занесем нужные нам файловые операции ( my_open , my_close , my_read , my_write , …) в структуру, описывающую файловые операции ( struct file_operations pugs_fops ) и ею инициализируем структуру, описывающую символьное устройство ( struct cdev c_dev ); используем для этого обращение cdev_init() .
Затем передадим эту структуру в VFS с помощью вызова cdev_add() . Обе операции cdev_init() и cdev_add() объявлены в
. Естественно, что также надо закодировать фактические операции с файлами ( my_open , my_close , my_read , my_write ).
Итак, для начала, давайте все это сделаем как можно проще — скажем, максимально просто в виде «null драйвера».
null — драйвер
Светлана повторила обычный процесс сборки, добавив при этом некоторые новые проверочные шаги, а именно:
- Собрала драйвер (файл .ko ) с помощью запуска команды make .
- Загрузила драйвер с помощью команды insmod .
- С помощью команды lsmod получила список всех загруженных модулей.
- С помощью команды cat /proc/devices . получила список используемых старших номеров major.
- Поэкспериментировала с «null драйвером» (подробности смотрите на рис.2).
- Выгрузила драйвер с помощью команды rmmod .
Рис.2: Эксперименты с «null драйвером»
Подведем итог
Светлана олпределенно была довольна; она сама написала символьный драйвер, который работает точно также, как и стандартный файл устройства /dev/null . Чтобы понять, что это значит, проверьте пару для файла /dev/null , а также выполните с ним команды echo и cat .
Но Светлану стала беспокоить одна особенность. В своем драйвере она использовала свои собственные вызовы ( my_open , my_close , my_read , my_write ), но, к удивлению, они, в отличие от любых других вызовов файловой системы, работают таким необычным образом. Что же тут необычного? Необычно, по крайней мере с точки зрения обычных файловых операций, то, что чтобы Светлана не записывала, при чтении она ничего не могла получить. Как она сможет решить эту проблему? Читайте следующую статью.
Источник
brenns10 / Makefile
/* |
* chardev.c: Creates a read-only char device that says how many times you’ve |
* read from the dev file. |
* |
* You can have some fun with this by removing the module_get/put calls, |
* allowing the module to be removed while the file is still open. |
* |
* Compile with `make`. Load with `sudo insmod chardev.ko`. Check `dmesg | tail` |
* output to see the assigned device number and command to create a device file. |
* |
* From TLDP.org’s LKMPG book. |
*/ |
# include linux/kernel.h > |
# include linux/module.h > |
# include linux/fs.h > |
# include asm/uaccess.h > /* for put_user */ |
/* |
* Prototypes — this would normally go in a .h file |
*/ |
int init_module ( void ); |
void cleanup_module ( void ); |
static int device_open ( struct inode *, struct file *); |
static int device_release ( struct inode *, struct file *); |
static ssize_t device_read ( struct file *, char *, size_t , loff_t *); |
static ssize_t device_write ( struct file *, const char *, size_t , loff_t *); |
# define SUCCESS 0 |
# define DEVICE_NAME » chardev « |
# define BUF_LEN 80 |
/* |
* Global variables are declared as static, so are global within the file. |
*/ |
static int Major; |
static int Device_Open = 0 ; |
static char msg[BUF_LEN]; |
static char *msg_Ptr; |
static struct file_operations fops = < |
. read = device_read, |
. write = device_write, |
. open = device_open, |
. release = device_release |
>; |
/* |
* This function is called when the module is loaded |
*/ |
int init_module ( void ) |
< |
Major = register_chrdev ( 0 , DEVICE_NAME, &fops); |
if (Major 0 ) < |
printk (KERN_ALERT » Registering char device failed with %d \n » , Major); |
return Major; |
> |
printk (KERN_INFO » I was assigned major number %d . To talk to \n » , Major); |
printk (KERN_INFO » the driver, create a dev file with \n » ); |
printk (KERN_INFO » ‘mknod /dev/ %s c %d 0’. \n » , DEVICE_NAME, Major); |
printk (KERN_INFO » Try various minor numbers. Try to cat and echo to \n » ); |
printk (KERN_INFO » the device file. \n » ); |
printk (KERN_INFO » Remove the device file and module when done. \n » ); |
return SUCCESS; |
> |
/* |
* This function is called when the module is unloaded |
*/ |
void cleanup_module ( void ) |
< |
/* |
* Unregister the device |
*/ |
unregister_chrdev (Major, DEVICE_NAME); |
> |
/* |
* Methods |
*/ |
/* |
* Called when a process tries to open the device file, like |
* «cat /dev/mycharfile» |
*/ |
static int device_open ( struct inode *inode, struct file *filp) |
< |
static int counter = 0 ; |
if (Device_Open) |
return -EBUSY; |
Device_Open++; |
sprintf (msg, » I already told you %d times Hello world! \n » , counter++); |
msg_Ptr = msg; |
/* |
* TODO: comment out the line below to have some fun! |
*/ |
try_module_get (THIS_MODULE); |
return SUCCESS; |
> |
/* |
* Called when a process closes the device file. |
*/ |
static int device_release ( struct inode *inode, struct file *filp) |
< |
Device_Open—; |
/* |
* Decrement the usage count, or else once you opened the file, you’ll never |
* get rid of the module. |
* |
* TODO: comment out the line below to have some fun! |
*/ |
module_put (THIS_MODULE); |
return SUCCESS; |
> |
/* |
* Called when a process, which already opened the dev file, attempts to read |
* from it. |
*/ |
static ssize_t device_read ( struct file *filp, /* see include/linux/fs.h */ |
char *buffer, /* buffer to fill with data */ |
size_t length, /* length of the buffer */ |
loff_t *offset) |
< |
/* |
* Number of bytes actually written to the buffer |
*/ |
int bytes_read = 0 ; |
/* |
* If we’re at the end of the message, return 0 signifying end of file. |
*/ |
if (*msg_Ptr == 0 ) |
return 0 ; |
/* |
* Actually put the data into the buffer |
*/ |
while (length && *msg_Ptr) < |
/* |
* The buffer is in the user data segment, not the kernel segment so «*» |
* assignment won’t work. We have to use put_user which copies data from the |
* kernel data segment to the user data segment. |
*/ |
put_user (*(msg_Ptr++), buffer++); |
length—; |
bytes_read++; |
> |
/* |
* Most read functions return the number of bytes put into the buffer |
*/ |
return bytes_read; |
> |
/* |
* Called when a process writes to dev file: echo «hi» > /dev/hello |
*/ |
static ssize_t |
device_write ( struct file *filp, const char *buf, size_t len, loff_t *off) |
< |
printk (KERN_ALERT » Sorry, this operation isn’t supported. \n » ); |
return -EINVAL; |
> |
obj-m += chardev.o |
all : |
make -C /lib/modules/ $( shell uname -r) /build M= $( PWD ) modules |
clean : |
make -C /lib/modules/ $( shell uname -r) /build M= $( PWD ) clean |
This comment has been minimized.
Copy link Quote reply
cirosantilli commented Jun 15, 2017
This comment has been minimized.
Copy link Quote reply
agusalex commented Mar 25, 2019
Fixed it by replacing it with:
#include
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Источник
Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы…
Содержание первой части:
Содержание второй части:
В предыдущих частях, мы подготовили модуль (kernel space), который уже может делать минимальную работу – некоторым пакетам давать проходить, а некоторым нет. Теперь было не плохо добавить возможность, получать данные и управлять работой модуля из обычной программы (user space). Например включать Firewall, выключать и получать статистику работы. Существует несколько способов это сделать. Мы пойдем по классическому способу.
Введение в Character device. Теория
Я советую для чтения английскую версию статьи Википедии, как более точную и объемную по этой теме чем русскую. Для нас будет достаточно понимания, что разные аппаратные устройства(hardware), работают и взаимодействуют с операционной системой, на очень низком уровне и конечно же происходит все это в kernel space. Linux/Unix системы, создали механизмы, одним из которых является character device, для того чтобы упростить взаимодействие с этими устройствами. Создавая character device, мы «просим» у ОС необходимые ресурсы и можем представить устройство в виде файла в специальной директории (как известно в linux все представлено в виде файлов и операции чтения\записи в него). При чтении из этого файла – мы можем получить данные от устройства (например пакеты которые пришли на сетевую карту), при записи в этот файл мы можем посылать данные устройству (например послать документ принтеру на печать). В нашем случае, устройства физически нет, но мы воспользуемся этими файлами для взаимодействия с нашей программой.
Введение в Character device. Практика
Создать и зарегистрировать такой драйвер очень просто, достаточно вызвать функцию
fw_major = register_chrdev(0, DEVICE_NAME, &fops); . Уже после этой функции, можно пойти в /dev/ вручную добавить устройство и начать с ним работать. Структура fops, определяет функции, которые будут вызываться при разных событиях с драйвером, например — чтением или записью. Функция возвращает major number – уникальный идентификационный номер.
В данном случае я выбрал только два события, но по ссылкам, что я приведу ниже, или самому почитав исходные тексты kernel можно найти полный список(довольно большой).
Ниже я определил, что при чтении с нашего устройства, мы получим количество всех перехваченных пакетов, а при записи, обнулим их:
Чтобы не добавлять каждый раз устройство вручную, можно сделать его регистрацию автоматической:
Уже сейчас можно видеть устройство в /dev, кроме того, после регистрации класса, оно также появляется и в /sys/class
Ниже приведен полный листинг, обратите внимание на использование goto. Обычно(=всегда), мы не используем goto в программировании, потому что это очень сильно портит понимание, содержание, читаемость кода и скорее всего говорит о проблемах в дизайне программы(спагетти-код). Но этот случай один из немногих где goto очень к месту.
User interface
Теперь напишем простую программу, которая будет читать и писать в созданный девайс для проверки работы:
И проверяем работу:
Как видно выше, сначала мы загрузили наш модуль. Потом откомпилировали программу для чтения\записи в устройство. Первый раз запустив sudo ./test all_msg, мы выполнили чтение из устройства и получили число 0. После этого мы послали 4 ping запроса на одно из сетевых устройств. Опять выполнили чтение, получили 16 пакетов (почему не 8мь? ;). Выполнили sudo ./test reset, которая обратилась на запись к устройству, которое в свою очередь все обнулило.
Так это выглядит с точки зрения драйвера:
Прежде чем мы продолжим, очень советую (но не обязательно) для углубления почитать тут. А также тут. Внизу еще есть ссылка на хорошую бесплатную книгу.
Введение в sysfs. Теория
Мы могли бы продолжить коммуникацию драйвера – пользователя через чтение\запись в /dev/fw_device, но не рекомендуется это делать, если нужно посылать\получать много информации (в отличии от байтов в нашем примере), а также данный способ считается устаревшим. И хотя в данной статье, нет больших объемов, я покажу как использовать sysfs, для коммуникации kernel user.
sysfs — виртуальная файловая система в операционной системе Linux. Экспортирует в пространство пользователя информацию ядра Linux о присутствующих в системе устройствах и драйверах. Впервые появилась в ядре версии 2.6. Необходимость создания была вызвана устаревшей системой работы ядра с устройствами.(https://ru.wikipedia.org/wiki/Sysfs)
То есть, благодаря sysfs, мы можем создавать целые структуры с иерархией из файлов, которые будут отображаться в /sys/class/fw и использовать их для чтения или записи. Например мы создадим два файла:
/sys/class/fw/acceptedMessages — чтение из которого вернет количество принятых пакетов
/sys/class/fw/dropedMessages — чтение из которого вернет количество запрещенных пакетов
Делается это очень просто. Обратите внимание, что после вызова выше:
Мы уже зарегистрировали класс и уже видели его в /sys/class. Осталось добавить два файла и определить их функции. Регистрируем файлы:
Вначале модуля, добавляем макросы DEVICE_ATTR, которые определяют чтение или запись, а также функции которые будут вызваны. Так как нам не зачем обрабатывать запись, то последнее поле NULL.
Обращение к ним через наш user interface происходит точно также как и с /dev/. Например:
Теперь самое время все собрать воедино, откомпилировать и хорошенько проверить.
Sysfs:
Accepted packets: делаем ping
И параллельно считываем
Проверяем dropped packets:
Пытаемся делать ping с host2 на host1
Параллельно смотрим «логи»
Кстати, обратите внимание, что тут, счетчик постоянно увеличивается на один (а не на два, как раньше), потому что, host1 не получает запросы от host2 и соответственно не отвечает. И для интереса dmesg:
Последнее — я выгружу fw и проверю, что без него сеть работает без ограничений:
Мы видим, что без нашего модуля, ping проходит без проблем.
Заключение
В первой части, мы сначала создали виртуальную сеть, для работы с тремя компьютерами. Потом мы рассмотрели написание простого модуля, который использовал netfilter для перехвата трафика. И в конце, добавили char device и sysfs, для представления функций модуля в файловой системе обычному пользователю через чтение\запись в файлы. В завершении написали программу для пользователя, для управления нашим устройством.
Буду очень рад любым конструктивным комментариям. В следующей части, мы существенно расширим функциональность данного модуля, сделаем его более похожим на простой firewall, а также посмотрим как он может защищать сеть от различного вида атак.
Источник