Как пишут драйверы linux

Драйверы устройств в Linux

Часть 2: Пишем в классе наш первый драйвер для Linux

Оригинал: «Device Drivers, Part 2: Writing Your First Linux Driver in the Classroom»
Автор: Anil Kumar Pugalia
Дата публикации: December 1, 2010
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

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

Светлана и Пагс добрались в свой класс с опозданием и увидели, что их профессор уже начал читать лекцию. Светлана робко попросила разрешения войти. Раздраженный профессор Гопи ответил: «Входите! Вы, друзья, опять сегодня опоздали, и по какой причине»?

Пагс поспешно ответил, что они обсуждали именно ту тему, которую сегодня изучают в классе — драйверы устройств в Linux. Пагс был более, чем счастлив, когда профессор сказал: «Хорошо! Тогда что-нибудь скажите о динамической загрузке в Linux. Если вы справитесь, то я прощу вас обоих!». Пагс знал, что один из способов сделать профессора счастливым, это — покритиковать Windows.

Он объяснил: «Как известно, при обычной установке драйверов в Windows для того, чтобы их активировать, необходимо перезагрузить систему. А если это, предположим, действительно неприемлемо в случае, если это нужно делать на сервере? Вот где выигрывает Linux. В Linux можно загружать и выгружать драйверы на лету, и это активно используется сразу после загрузки системы. Кроме того, драйвер мгновенно отключается после его выгрузки. Это называется динамической загрузкой и выгрузкой драйверов в Linux «.

Это впечатлило профессора. «Хорошо! Идите на свои места, но больше не опаздывайте». Профессор продолжил лекцию: «Теперь, когда вы уже знаете, что такое динамическая загрузка и выгрузка драйверов, я, прежде, чем мы перейдем к написанию нашего первого драйверов, покажу вам, как загружать и выгружать драйвера».

Динамическая загрузка драйверов

Эти динамически загружаемые драйвера чаще всего называют модулями, которые собираются в виде отдельных модулей с расширением .ko (объект ядра). В каждой системе Linux в корне файловой системы (/) есть стандартное место для всех предварительно собранных модулей. Они организованы аналогично древовидной структуре исходных кодов ядра и находятся в директории /lib/modules/ /kernel , где результат вывода системной команды uname -r (см.рис.1).

Рис.1: Предварительно собранные модули Linux

Чтобы динамически загружать и выгружать драйверы, воспользуйтесь следующими командами, которые находятся в директории /sbin и должны выполняться с привилегиями пользователя root:

  • lsmod — список модулей, загруженных в текущий момент
  • insmod — добавление / загрузка указанного файла модуля
  • modprobe — добавление / загрузка модуля вместе со всеми его зависимостями
  • rmmod — удаление / выгрузка модуля

Давайте в качестве примера рассмотрим соответствующие драйвера файловой системы FAT. На рис.2 показан весь процесс нашего эксперимента. Файлы с модулями будут fat.ko , vfat.ko и т.д., находящиеся в директории fat (в vfat для старых версий ядра) в /lib/modules/`uname -r`/kernel/fs . Если они представлены в сжатом формате .gz , вам нужно будет распаковать их с помощью команды gunzip , прежде чем вы сможете выполнить операцию insmod .

Рис.2: Операции с модулями Linux

Модуль vfat зависит от модуля fat , так что первым должен быть загружен модуль fat.ko . Чтобы автоматически выполнить распаковку и загрузку зависимостей, воспользуйтесь командой modprobe . Обратите внимание, что когда вы пользуетесь командой modprobe , вы не должны в имени модуля указывать расширение .ko . Команда rmmod используется для выгрузки модулей.

Наш первый драйвер для Linux

Перед тем, как написать наш первый драйвер, давайте рассмотрим некоторые понятия. Драйвер никогда не работает сам по себе. Он похож на библиотеку, загружаемую из-за функций, которые будут вызваны из работающего приложения. Он написан на языке C, но в нем отсутствует функция main() . Кроме того, он будет загружаться / компоноваться с ядром, поэтому он должен компилироваться аналогично тому, как было откомпилировано ядро, и вы можете в качестве заголовочных файлов использовать только те, что есть в исходном коде ядра, а не из стандартного директория /usr/include .

Интересный факт, касающийся ядра, это то, что оно, как мы видим даже на примере нашего первого драйвера, представляет собой объектно-ориентированную реализацию на языке C. В любом драйвере есть конструктор и деструктор. Когда модуль успешно загружается в ядро, то вызывается конструктор модуля, а дескруктор модуля вызывается, когда команде rmmod удается успешно выгрузить модуль. Это в драйвере две обычные функции, разве что они называются init и exit, соответственно, и вызываются с помощью макросов module_init() и module_exit() , которые определены в заголовков ядра module.h .

С учетом вышесказанного это полный код нашего первого драйвера; назовем его ofd.c. Обратите внимание, что отсутствует заголовок stdio.h (заголовок пользовательского пространства), вместо него мы используем аналог kernel.h (заголовок пространства ядра). Функция printk() эквивалентна функции printf() . Кроме того, для обеспечения совместимости версии модуля с ядром, в которое будет загружен модуль, добавлен заголовок version.h . С помощью макроса MODULE_* заполняется информация, относящаяся к модулю, которая будет использована как «подпись» модуля.

Читайте также:  Linux поменять расширение файлов

Сборка нашего первого драйвера

Т.к. у нас есть код на языке C, настало время его скомпилировать и создать файл модуля ofd.ko . Для этого мы используем систему сборки ядра. В приведенном ниже файле Makefile происходит обращение к системе сборки ядра из исходных кодов, а файл Makefile ядра, в свою очередь, обращается к файлу Makefile нашего нового драйвера с тем, чтобы собрать драйвер.

Чтобы собрать драйвер для Linux, у вас в системе должен быть исходный код ядра (или, по крайней мере, заголовки ядра). Предполагается, что исходный код ядра будет находиться в директории /usr/src/linux . Если в вашей системе он находится в каком-нибудь другом месте, то укажите это место в переменной KERNEL_SOURCE в файле Makefile .

Когда есть код на языке C ( ofd.c ) и готов файл Makefile , то все, что нам нужно сделать для сборки нашего первого драйвера ( ofd.ko ), это вызвать команду make .

Подведем итог

Как только у нас будет файл ofd.ko , мы в роли пользователя root или с помощью команды sudo выполним обычные действия.

Команда lsmod должна вам сообщить о том, что драйвер ofd загружен.

Пока студенты экспериментировали со своим первым модулем, прозвенел звонок, сообщивший об окончании урока. Профессор Гопи подвел итог: «В настоящий момент мы не увидели ничего, кроме того, что модуль lsmod сообщил о загрузке драйвера. Куда выводит информацию команда printk ? Найдите это самостоятельно на лабораторных занятиях и познакомьте меня с своими выводами. Также учтите, что наш первый драйвер будет шаблоном для любого драйвера, который можно написать для Linux. Написание специализированных драйверов это всего лишь вопрос о том, чем будет заполнен конструктор и деструктор драйвера. Поэтому дальнейшее изучение будет представлять собой расширение данного драйвера с целью получить драйвер с конкретными функциональными возможностями».

Источник

Как написать свой первый 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 служит для хранения старший и младших номеров устройств.

Читайте также:  Trassir client 8 128 удаленное рабочее место trassir os linux

Возвращаем значение или обрабатываем ошибку и удаляем устройство.

Выше были представлены структуры scull_dev и cdev, которые реализуют интерфейс между нашим устройством и ядром. Функция scull_setup_cdev выполняет инициализацию и добавление структуры в систему.

Удаление

Функция scull_cleanup_module вызывается при удалении модуля устройства из ядра.
Обратный процесс инициализации, удаляем структуры устройств, освобождаем память и удаляем выделенные ядром младшие и старшие номера.

С удовольствием выслушаю конструктивную критику и буду ждать feedback’a.

Если вы нашли ошибки или я не правильно изложил материал, пожалуйста, укажите мне на это.
Для более быстрой реакции пишите в ЛС.

Источник

Пишем свой драйвер под 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(). Она как раз и занимается тем, что перекидывает данные из пространства ядра в пользовательское.

Хочу ещё сказать пару слов о символьных устройствах.

Выполните команду ls -l /dev/sda* . Вы увидите что-то вроде:
brw-rw—- 1 root disk 8, 0 2010-10-11 10:23 /dev/sda
brw-rw—- 1 root disk 8, 1 2010-10-11 10:23 /dev/sda1
brw-rw—- 1 root disk 8, 2 2010-10-11 10:23 /dev/sda2
brw-rw—- 1 root disk 8, 5 2010-10-11 10:23 /dev/sda5

Между словом «disk» и датой есть два числа разделённых запятой. Первое число называют старшим номером устройства. Старший номер указывает на то, какой драйвер используется для обслуживания данного устройства. Каждый драйвер имеет свой уникальный старший номер.

Файлы устройства создаются с помощью команты mknod, например: mknod /dev/test c 12 . Этой командой мы создадим устройство /dev/test и укажем для него старший номер (12).

Я не буду сильно углубляться в теорию, т.к. кому интересно – тот сможет сам почитать про это подробнее. Я дам ссылку в конце.

Прежде чем начать

Нужно знать несколько «волшебных» команд:

  • insmod – добавить модуль в ядро
  • rmmod – соответственно, удалить
  • lsmod – вывести список текущих модулей
  • modinfo – вывести информацию о модуле

Для компиляции модуля нам потребуются заголовки текущего ядра.

В debian/ubutnu их можно легко поставить так (к примеру для 2.6.26-2-686):
apt-get install linux-headers-2.6.26-2-686
Либо собрать пакет для вашего текущего ядра самим: fakeroot make-kpkg kernel_headers

Исходник

#include

  • /* Для printk() и т.д. */
    #include
  • /* Эта частичка древней магии, которая оживляет модули */
    #include
  • /* Определения макросов */
    #include
  • #include /* put_user */

  • // Ниже мы задаём информацию о модуле, которую можно будет увидеть с помощью 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, воизбежание конфликтов имен.
    static int major_number; /* Старший номер устройства нашего драйвера */
    static int is_device_open = 0; /* Используется ли девайс ? */
    static char text[ 5 ] = «test\n» ; /* Текст, который мы будет отдавать при обращении к нашему устройству */
    static char * text_ptr = text; /* Указатель на текущую позицию в тексте */

    // Прописываем обработчики операций на устройством
    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 );

    if ( major_number «Registering the character device failed with %d\n» , major_number );
    return major_number;
    >

    // Сообщаем присвоенный нам старший номер устройства
    printk( «Test module is loaded!\n» );

    printk( «Please, create a dev file with ‘mknod /dev/test c %d 0’.\n» , major_number );

    // Функция выгрузки модуля
    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;

    if ( is_device_open )
    return -EBUSY;

    static int device_release( struct inode *inode, struct file *file )
    <
    is_device_open—;
    return SUCCESS;
    >

    device_write( struct file *filp, const char *buff, size_t len, loff_t * off )
    <
    printk( «Sorry, this operation isn’t supported.\n» );
    return -EINVAL;
    >

    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;

    return byte_read;
    >

    * This source code was highlighted with Source Code Highlighter .

    Сборка модуля

    Ну а теперь можем написать небольшой Makefile:

    obj-m += test.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

    И проверить его работоспособность:

    root@joker:/tmp/test# make
    make -C /lib/modules/2.6.26-2-openvz-amd64/build M=/tmp/test modules
    make[1]: Entering directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64′
    CC [M] /tmp/1/test.o
    Building modules, stage 2.
    MODPOST 1 modules
    CC /tmp/test/test.mod.o
    LD [M] /tmp/test/test.ko
    make[1]: Leaving directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64′

    Посмотрим что у нас получилось:

    root@joker:/tmp/test# ls -la
    drwxr-xr-x 3 root root 4096 Окт 21 12:32 .
    drwxrwxrwt 12 root root 4096 Окт 21 12:33 ..
    -rw-r—r— 1 root root 219 Окт 21 12:30 demo.sh
    -rw-r—r— 1 root root 161 Окт 21 12:30 Makefile
    -rw-r—r— 1 root root 22 Окт 21 12:32 modules.order
    -rw-r—r— 1 root root 0 Окт 21 12:32 Module.symvers
    -rw-r—r— 1 root root 2940 Окт 21 12:30 test.c
    -rw-r—r— 1 root root 10364 Окт 21 12:32 test.ko
    -rw-r—r— 1 root root 104 Окт 21 12:32 .test.ko.cmd
    -rw-r—r— 1 root root 717 Окт 21 12:32 test.mod.c
    -rw-r—r— 1 root root 6832 Окт 21 12:32 test.mod.o
    -rw-r—r— 1 root root 12867 Окт 21 12:32 .test.mod.o.cmd
    -rw-r—r— 1 root root 4424 Окт 21 12:32 test.o
    -rw-r—r— 1 root root 14361 Окт 21 12:32 .test.o.cmd
    drwxr-xr-x 2 root root 4096 Окт 21 12:32 .tmp_versions

    Теперь посмотрим информацию о только что скомпилированном модуле:

    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)

    UPD:
    У некоторых может не собраться модуль через Makefile, описанный выше.
    Решение:
    Создаём Makefile только с одной строкой: obj-m += test.o
    И запускаем сборку так:
    make -C /usr/src/linux-headers-`uname -r` SUBDIRS=$PWD modules

    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;

    Источник

    Оцените статью