Как написать свой первый Linux device driver. Часть 2

    Привет хаброчитателям!

    В предыдущей части мы рассмотрели базовые структуры, а также написали инициализацию и удаление устройства.

    В данной статье мы добавим в наш драйвер функции открытия scull_open, чтения/записи scull_read/scull_write и получим первый рабочий драйвер устройства.



    Хочу выразить благодарность всем пользователям, которые прочитали, лайкнули и прокомментировали мою предыдущую статью. Отдельное спасибо за уточнения Kolyuchkin и dlinyj.



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

    Сразу к делу!

    В предыдущей статье мы не рассмотрели одну функцию, которая является частью scull_cleanup_module, а именно scull_trim. Как вы можете наблюдать в функции присутствует цикл, который просто проходится по связному списку и возвращает память ядру. Мы не будем заострять тут наше внимание. Главное впереди!

    scull_trim
    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;
    }
    


    Перед рассмотрением функции sull_open, я хотел бы сделать маленькое отступление.

    Многое в системе Linux может быть представлено в виде файла. Какие операции чаще совершаются с файлами — открытие, чтение, запись и закрытие. Также и с драйверами устройств, мы можем открыть, закрыть, прочитать и записать в устройство.

    Поэтому в структуре file_operations, мы видим такие поля как: .read, .write, .open и .release — это базовые операции, которые может выполнять драйвер.

    Функция scull_open


    И сразу код:

    int scull_open(struct inode *inode, struct file *flip)
    {
    	struct scull_dev *dev;
    
    	dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    	flip->private_data = dev;
    
    	if ((flip->f_flags & O_ACCMODE) == O_WRONLY) {
    		if (down_interruptible(&dev->sem))
    			return -ERESTARTSYS;
    
    		scull_trim(dev);
    		up(&dev->sem);
    	}
    	
    	printk(KERN_INFO "scull: device is opened\n");
    
    	return 0;
    }
    

    Функция принимает два аргумента:

    1. Указатель на структуру inode. Структура inode — это индексный дескриптор, который хранит информацию о файлах, каталогах и объектах файловой системы.
    2. Указатель на структуру file. Структура, которая создается ядром при каждом открытии файла, содержит информацию, необходимую верхним уровням ядра.

    Главной функцией scull_open является инициализация устройства (если устройство открыто первый раз) и заполнение необходимых полей структур для его корректной работы.
    Так как наше устройство ничего не делает, то нам и нечего инициализировать:)

    Но чтобы создать видимость работы, мы выполним несколько действий:

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    flip->private_data = dev;

    В выше приведенном коде с помощью container_of мы получаем указатель на cdev типа struct scull_dev, используя inode->i_cdev. Полученный указатель записываем в поле private_data.

    if ((flip->f_flags & O_ACCMODE) == O_WRONLY) {
    ...
    

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

    Функция scull_read


    Код функции
    ssize_t scull_read(struct file *flip, char __user *buf, size_t count,
    			loff_t *f_pos)
    {
    	struct scull_dev *dev = flip->private_data;
    	struct scull_qset *dptr;
    	int quantum = dev->quantum, qset = dev->qset;
    	int itemsize = quantum * qset;
    	int item, s_pos, q_pos, rest;
    	ssize_t rv = 0;
    
    	if (down_interruptible(&dev->sem))
    		return -ERESTARTSYS;
    
    	if (*f_pos >= dev->size) {	
    		printk(KERN_INFO "scull: *f_pos more than size %lu\n", dev->size);
    		goto out;
    	}
    
    	if (*f_pos + count > dev->size) {
    		printk(KERN_INFO "scull: correct count\n");	
    		count = dev->size - *f_pos;
    	}
    
    	item = (long)*f_pos / itemsize;	
    	rest = (long)*f_pos % itemsize;	
    
    	s_pos = rest / quantum;		
    	q_pos = rest % quantum;		
    
    	dptr = scull_follow(dev, item);	
    
    	if (dptr == NULL || !dptr->data || !dptr->data[s_pos])
    		goto out;
    
    	if (count > quantum - q_pos)
    		count = quantum - q_pos;
    
    	if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
    		rv = -EFAULT;
    		goto out;
    	}
    
    	*f_pos += count;
    	
    	rv = count;
    
    out:
    	up(&dev->sem);
    	return rv;
    }
    


    Сейчас я постараюсь описать смысл использования функции read.
    Так как наше устройство является символьным, то мы можем работать с ним как с потоком байтов. А что можно делать с потоком байтов? Правильно — читать. Значит, как понятно из названия функции, она будет читать из устройства байтики.

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

    buf — это указатель на строку, а __user говорит нам о том, что этот указатель находится в пространстве пользователя. Аргумент передает пользователь.
    count — количество байтов, которые нужно прочитать. Аргумент передает пользователь.
    f_pos — смещение. Аргумент передает ядро.

    Т.е., когда пользователь хочет прочитать из устройства, он вызывает функцию read (не scull_read) при этом указывает буфер куда будет записана информация и количество читаемых байт.
    Теперь немного подробнее рассмотрим код:

    if (*f_pos >= dev->size) {	
    	printk(KERN_INFO "scull: *f_pos more than size %lu\n", dev->size);
    	goto out;
    }
    
    if (*f_pos + count > dev->size) {
    	printk(KERN_INFO "scull: correct count\n");	
    	count = dev->size - *f_pos;
    }
    

    Первым делом идут проверки:

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

    А вот и предмет разговора:

    if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
    	rv = -EFAULT;
    	goto out;
    }
    

    copy_to_user — копирует данные в buf (который находится в пространстве пользователя) из памяти, которую выделило ядро dptr->data[s_pos] размером count.

    Если вам сейчас не понятны все эти переменные : s_pos, q_pos, item, rest — не беда, тут главное понять смысл функции read, а уже в 3 части статьи мы протестируем наш драйвер, и там уже будет понятно за что отвечает каждая из них. Но если вы хотите узнать об этом сейчас, вы всегда можете использовать printk (если вы понимаете, о чем я:)).

    Функция scull_write


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

    Код функции
    ssize_t scull_write(struct file *flip, const char __user *buf, size_t count,
    					loff_t *f_pos)
    {
    	struct scull_dev *dev = flip->private_data;
    	struct scull_qset *dptr;
    	int quantum = dev->quantum, qset = dev->qset;
    	int itemsize = quantum * qset;
    	int item, s_pos, q_pos, rest;
    	ssize_t rv = -ENOMEM;
    
    	if (down_interruptible(&dev->sem))
    		return -ERESTARTSYS;
    
    	item = (long)*f_pos / itemsize;
    	rest = (long)*f_pos % itemsize;
    	
    	s_pos = rest / quantum;
    	q_pos = rest % quantum;
    
    	dptr = scull_follow(dev, item);
    
    	if (dptr == NULL)
    		goto out;
    
    	if (!dptr->data) {
    		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
    
    		if (!dptr->data)
    			goto out;
    		
    		memset(dptr->data, 0, qset * sizeof(char *));	
    	}	
    
    	if (!dptr->data[s_pos]) {
    		dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
    		
    		if (!dptr->data[s_pos])
    			goto out;
    	}
    
    
    	if (count > quantum - q_pos)
    		count = quantum - q_pos;
    
    	if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count)) {
    		rv = -EFAULT;
    		goto out;
    	}
    
    	*f_pos += count;
    	rv = count;
    
    	if (dev->size < *f_pos)
    		dev->size = *f_pos;
    
    out:
    	up(&dev->sem);
    	return rv;
    }
    


    Все, мы написали полноценный бесполезный драйвер, внизу будет приведен полный код, а в следующей статье мы его протестируем.

    Полный код
    #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;
    }
    
    int scull_open(struct inode *inode, struct file *flip)
    {
    	struct scull_dev *dev;
    
    	dev = container_of(inode->i_cdev, struct scull_dev, cdev); 
    	flip->private_data = dev;
    
    	if ((flip->f_flags & O_ACCMODE) == O_WRONLY) { 
    		if (down_interruptible(&dev->sem))
    			return -ERESTARTSYS;
    
    		scull_trim(dev);
    		up(&dev->sem);
    	}
    	
    	printk(KERN_INFO "scull: device is opend\n");
    
    	return 0;
    }
    
    int scull_release(struct inode *inode, struct file *flip)
    {
    	return 0;
    }
    
    struct scull_qset *scull_follow(struct scull_dev *dev, int n)
    {
    	struct scull_qset *qs = dev->data;
    
    	if (!qs) {
    		qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
    		
    		if (qs == NULL)
    			return NULL;
    		
    		memset(qs, 0, sizeof(struct scull_qset));
    	}
    
    	while (n--) {
    		if (!qs->next) {
    			qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
    			
    			if (qs->next == NULL)
    				return NULL;
    
    			memset(qs->next, 0, sizeof(struct scull_qset));	
    		}
    		
    		qs = qs->next;
    		continue;	
    	}
    	
    	return qs;
    }
    
    ssize_t scull_read(struct file *flip, char __user *buf, size_t count,
    				loff_t *f_pos)
    {
    	struct scull_dev *dev = flip->private_data;
    	struct scull_qset *dptr;
    	int quantum = dev->quantum, qset = dev->qset;
    	int itemsize = quantum * qset;
    	int item, s_pos, q_pos, rest;
    	ssize_t rv = 0;
    
    	if (down_interruptible(&dev->sem))	
    		return -ERESTARTSYS;
    
    	if (*f_pos >= dev->size) {		
    		printk(KERN_INFO "scull: *f_pos more than size %lu\n", dev->size);
    		goto out;
    	}
    
    	if (*f_pos + count > dev->size) {	
    		printk(KERN_INFO "scull: correct count\n");	
    		count = dev->size - *f_pos;
    	}
    
    	item = (long)*f_pos / itemsize;		
    	rest = (long)*f_pos % itemsize;	
    
    	s_pos = rest / quantum;			
    	q_pos = rest % quantum;			
    
    	dptr = scull_follow(dev, item);	
    
    	if (dptr == NULL || !dptr->data || !dptr->data[s_pos])
    		goto out;
    
    	if (count > quantum - q_pos)
    		count = quantum - q_pos;
    
    	if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
    		rv = -EFAULT;
    		goto out;
    	}
    
    	
    	*f_pos += count;		
    	rv = count;
    
    out:
    	up(&dev->sem);
    	return rv;
    }
    
    ssize_t scull_write(struct file *flip, const char __user *buf, size_t count,
    					loff_t *f_pos)
    {
    	struct scull_dev *dev = flip->private_data;
    	struct scull_qset *dptr;
    	int quantum = dev->quantum, qset = dev->qset;
    	int itemsize = quantum * qset;
    	int item, s_pos, q_pos, rest;
    	ssize_t rv = -ENOMEM;
    
    	if (down_interruptible(&dev->sem))
    		return -ERESTARTSYS;
    
    	item = (long)*f_pos / itemsize;
    	rest = (long)*f_pos % itemsize;
    	
    	s_pos = rest / quantum;
    	q_pos = rest % quantum;
    
    	dptr = scull_follow(dev, item);
    
    	if (dptr == NULL)
    		goto out;
    
    	if (!dptr->data) {
    		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
    
    		if (!dptr->data)
    			goto out;
    		
    		memset(dptr->data, 0, qset * sizeof(char *));	
    	}	
    
    	if (!dptr->data[s_pos]) {
    		dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
    		
    		if (!dptr->data[s_pos])
    			goto out;
    	}
    
    
    	if (count > quantum - q_pos)
    		count = quantum - q_pos;
    
    	if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count)) {
    		rv = -EFAULT;
    		goto out;
    	}
    
    	*f_pos += count;
    	rv = count;
    
    	if (dev->size < *f_pos)
    		dev->size = *f_pos;
    
    out:
    	up(&dev->sem);
    	return rv;
    }
    
    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("AUTHOR");
    MODULE_LICENSE("GPL");
    
    module_init(scull_init_module);		
    module_exit(scull_cleanup_module);	
    


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

    Упрощенный код
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/semaphore.h>
    #include <linux/uaccess.h>
    		
    int scull_minor = 0;
    int scull_major = 0;	
    
    struct char_device {
    	char data[100];
    } device;
    
    struct cdev *p_cdev;
    
    ssize_t scull_read(struct file *flip, char __user *buf, size_t count,
    				loff_t *f_pos)
    {
    	int rv;
    
    	printk(KERN_INFO "scull: read from device\n");
    
    	rv = copy_to_user(buf, device.data, count);
    
    	return rv;
    }
    
    ssize_t scull_write(struct file *flip, char __user *buf, size_t count,
    				loff_t *f_pos)
    {
    	int rv;
    
    	printk(KERN_INFO "scull: write to device\n");
    
    	rv = copy_from_user(device.data, buf, count);
    
    	return rv;
    }
    
    int scull_open(struct inode *inode, struct file *flip)
    {
    	printk(KERN_INFO "scull: device is opend\n");
    
    	return 0;
    }
    
    int scull_release(struct inode *inode, struct file *flip)
    {
    	printk(KERN_INFO "scull: device is closed\n");
    
    	return 0;
    }
    
    struct file_operations scull_fops = {		
    	.owner = THIS_MODULE,			
    	.read = scull_read,
    	.write = scull_write,
    	.open = scull_open,
    	.release = scull_release,
    };
    
    void scull_cleanup_module(void)
    {
    	dev_t devno = MKDEV(scull_major, scull_minor);
    
    	cdev_del(p_cdev);
    
    	unregister_chrdev_region(devno, 1); 
    }
    
    static int scull_init_module(void)
    {
    	int rv;
    	dev_t dev;
    
    	rv = alloc_chrdev_region(&dev, scull_minor, 1, "scull");	
    
    	if (rv) {
    		printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
    		return rv;
    	}
    
    	scull_major = MAJOR(dev);
    
    	p_cdev = cdev_alloc();
    	cdev_init(p_cdev, &scull_fops);
    
    	p_cdev->owner = THIS_MODULE;
    	p_cdev->ops = &scull_fops;
    
    	rv = cdev_add(p_cdev, dev, 1);
    
    	if (rv)
    		printk(KERN_NOTICE "Error %d adding scull", rv);
    
    	printk(KERN_INFO "scull: register device major = %d minor = %d\n", scull_major, scull_minor);
    
    	return 0;
    }
    
    MODULE_AUTHOR("Name Surname");
    MODULE_LICENSE("GPL");
    
    module_init(scull_init_module);
    module_exit(scull_cleanup_module);
    
    


    Опрос


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

    Если у вас уже есть такой опыт, вы можете поделиться им и написать мне, с какими вы столкнулись проблемами/ошибками при переносе драйверов устройств. А я в свою очередь постараюсь добавить ваш опыт в статью (обязательно укажу вас в ней).
    Спасибо! :)
    Писать про портирование?
    • 92.9%Да, будет интересно прочитать.53
    • 7%Нет, не нужно делать из хабра мусор!4

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    • +13
    • 7,9k
    • 3
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 3
    • –1
      В данной статье мы добавим в наш драйвер функции открытия scull_open, чтения/записи scull_read/scull_write и получим первый рабочий драйвер устройства
      Хотелось бы, так сказать, в общих чертах понять, кем будет работать этот первый рабочий драйвер. Он планируется как dummy или всё-таки он будет делать что-то полезное?
      • 0
        Добрый вечер!
        В начале первой части написано:
        Цель данной статьи — показать принцип реализации драйверов устройств в системе Linux, на примере простого символьного драйвера.
        Обычно драйвера устройств, как бы это странно не звучало, пишутся для конкретного устройства, но в данной статье показан драйвер для «фейковго устройства». Это сделано для того, чтобы показать принцип реализации драйверов устройств в системе Linux. Как вы можете видеть функции read и write просто осуществляют чтение и запись из/в выделенного потока байтов.
        • +1
          А можно весь код загихабить к примеру? И Больше кишочек хочется?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.