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

в 13:28, , рубрики: C, drivers, linux, Разработка под Linux

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

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

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

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

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

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

Сразу к делу!

В предыдущей статье мы не рассмотрели одну функцию, которая является частью 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 opendn");

	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 %lun", dev->size);
		goto out;
	}

	if (*f_pos + count > dev->size) {
		printk(KERN_INFO "scull: correct countn");	
		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 %lun", dev->size);
	goto out;
}

if (*f_pos + count > dev->size) {
	printk(KERN_INFO "scull: correct countn");	
	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 opendn");

	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 %lun", dev->size);
		goto out;
	}

	if (*f_pos + count > dev->size) {	
		printk(KERN_INFO "scull: correct countn");	
		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 %dn", 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 = %dn", 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 devicen");

	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 devicen");

	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 opendn");

	return 0;
}

int scull_release(struct inode *inode, struct file *flip)
{
	printk(KERN_INFO "scull: device is closedn");

	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 %dn", 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 = %dn", scull_major, scull_minor);

	return 0;
}

MODULE_AUTHOR("Name Surname");
MODULE_LICENSE("GPL");

module_init(scull_init_module);
module_exit(scull_cleanup_module);

Опрос

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

Если у вас уже есть такой опыт, вы можете поделиться им и написать мне, с какими вы столкнулись проблемами/ошибками при переносе драйверов устройств. А я в свою очередь постараюсь добавить ваш опыт в статью (обязательно укажу вас в ней).
Спасибо! :)

Автор: Alexey Ivanov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js