Здравствуйте, дорогие хабрачитатели.

Цель данной статьи — показать принцип реализации драйверов устройств в системе Linux, на примере простого символьного драйвера.

Для меня же, главной целью является подвести итог и сформировать базовые знания для написания будущих модулей ядра, а также получить опыт изложения технической литературы для публики, т.к. через полгода я буду выступать со своим дипломным проектом (да я студент).

Это моя первая статья, пожалуйста не судите строго!

P.S

Получилось слишком много букв, поэтому я принял решение разделить статью на три части:

Часть 1 — Введение, инициализация и очистка модуля ядра.
Часть 2 — Функции open, read, write и trim.
Часть 3 — Пишем Makefile и тестируем устройство.

Перед вступлением, хочу сказать, что здесь будут изложены базовые вещи, более подробная информация будет изложена во второй и последней части данной статьи.

Итак, начнем.

Подготовительные работы


UPD.

Спасибо Kolyuchkin за уточнения.

Символьный драйвер (Char driver) — это, драйвер, который работает с символьными устройствами.
Символьные устройства — это устройства, к которым можно обращаться как к потоку байтов.
Пример символьного устройства — /dev/ttyS0, /dev/tty1.

UPD.

К вопросу про проверсию ядра:
~$ uname -r
4.4.0-93-generic

Драйвер представляет каждое символьное устройство структурой scull_dev, а также предостовляет интерфейс cdev к ядру.

struct scull_dev {
	struct scull_qset *data;  /* Указатель на первый кусок памяти */
	int quantum;		  /* Размер одного кванта памяти */
	int qset;		  /* Количество таких квантов */
	unsigned long size;	  /* Размер используемой памяти */
	struct semaphore sem;     /* Используется семафорами */
	struct cdev cdev;	  /* Структура, представляющая символьные устройства */
};

struct scull_dev *scull_device;

Устройство будет представлять связный список указателей, каждый из которых указывает на структуру scull_qset.

struct scull_qset {
	void **data;
	struct scull_qset *next;
};

Для наглядности посмотрите на картинку.

image

Для регистрации устройства, нужно задать специальные номера, а именно:

MAJOR — старший номер (является уникальным в системе).
MINOR — младший номер (не является уникальным в системе).

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

После того как мы определили номера для нашего устройства, мы должны установить связь между этими номерами и операциями драйвера. Это можно сделать используя структуру file_operations.

struct file_operations scull_fops = {
	.owner = THIS_MODULE,
	.read = scull_read,
	.write = scull_write,
	.open = scull_open,
	.release = scull_release,
};

В ядре есть специальные макросы module_init/module_exit, которые указывают путь к функциям инициализации/удаления модуля. Без этих определений функции инициализации/удаления никогда не будут вызваны.

module_init(scull_init_module);
module_exit(scull_cleanup_module);

Здесь будем хранить базовую информацию об устройстве.

int scull_major = 0;		/* MAJOR номер*/
int scull_minor = 0;		/* MINOR номер*/
int scull_nr_devs = 1;		/* Количество регистрируемых устройств */
int scull_quantum = 4000;	/* Размер памяти в байтах */
int scull_qset = 1000;		/* Количество квантов памяти */

Последним этапом подготовительной работы будет подключение заголовочных файлов.
Краткое описание приведено ниже, но если вы хотите копнуть поглубже, то добро пожаловать на прекрасный сайт: lxr

#include <linux/module.h> /* Содержит функции и определения для динамической загрузки модулей ядра */
#include <linux/init.h>  /* Указывает на функции инициализации и очистки */
#include <linux/fs.h>    /* Содержит функции регистрации и удаления драйвера */
#include <linux/cdev.h>  /* Содержит необходимые функции для символьного драйвера */
#include <linux/slab.h>  /* Содержит функцию ядра для управления памятью */
#include <asm/uaccess.h> /* Предоставляет доступ к пространству пользователя */

Инициализация


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

static int scull_init_module(void)
{
	int rv, i;
	dev_t dev;

	rv = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");	

	if (rv) {
		printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
		return rv;
	}

        scull_major = MAJOR(dev);

	scull_device = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
	
	if (!scull_device) {
		rv = -ENOMEM;
		goto fail;
	}

	memset(scull_device, 0, scull_nr_devs * sizeof(struct scull_dev));

	for (i = 0; i < scull_nr_devs; i++) {
		scull_device[i].quantum = scull_quantum;
		scull_device[i].qset = scull_qset;
		sema_init(&scull_device[i].sem, 1);
		scull_setup_cdev(&scull_device[i], i);
	}

	dev = MKDEV(scull_major, scull_minor + scull_nr_devs);	

	return 0;

fail:
	scull_cleanup_module();
	return rv;
}

Первым делом, вызывая alloc_chrdev_region мы регистрируем диапазон символьных номеров устройств и указываем имя устройства. После вызовом MAJOR(dev) мы получаем старший номер.
Далее проверяется вернувшееся значение, если оно является кодом ошибки, то выходим из функции. Стоит отметить, что при разработке реального драйвера устройства следует всегда проверять возвращаемые значения, а также указатели на любые элементы (NULL?).

rv = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");	

if (rv) {
	printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
	return rv;
}

scull_major = MAJOR(dev);

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

Выделяем память, делая вызов функции kmalloc и обязательно проверяем указатель на NULL.

UPD

Стоит упомянуть, что вместо вызова двух функций kmalloc и memset, можно использовать один вызов kzalloc, который выделят область памяти и инициализирует ее нулями.

scull_device = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
	
if (!scull_device) {
	rv = -ENOMEM;
	goto fail;
}

memset(scull_device, 0, scull_nr_devs * sizeof(struct scull_dev));

Продолжаем инициализацию. Главная здесь функция — это scull_setup_cdev, о ней мы поговорим чуть ниже. MKDEV служит для хранения старший и младших номеров устройств.

for (i = 0; i < scull_nr_devs; i++) {
		scull_device[i].quantum = scull_quantum;
		scull_device[i].qset = scull_qset;
		sema_init(&scull_device[i].sem, 1);
		scull_setup_cdev(&scull_device[i], i);
	}

	dev = MKDEV(scull_major, scull_minor + scull_nr_devs);

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

return 0;

fail:
	scull_cleanup_module();
	return rv;
}

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

static void scull_setup_cdev(struct scull_dev *dev, int index)
{
	int err, devno = MKDEV(scull_major, scull_minor + index);

	cdev_init(&dev->cdev, &scull_fops); 

	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &scull_fops;

	err = cdev_add(&dev->cdev, devno, 1);

	if (err)
		printk(KERN_NOTICE "Error %d adding scull  %d", err, index);
}

Удаление


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

void scull_cleanup_module(void)
{
	int i;
	dev_t devno = MKDEV(scull_major, scull_minor);

	if (scull_device) {
		for (i = 0; i < scull_nr_devs; i++) {
			scull_trim(scull_device + i);
			cdev_del(&scull_device[i].cdev);	
		}
		
		kfree(scull_device);
	}

	unregister_chrdev_region(devno, scull_nr_devs); 
}

Полный код
#include <linux/module.h> 	
#include <linux/init.h> 	
#include <linux/fs.h> 		
#include <linux/cdev.h> 	
#include <linux/slab.h> 	
#include <asm/uaccess.h>

int scull_major = 0;		
int scull_minor = 0;		
int scull_nr_devs = 1;		
int scull_quantum = 4000;	
int scull_qset = 1000;	

struct scull_qset {
	void **data;			
	struct scull_qset *next; 	
};

struct scull_dev {
	struct scull_qset *data;  
	int quantum;		 
	int qset;		  
	unsigned long size;	  
	unsigned int access_key;  
	struct semaphore sem;    
	struct cdev cdev;	 
};

struct scull_dev *scull_device;

int scull_trim(struct scull_dev *dev)
{
	struct scull_qset *next, *dptr;
	int qset = dev->qset; 
	int i;

	for (dptr = dev->data; dptr; dptr = next) { 
		if (dptr->data) {
			for (i = 0; i < qset; i++)
				kfree(dptr->data[i]);

			kfree(dptr->data);
			dptr->data = NULL;
		}

		next = dptr->next;
		kfree(dptr);
	}

	dev->size = 0;
	dev->quantum = scull_quantum;
	dev->qset = scull_qset;
	dev->data = NULL;

	return 0;
}

struct file_operations scull_fops = {		
	.owner = THIS_MODULE,			
	//.read = scull_read,
	//.write = scull_write,
	//.open = scull_open,
	//.release = scull_release,
};

static void scull_setup_cdev(struct scull_dev *dev, int index)
{
	int err, devno = MKDEV(scull_major, scull_minor + index);	

	cdev_init(&dev->cdev, &scull_fops);

	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &scull_fops;

	err = cdev_add(&dev->cdev, devno, 1);

	if (err)
		printk(KERN_NOTICE "Error %d adding scull  %d", err, index);
}

void scull_cleanup_module(void)
{
	int i;
	dev_t devno = MKDEV(scull_major, scull_minor);

	if (scull_device) {
		for (i = 0; i < scull_nr_devs; i++) {
			scull_trim(scull_device + i);		
			cdev_del(&scull_device[i].cdev);	
		}
		
		kfree(scull_device);
	}

	unregister_chrdev_region(devno, scull_nr_devs); 
}

static int scull_init_module(void)
{
	int rv, i;
	dev_t dev;

		
	rv = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");	

	if (rv) {
		printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
		return rv;
	}

        scull_major = MAJOR(dev);

	scull_device = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);	
	
	if (!scull_device) {
		rv = -ENOMEM;
		goto fail;
	}

	memset(scull_device, 0, scull_nr_devs * sizeof(struct scull_dev));		

	for (i = 0; i < scull_nr_devs; i++) {						
		scull_device[i].quantum = scull_quantum;
		scull_device[i].qset = scull_qset;
		sema_init(&scull_device[i].sem, 1);
		scull_setup_cdev(&scull_device[i], i);					
	}

	dev = MKDEV(scull_major, scull_minor + scull_nr_devs);	
	
	printk(KERN_INFO "scull: major = %d minor = %d\n", scull_major, scull_minor);

	return 0;

fail:
	scull_cleanup_module();
	return rv;
}

MODULE_AUTHOR("Your name");
MODULE_LICENSE("GPL");

module_init(scull_init_module);		
module_exit(scull_cleanup_module);	


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

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

Спасибо!

Литература


  • Linux device drivers 3rd edition
  • Essential linux device drivers

Комментарии (40)


  1. Kolyuchkin
    14.09.2017 17:04
    +6

    Почти все разделы LDD (как 2, так и 3) уже переведены вроде бы… Да и на Хабре подобные посты есть, например, вот

    То, что вы разбирались с проблемой «написания драйверов под Linux» по LDD (Linux Device Drivers), видно сразу — название структур драйвера даже не удосужились поменять))) Тогда вопрос/рекомендация: «Как Вы считаете, стоит упомянуть LDD в списке литературы поста?»

    Второе, о что споткнулся мой мозг — это определение «Символьного драйвера». Оно без понимания сути и контекста просто выдернуто из аннотации к разделу про символьные драйвера из LDD. Хотя, правильное и полное определение дается еще во Введении.

    И да, в посте Вы не указали, под какое ядро Linux пишете драйвер, а это важно.


    1. Alexeynew Автор
      14.09.2017 17:19
      -1

      Добрый день.
      Да, вы правы, действительно статьи есть, но та статья, которую вы упомянули требует дополнительного аппаратного обеспечения, символьный же драйвер, не требует, по сути, ничего.
      По поводу, литературы, вы также абсолютно правы, я хотел бы привести список, когда написал бы последнюю часть этой статьи.
      Названия не менял по этой же причине, я не хотел показать, что я написал драйвер, не используя литературы, наоборот, я хочу чтобы люди, которые хотят попробовать написать драйвер имели больше информации и пояснений.
      По поводу второго вашего замечания, как бы вы дали определения символьного драйвера? (Который по сути и работает с памятью, которое выделило ядро, и единственные действия, которые осуществляет, copy_from/to_user space.)


      1. Kolyuchkin
        14.09.2017 17:35
        +3

        Самое простое и понятное определение символьного драйвера — это драйвер для символьных устройств (классификация типов устройств приведена во введении LDD). А Вы привели определение именно Вашего драйвера с учетом его функционального назначения — это не верно.

        Литературу приводят обычно в первой части цикла статей, либо в аннотации к циклу.

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

        Открою Вам секрет, LPT-порт — это символьное устройство, а значит и драйвер к нему тоже символьный)) (опять Вы «по верхам» смотрели)
        И еще одно важное замечание, не все драйвера, которые работают с памятью ядра, являются символьными — например, драйвер RAM-диска блочный. Об этом Вы еще прочитаете в других разделах LDD, чего я искренне желаю, если не потеряете интерес к этому после диплома.


      1. nckma
        15.09.2017 11:55

        Я когда писал драйвер столкнулся с тем, что кое-что из LDD 3rd Ed. уже не соответствует действительности — устарело.

        Еще могу добавить, что для себя я понял, что эксперименты с ядром Линукс удобно проводить с Raspberry PI3. Почти настоящий компьютер, стоит не дорого, если при ошибке в кернел упадет — не жалко. Легко привести в исходное состояние перезаписыванием флешки.


        1. dlinyj
          15.09.2017 14:49

          нельзя просто так взять и написать драйвер. Всё написанное выше актуально ТОЛЬКО к конкретной версии ядра! Поэтому автору следует указывать какая версия ядра. Даже если меняется последний номер может всё перестать работать!!!

          А эксперименты лучше всего проводить на виртуалке — всё сильно быстрее и удобнее. А любое оборудование легко в неё шарится.


          1. nckma
            15.09.2017 15:05

            Да, виртуалка — это тоже хороший метод.


            1. dlinyj
              15.09.2017 15:08

              В ряде случаев единственный.


              1. Kolyuchkin
                15.09.2017 15:27
                +1

                Когда я только начинал писать драйвера под Linux (толком и с Linux-ом тогда и не был знаком, но по работе пришлось заняться), на первых же тестах драйвера система валилась с порчей файловой системы (блочный драйвер я писал). В тот день мне пришлось раз пять переустанавливать Linux (МСВС 3.0 на ядре 2.4.32). На следующий день я установил ее на виртуалке и с тех пор уяснил, что при «системном творчестве» виртуалка обязательна, что под Windows, что под Linux)))


                1. dlinyj
                  15.09.2017 16:16

                  Копировать и разворачивать в десятки раз проще.


                  1. Kolyuchkin
                    15.09.2017 16:24

                    Полностью с Вами согласен. Так я в последствии и поступал.


          1. MacIn
            15.09.2017 16:49

            Даже если меняется последний номер может всё перестать работать!!!

            Почему? Интересно, отчего такая лютая несовместимость.


            1. dlinyj
              15.09.2017 16:50

              Нет стандарта кода на ядро. На пространство пользователя есть, а на ядро нет. Поэтому оно динамически развивается наращивая глюки


              1. MacIn
                15.09.2017 16:58

                Спасибо. А есть какие-нибудь конкретные примеры несовместимости?


                1. dlinyj
                  15.09.2017 17:07

                  Эм? Ну примеры книги LDD2 не будут работать на современных ядрах, например. Какие примеры вам нужны? Другие названия функций, другие вызовы и т.п.


                  1. MacIn
                    15.09.2017 17:33

                    Другие названия функций, другие вызовы и т.п.

                    А, даже настолько…
                    Мне это удивительно, потому что в основном примеры под какой-нибудь NT 3.1 и под 7-8-10 заведутся (не считая защитных механизмов).


                    1. dlinyj
                      15.09.2017 17:36

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


                1. Alexeynew Автор
                  15.09.2017 17:14

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


                  1. dlinyj
                    15.09.2017 17:30

                    Да там всё может поменяться. Может порядок аргументов в функции. Короче, гадость, если нужна поддержка многих ядер.


    1. dlinyj
      15.09.2017 14:48

      Это мою статеечку что ли привели?


      1. Kolyuchkin
        15.09.2017 15:17

        Она оказалась удачным примером по двум причинам:
        1) первая после текущей в списке поиска Хабра по запросу «linux device drivers»;
        2) удачная в плане изложения и структуры.


        1. dlinyj
          15.09.2017 15:48

          Да, с той целью и писалась. Писал около полутора месяцев.


    1. MacIn
      15.09.2017 14:58

      Делению устройств на блочные и символьные (именно в русскоязычной литературе) уже много десятков лет, странно, что требуется дополнительное описание.


      1. Kolyuchkin
        15.09.2017 15:20
        +1

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


  1. Alexeynew Автор
    14.09.2017 17:49

    А Вы привели определение именно Вашего драйвера с учетом его функционального назначения — это не верно.
    Соглашусь, был рассмотрен частный случай. Я пытался, каким то образом обобщить определение (Попытка провалилась:().
    Литературу приводят обычно в первой части цикла статей, либо в аннотации к циклу.
    Действительно, лучше сразу указать.
    Открою Вам секрет, LPT-порт — это символьное устройство, а значит и драйвер к нему тоже символьный)) (опять Вы «по верхам» смотрели)
    И еще одно важное замечание, не все драйвера, которые работают с памятью ядра, являются символьными — например, драйвер RAM-диска блочный. Об этом Вы еще прочитаете в других разделах LDD, чего я искренне желаю, если не потеряете интерес к этому после диплома.
    Да, я знаю, что LPT порт — это символьное устройство))
    Но, спасибо за замечания, попытаюсь учесть.


    1. Kolyuchkin
      14.09.2017 17:53
      +2

      Всегда рад помочь. Удачи Вам в Вашем деле.


  1. Zakyann
    14.09.2017 19:55
    -5

    Спасибо, интересно для общего развития. Никогда не писал драйвера под Линукс. Писал мелкий драйверок под винду и крупный проект под линь.


    1. Zakyann
      15.09.2017 15:21

      Больше минусов, больше!!! Особенно без коментариев ) Обожают этот ресурс )


      1. dlinyj
        15.09.2017 15:49
        +1

        Нам же интересно знать что вы не писали драйверов под линукс! Это же бесценный комментарий ;).


        1. Zakyann
          15.09.2017 16:06
          +1

          Ок, ухожу-ухожу-ухожу.


  1. mike_y_k
    15.09.2017 01:19
    +1

    Ждём завершения, тогда наверное и окончательный разбор полётов будет…
    И успехов в освоениии.
    Из замечаний пока только уже сказанное про ядро и целевую систему — вопрос для последователей стоит уточнить, иначе возможны разные конфузы…


    1. Alexeynew Автор
      15.09.2017 07:11

      Спасибо, в ближайшее время добавлю эту информацию.


  1. bolick
    15.09.2017 02:07

    Скажите, а этот замечательный драйвер прошел ревью в соответствующем списке рассылки?
    Думаю, что нет — уж больно много конструкций, которые при первой же итерации ревью обычно просят исправить.


    Например, зачем вызывать kmalloc(), а потом memset(), если можно сразу же сделать kzalloc()?
    Более того, есть еще и devm_kzalloc(), при использовании которого вообще не нужно думать об освобождении ресурса, см. https://www.kernel.org/doc/Documentation/driver-model/devres.txt.


    Уверен, что разработчики, лучше меня знакомые с затронутой подсистемой, увидят и более специфические моменты.


    Это я все к тому, что было бы здорово подавать хороший пример ("best practice", так сказать, на сегодняшний день), а не просто показывать некий код из книжки как-то и где-то работающий (в лучшем случае).


    1. Alexeynew Автор
      15.09.2017 07:18

      Да, вы правы, можно использовать обе функции, в статье же используется метод «в лоб», но упомянуть про эти них тоже стоит, добавлю в ближайшее время, спасибо.


  1. Zimy4
    15.09.2017 11:58

    Буду ждать следующую статью. Единственное что меня(IMHO) немного задело, это использование goto. Я думаю можно было обойтись и без него.


    1. dlinyj
      15.09.2017 14:51

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


    1. Alexeynew Автор
      15.09.2017 15:32

      Не стоит ужасаться использования goto в данном примере, это обычная практика обработки ошибок в различных драйверах. Посмотрите lxr — очень полезный ресурс.


      1. dlinyj
        15.09.2017 15:49

        Это сделано для скорости.


  1. OLetvin
    15.09.2017 11:58
    -1

    Весьма недурная статья! Для начинающих драйвероидов очень полезна будет. Спасибо!


    1. dlinyj
      15.09.2017 14:51

      Для начинающих почитайте LDD. Ну и я как-то делал вебинар по модулям ядра linuxю Был бы спрос — провёл бы ещё.


  1. dlinyj
    15.09.2017 14:56

    Кстати, касательно ссылок. На мой пост привели ссылку, там я постарался очень кратко разжевать о написании драйверов на примере живого устройства (иначе зачем нужен драйвер?). И механизмы отладки. Но вот просто шикардосный гайд о создании реального рабочего устройства, лучшего в русскоязычном сегменте нет. Всё остальное спекуляции по LDD.
    Часть 1
    Часть 2
    Часть 3

    Обязательно указывайте версии ядра в котором вы ведёте примеры.