Пишем свой драйвер под 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 < 0 )
      {
        printk( "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/chardev c %d 0'.\n", major_number );

      return SUCCESS;
    }

    // Функция выгрузки модуля
    static void __exit test_exit( void )
    {
      // Освобождаем устройство
      int ret = unregister_chrdev( major_number, DEVICE_NAME );

      if ( ret < 0 )
        printk( "Error in unregister_chrdev: %d\n", ret );

      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;

      is_device_open++;

      return SUCCESS;
    }

    static int device_release( struct inode *inode, struct file *file )
    {
      is_device_open--;
      return SUCCESS;
    }

    static ssize_t

    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;

      while ( length && *text_ptr )
      {
        put_user( *( text_ptr++ ), buffer++ );
        length--;
        byte_read++;
      }

      return byte_read;
    }



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


Ну а теперь можем написать небольшой 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

test 6920 0 



И что попало в логи:

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

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)




Вас также может заинтересовать:

Microsoft выпустит Linux-драйвер ODBC для СУБД SQL Server
Компания Microsoft выпустила тестовый Linux-драйвер для MS SQL Server
В России запустят свой Skype
cupscapt - свободный драйвер для GDI-принтеров Canon
Red Hat выпустила 27-минутный фильм про свой путь в мире Open Source
Яндекс открыл свой магазин приложений для Android — Яндекс.Store