Драйверы устройств в Linux
Часть 2: Пишем в классе наш первый драйвер для Linux
Оригинал: «Device Drivers, Part 2: Writing Your First Linux Driver in the Classroom»
Автор: Anil Kumar Pugalia
Дата публикации: December 1, 2010
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.
Светлана и Пагс добрались в свой класс с опозданием и увидели, что их профессор уже начал читать лекцию. Светлана робко попросила разрешения войти. Раздраженный профессор Гопи ответил: «Входите! Вы, друзья, опять сегодня опоздали, и по какой причине»?
Пагс поспешно ответил, что они обсуждали именно ту тему, которую сегодня изучают в классе — драйверы устройств в Linux. Пагс был более, чем счастлив, когда профессор сказал: «Хорошо! Тогда что-нибудь скажите о динамической загрузке в Linux. Если вы справитесь, то я прощу вас обоих!». Пагс знал, что один из способов сделать профессора счастливым, это — покритиковать Windows.
Он объяснил: «Как известно, при обычной установке драйверов в Windows для того, чтобы их активировать, необходимо перезагрузить систему. А если это, предположим, действительно неприемлемо в случае, если это нужно делать на сервере? Вот где выигрывает Linux. В Linux можно загружать и выгружать драйверы на лету, и это активно используется сразу после загрузки системы. Кроме того, драйвер мгновенно отключается после его выгрузки. Это называется динамической загрузкой и выгрузкой драйверов в Linux «.
Это впечатлило профессора. «Хорошо! Идите на свои места, но больше не опаздывайте». Профессор продолжил лекцию: «Теперь, когда вы уже знаете, что такое динамическая загрузка и выгрузка драйверов, я, прежде, чем мы перейдем к написанию нашего первого драйверов, покажу вам, как загружать и выгружать драйвера».
Динамическая загрузка драйверов
Рис.1: Предварительно собранные модули Linux
Чтобы динамически загружать и выгружать драйверы, воспользуйтесь следующими командами, которые находятся в директории /sbin и должны выполняться с привилегиями пользователя root:
Рис.2: Операции с модулями Linux
Наш первый драйвер для Linux
Сборка нашего первого драйвера
Подведем итог
Команда lsmod должна вам сообщить о том, что драйвер ofd загружен.
Как написать свой первый Linux device driver. Часть 2
В предыдущей части мы рассмотрели базовые структуры, а также написали инициализацию и удаление устройства.
В данной статье мы добавим в наш драйвер функции открытия scull_open, чтения/записи scull_read/scull_write и получим первый рабочий драйвер устройства.
Хочу выразить благодарность всем пользователям, которые прочитали, лайкнули и прокомментировали мою предыдущую статью. Отдельное спасибо за уточнения Kolyuchkin и dlinyj.
В прошлый раз поступило предложение не рассматривать подробно внутренности каждой функции, поэтому в данной статье я попытаюсь представить их в более широком смысле.
Сразу к делу!
В предыдущей статье мы не рассмотрели одну функцию, которая является частью scull_cleanup_module, а именно scull_trim. Как вы можете наблюдать в функции присутствует цикл, который просто проходится по связному списку и возвращает память ядру. Мы не будем заострять тут наше внимание. Главное впереди!
Перед рассмотрением функции sull_open, я хотел бы сделать маленькое отступление.
Многое в системе Linux может быть представлено в виде файла. Какие операции чаще совершаются с файлами — открытие, чтение, запись и закрытие. Также и с драйверами устройств, мы можем открыть, закрыть, прочитать и записать в устройство.
Функция scull_open
Функция принимает два аргумента:
Но чтобы создать видимость работы, мы выполним несколько действий:
В выше приведенном коде с помощью container_of мы получаем указатель на cdev типа struct scull_dev, используя inode->i_cdev. Полученный указатель записываем в поле private_data.
Далее, все еще проще, если файл открыт для записи — очистим его перед использованием и выведем сообщение о том, что устройство открыто.
Функция scull_read
Сейчас я постараюсь описать смысл использования функции read.
Так как наше устройство является символьным, то мы можем работать с ним как с потоком байтов. А что можно делать с потоком байтов? Правильно — читать. Значит, как понятно из названия функции, она будет читать из устройства байтики.
Когда вызывается функция чтения, ей передаются несколько аргументов, первый из них мы уже рассмотрели, теперь посмотрим на остальные.
buf — это указатель на строку, а __user говорит нам о том, что этот указатель находится в пространстве пользователя. Аргумент передает пользователь.
count — количество байтов, которые нужно прочитать. Аргумент передает пользователь.
f_pos — смещение. Аргумент передает ядро.
Т.е., когда пользователь хочет прочитать из устройства, он вызывает функцию read (не scull_read) при этом указывает буфер куда будет записана информация и количество читаемых байт.
Теперь немного подробнее рассмотрим код:
Первым делом идут проверки:
copy_to_user — копирует данные в buf (который находится в пространстве пользователя) из памяти, которую выделило ядро dptr->data[s_pos] размером count.
Если вам сейчас не понятны все эти переменные : s_pos, q_pos, item, rest — не беда, тут главное понять смысл функции read, а уже в 3 части статьи мы протестируем наш драйвер, и там уже будет понятно за что отвечает каждая из них. Но если вы хотите узнать об этом сейчас, вы всегда можете использовать printk (если вы понимаете, о чем я:)).
Функция scull_write
В виду того, что функция scull_write очень похожа на scull_read, и ее отличие от вышерассмотренной понятен из названия, я не буду расписывать эту функцию, а предлагаю вам осмыслить ее самостоятельно.
Все, мы написали полноценный бесполезный драйвер, внизу будет приведен полный код, а в следующей статье мы его протестируем.
После того как я прочитал статью, то понял, что получилось слишком много кода и лишних функций, поэтому ниже приведен максимально простой вариант реализации данного драйвера.
Опрос
Так сложились обстоятельства, что в данный момент у меня есть задачи по портированию драйверов устройств с одной версии ядра на другую. Интересно ли вам было бы прочитать как происходит этот процесс на конкретных примерах?
Если у вас уже есть такой опыт, вы можете поделиться им и написать мне, с какими вы столкнулись проблемами/ошибками при переносе драйверов устройств. А я в свою очередь постараюсь добавить ваш опыт в статью (обязательно укажу вас в ней).
Спасибо! 🙂
Пишем свой драйвер под Linux
Хочу признаться сразу, что я вас отчасти обманул, ибо драйвер, если верить википедии — это компьютерная программа, с помощью которой другая программа (обычно операционная система) получает доступ к аппаратному обеспечению некоторого устройства. А сегодня мы создадим некую заготовку для драйвера, т.к. на самом деле ни с каким железом мы работать не будем. Эту полезную функциональность вы сможете добавить сами, если пожелаете.
То, что мы сегодня создадим, корректнее будет назвать LKM (Linux Kernel Module или загрузочный модуль ядра). Стоит сказать, что драйвер – это одна из разновидностей LKM.
Писать модуль мы будем под ядра линейки 2.6. LKM для 2.6 отличается от 2.4. Я не буду останавливаться на различиях, ибо это не входит в рамки поста.
Мы создадим символьное устройство /dev/test, которое будет обрабатываться нашим модулем. Хочу сразу оговориться, что размещать символьное устройство не обязательно в каталоге /dev, просто это является частью «древнего магического ритуала».
Немного теории
Если кратко, то LKM – это объект, который содержит код для расширения возможностей уже запущенного ядра Linux. Т.е. работает он в пространстве ядра, а не пользователя. Так что не стоит экспериментировать на рабочем сервере. В случае ошибки, закравшейся в модуль, получите kernel panic. Будем считать, что я вас предупредил.
Модуль ядра должен иметь как минимум 2 функции: функцию инициализации и функцию выхода. Первая вызывается во время загрузки модуля в пространство ядра, а вторая, соответственно, при выгрузке его. Эти функции задаются с помощью макроопределений: module_init и module_exit.
Стоит сказать несколько слов о функции printk(). Основное назначение этой функции — реализация механизма регистрации событий и предупреждений. Иными словами эта функция для записи в лог ядра некой информации.
Т.к. драйвер работает в пространстве ядра, то он отграничен от адресного пространства пользователя. А нам хотелось бы иметь возможность вернуть некий результат. Для этого используется функция put_user(). Она как раз и занимается тем, что перекидывает данные из пространства ядра в пользовательское.
Хочу ещё сказать пару слов о символьных устройствах.
Между словом «disk» и датой есть два числа разделённых запятой. Первое число называют старшим номером устройства. Старший номер указывает на то, какой драйвер используется для обслуживания данного устройства. Каждый драйвер имеет свой уникальный старший номер.
Я не буду сильно углубляться в теорию, т.к. кому интересно – тот сможет сам почитать про это подробнее. Я дам ссылку в конце.
Прежде чем начать
Для компиляции модуля нам потребуются заголовки текущего ядра.
В debian/ubutnu их можно легко поставить так (к примеру для 2.6.26-2-686):
apt-get install linux-headers-2.6.26-2-686
Либо собрать пакет для вашего текущего ядра самим: fakeroot make-kpkg kernel_headers
Исходник
// Ниже мы задаём информацию о модуле, которую можно будет увидеть с помощью Modinfo
MODULE_LICENSE( «GPL» );
MODULE_AUTHOR( «Alex Petrov
» );
MODULE_DESCRIPTION( «My nice module» );
MODULE_SUPPORTED_DEVICE( «test» ); /* /dev/testdevice */
#define SUCCESS 0
#define DEVICE_NAME «test» /* Имя нашего устройства */
// Поддерживаемые нашим устройством операции
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 * );
// Прописываем обработчики операций на устройством
static struct file_operations fops =
<
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
>;
// Функция загрузки модуля. Входная точка. Можем считать что это наш main()
static int __init test_init( void )
<
printk( KERN_ALERT «TEST driver loaded!\n» );
// Регистрируем устройсво и получаем старший номер устройства
major_number = register_chrdev( 0, DEVICE_NAME, &fops );
// Сообщаем присвоенный нам старший номер устройства
printk( «Test module is loaded!\n» );
// Функция выгрузки модуля
static void __exit test_exit( void )
<
// Освобождаем устройство
unregister_chrdev( major_number, DEVICE_NAME );
printk( KERN_ALERT «Test module is unloaded!\n» );
>
// Указываем наши функции загрузки и выгрузки
module_init( test_init );
module_exit( test_exit );
static int device_open( struct inode *inode, struct file *file )
<
text_ptr = text;
static int device_release( struct inode *inode, struct file *file )
<
is_device_open—;
return SUCCESS;
>
static ssize_t device_read( struct file *filp, /* include/linux/fs.h */
char *buffer, /* buffer */
size_t length, /* buffer length */
loff_t * offset )
<
int byte_read = 0;
if ( *text_ptr == 0 )
return 0;
Сборка модуля
Ну а теперь можем написать небольшой Makefile:
И проверить его работоспособность:
Посмотрим что у нас получилось:
Теперь посмотрим информацию о только что скомпилированном модуле:
root@joker:/tmp/test# modinfo test.ko
filename: test.ko
description: My nice module
author: Alex Petrov
license: GPL
depends:
vermagic: 2.6.26-2-openvz-amd64 SMP mod_unload modversions
Ну и наконец установим модуль в ядро:
root@joker:/tmp/test# insmod test.ko
Посмотрим есть ли наш модуль с списке:
root@joker:/tmp/test# lsmod | grep test
И что попало в логи:
root@joker:/tmp/test# dmesg | tail
[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.
Наш модуль подсказываем нам что нужно сделать.
Последуем его совету:
root@joker:/tmp/test# mknod /dev/test c 249 0
Ну и наконец проверим работает ли наш модуль:
root@joker:/tmp/test# cat /dev/test
Наш модуль не поддерживает приём данных со стороны пользователя:
root@joker:/tmp/test# echo 1 > /dev/test
bash: echo: ошибка записи: Недопустимый аргумент
Посмотрим что что скажет модуль на наши действия:
root@joker:/tmp/test# dmesg | tail
[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.
[829747.462715] Sorry, this operation isn’t supported.
root@joker:/tmp/test# rmmod test
И посмотрим что он нам скажет на прощание:
root@joker:/tmp/test# dmesg | tail
[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.
[829747.462715] Sorry, this operation isn’t supported.
[829893.681197] Test module is unloaded!
Удалим файл устройства, что бы он нас не смущал:
root@joker:/tmp/test# rm /dev/test
Заключение
Дальнейшее развитие этой «заготовки» зависит только от вас. Можно превратить её в настоящий драйвер, который будет предоставлять интерфейс к вашему девайсу, либо использовать для дальнейшего изучения ядра Linux.
Только что в голову пришла совершенно безумная идея сделать sudo через файл устройства. Т.е. посылаем в /dev/test команду и она выполняется от имени root.
Литература
И под конец дам ссылку на книгу заклинаний LKMPG (Linux Kernel Module Programming Guide)
UPD2:
Поправил ошибки в исходнике.
Парсер глючит и сохраняет ‘MODULE_DEscriptION( «My nice module» );’. Естественно в module_description все буквы заглавные.
UPD3:
segoon прислал несколько поправок к посту:
1) В функции device_open() находится race condition:
static int device_open( struct inode *inode, struct file *file )
<
text_ptr = text;
Как написать свой первый Linux device driver
Здравствуйте, дорогие хабрачитатели.
Цель данной статьи — показать принцип реализации драйверов устройств в системе Linux, на примере простого символьного драйвера.
Для меня же, главной целью является подвести итог и сформировать базовые знания для написания будущих модулей ядра, а также получить опыт изложения технической литературы для публики, т.к. через полгода я буду выступать со своим дипломным проектом (да я студент).
Это моя первая статья, пожалуйста не судите строго!
Получилось слишком много букв, поэтому я принял решение разделить статью на три части:
Часть 1 — Введение, инициализация и очистка модуля ядра.
Часть 2 — Функции open, read, write и trim.
Часть 3 — Пишем Makefile и тестируем устройство.
Перед вступлением, хочу сказать, что здесь будут изложены базовые вещи, более подробная информация будет изложена во второй и последней части данной статьи.
Подготовительные работы
Спасибо Kolyuchkin за уточнения.
Символьный драйвер (Char driver) — это, драйвер, который работает с символьными устройствами.
Символьные устройства — это устройства, к которым можно обращаться как к потоку байтов.
Пример символьного устройства — /dev/ttyS0, /dev/tty1.
К вопросу про проверсию ядра:
Драйвер представляет каждое символьное устройство структурой scull_dev, а также предостовляет интерфейс cdev к ядру.
Устройство будет представлять связный список указателей, каждый из которых указывает на структуру scull_qset.
Для наглядности посмотрите на картинку.
Для регистрации устройства, нужно задать специальные номера, а именно:
MAJOR — старший номер (является уникальным в системе).
MINOR — младший номер (не является уникальным в системе).
В ядре есть механизм, который позволяет регистрировать специализированные номера вручную, но такой подход нежелателен и лучше вежливо попросить ядро динамически выделить их для нас. Пример кода будет ниже.
После того как мы определили номера для нашего устройства, мы должны установить связь между этими номерами и операциями драйвера. Это можно сделать используя структуру file_operations.
В ядре есть специальные макросы module_init/module_exit, которые указывают путь к функциям инициализации/удаления модуля. Без этих определений функции инициализации/удаления никогда не будут вызваны.
Здесь будем хранить базовую информацию об устройстве.
Последним этапом подготовительной работы будет подключение заголовочных файлов.
Краткое описание приведено ниже, но если вы хотите копнуть поглубже, то добро пожаловать на прекрасный сайт: lxr
Инициализация
Теперь давайте посмотрим на функцию инициализации устройства.
Первым делом, вызывая alloc_chrdev_region мы регистрируем диапазон символьных номеров устройств и указываем имя устройства. После вызовом MAJOR(dev) мы получаем старший номер.
Далее проверяется вернувшееся значение, если оно является кодом ошибки, то выходим из функции. Стоит отметить, что при разработке реального драйвера устройства следует всегда проверять возвращаемые значения, а также указатели на любые элементы (NULL?).
Если вернувшееся значение не является кодом ошибки, продолжаем выполнять инициализацию.
Выделяем память, делая вызов функции kmalloc и обязательно проверяем указатель на NULL.
Стоит упомянуть, что вместо вызова двух функций kmalloc и memset, можно использовать один вызов kzalloc, который выделят область памяти и инициализирует ее нулями.
Продолжаем инициализацию. Главная здесь функция — это scull_setup_cdev, о ней мы поговорим чуть ниже. MKDEV служит для хранения старший и младших номеров устройств.
Возвращаем значение или обрабатываем ошибку и удаляем устройство.
Выше были представлены структуры scull_dev и cdev, которые реализуют интерфейс между нашим устройством и ядром. Функция scull_setup_cdev выполняет инициализацию и добавление структуры в систему.
Удаление
Функция scull_cleanup_module вызывается при удалении модуля устройства из ядра.
Обратный процесс инициализации, удаляем структуры устройств, освобождаем память и удаляем выделенные ядром младшие и старшие номера.
С удовольствием выслушаю конструктивную критику и буду ждать feedback’a.
Если вы нашли ошибки или я не правильно изложил материал, пожалуйста, укажите мне на это.
Для более быстрой реакции пишите в ЛС.
Русские Блоги
Написать простой драйвер под Linux
В этой статье сначала описывается пример драйвера, который можно протестировать и запустить, а затем обсуждаются элементы шаблона драйвера в Linux и процесс выполнения от приложения к драйверу в Linux. Я считаю, что описание от мелкого до глубокого, от конкретных примеров до абстрактной теории, для начинающих легче начать с двери, управляемой Linux.
1. Простой пример драйвера
Файл драйвера hello.c
При печати с использованием printk добавление «KERN_EMERG» к параметрам может обеспечить вывод информации для печати на консоль. Поскольку printk print разделен на 8 уровней, верхний уровень выводится на консоль, а нижний уровень выводится в файл журнала.
Makefile требуется для компиляции драйвера
Для компиляции файла драйвера необходим подходящий make-файл, потому что при компиляции драйвера нужно знать файл заголовка ядра, правила компиляции и т. Д.
Тестовый верхний код приложения hellotest.c
В верхнем тестовом случае сначала откройте файл устройства, а затем запишите данные на устройство. Таким образом, будут вызваны соответствующие функции xxx_open и xxx_write в драйвере, и информация о печати драйвера может быть использована для определения, действительно ли соответствующая функция выполняется должным образом.
Во-вторых, пример теста диска
Общий метод тестирования состоит в том, чтобы скомпилировать драйвер и тестовое приложение верхнего уровня, загрузить драйвер и вызвать драйвер через приложение верхнего уровня и, наконец, выгрузить драйвер.
1. Скомпилируйте драйвер
Создайте команду, напрямую вызовите Makefile для компиляции hello.c и, наконец, сгенерируйте «hellomodule.ko».
2. Скомпилируйте приложение верхнего уровня
С помощью этой команды вы можете скомпилировать приложение hellotest верхнего уровня.
3. Загрузите драйвер
Когда insmod загружает драйвер, вызывается функция hello_init (), и распечатанная отладочная информация выглядит следующим образом.
Кроме того, вы можете увидеть загруженные модули в «/ proc / devices».
4. Создайте узел
Хотя драйвер hellomodule.ko был загружен, и загруженный модуль HelloModule также отображается в файле / proc / devices, этот модуль не может использоваться, поскольку в каталоге устройств / dev нет соответствующего файла устройства. Поэтому вам необходимо создать узел устройства.
Номер основного устройства модуля HelloModule в / proc / devices равен 231. Когда узел создается, файл устройства / dev / hellodev подключается к номеру основного устройства. Таким образом, когда приложение манипулирует файлом / dev / hellodev, оно обнаружит модуль HelloModule.
Разница между / proc / devices и / dev
5. Вызов драйвера приложения верхнего уровня
Приложение hellotest сначала открывает файл «/ dev / hellodev», а затем записывает переменную val в этот файл. В течение этого периода будут вызваны функции hello_open и hello_write в базовом драйвере. Ниже приведены результаты работы hellotest.
6. Удалите драйвер
Когда insmod удаляет драйвер, он вызывает функцию hello_exit (), и печатная информация об отладке выглядит следующим образом.
Суммируйте последовательность операций модуля:
(1) Зарегистрируйте модуль с помощью команды insmod
(2) Создайте файл устройства «xxx» в каталоге / dev с помощью команды mknod и установите соединение с модулем через основной номер устройства.
(3) Прикладной уровень управляет базовым модулем через файл устройства / dev / xxx
Три, диск шаблон
1. Заголовочные файлы
init.h определяет функции, связанные с инициализацией и выходом драйвера
kernel.h определяет часто используемые прототипы функций и определения макросов
module.h определяет функции, переменные и макросы, связанные с модулем ядра
2. Функция инициализации
Когда драйвер загружается в ядро, эта функция инициализации будет выполняться автоматически.
Функция инициализации используется для инициализации модуля, как следует из названия. Обычно используемая функция заключается в регистрации функции через register_chrdev. Ядро выделяет часть памяти (массив) для хранения набора функций символьного устройства.Функция register_chrdev заполнит содержимое hello_flops в позиции HELLO_MAJOR этого массива, которое должно зарегистрировать адрес функции функции HelloModule для набора памяти управления устройством.
Метафора изображения, по-видимому, заключается в том, что операционная система предоставляет множество вешалок для одежды, а зарегистрированное устройство похоже на повешение одежды на определенной вешалке для одежды. На одежде много карманов, как будто каждый модуль имеет множество функциональных программных интерфейсов. Очевидно, что если вы хотите использовать определенную функцию устройства, вы можете сначала найти соответствующую вешалку для одежды, затем найти соответствующий карман, вызвать соответствующую функцию и выполнить действие.
3. Выход из функции
Когда драйвер удален, функция выхода будет выполнена автоматически, выполнив некоторые понятные задачи.
При загрузке драйвера мы регистрировали соответствующие функции функции модуля в памяти устройства управления. При удалении драйвера необходимо очистить пространство памяти, занимаемое этим модулем. Таким образом, при регистрации других устройств остается больше места для выбора.
Метафора изображения заключается в том, что при выгрузке диска одежда снимается с вешалки, так что вешалка становится пустой.
4. Информация об авторских правах
Ядро Linux выпущено в соответствии с GPL, и драйвер Linux также должен предоставлять информацию об авторских правах, в противном случае система выдаст предупреждение при загрузке в ядро.
5. Функциональная функция
View Code
В-четвертых, процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня
1. Иерархическая структура системы Linux
2. Процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня
Возьмем функцию «open (» / dev / hellodev «, O_RDWR)» в качестве примера для иллюстрации.
(1) Приложение использует функцию открытия, предоставляемую библиотекой, чтобы открыть файл устройства, представляющий hellodev.
(2) Библиотека выполняет swi-инструкцию в соответствии с параметрами, переданными функцией open, что приведет к сбоям в работе процессора и попаданию в ядро.
(3) Функция обработки исключений ядра находит соответствующий драйвер в соответствии с этими параметрами.
(4) Выполните соответствующий драйвер.
(5) Верните дескриптор файла в библиотеку, а затем в приложение.
3. Характеристики исполнения водителя
В отличие от прикладных программ, драйвер никогда не запускается активно, он пассивен: он инициализируется в соответствии с требованиями прикладной программы, а чтение и запись выполняется в соответствии с требованиями прикладной программы. Драйвер загружается в ядро, просто говоря ядру: «Я здесь, я могу выполнять эти задания», и когда эти задания запускаются, это зависит от приложения.
Драйвер работает в «пространстве ядра», которое является частью «доверия» системы. Ошибки драйвера могут привести к сбою всей системы.
«Полное руководство по разработке приложений для встроенного Linux»