Привет хаброчитателям!
В предыдущей части мы рассмотрели базовые структуры, а также написали инициализацию и удаление устройства.
В данной статье мы добавим в наш драйвер функции открытия scull_open, чтения/записи scull_read/scull_write и получим первый рабочий драйвер устройства.
Хочу выразить благодарность всем пользователям, которые прочитали, лайкнули и прокомментировали мою предыдущую статью. Отдельное спасибо за уточнения Kolyuchkin и dlinyj.
В прошлый раз поступило предложение не рассматривать подробно внутренности каждой функции, поэтому в данной статье я попытаюсь представить их в более широком смысле.
Сразу к делу!
В предыдущей статье мы не рассмотрели одну функцию, которая является частью scull_cleanup_module, а именно scull_trim. Как вы можете наблюдать в функции присутствует цикл, который просто проходится по связному списку и возвращает память ядру. Мы не будем заострять тут наше внимание. Главное впереди!
Перед рассмотрением функции sull_open, я хотел бы сделать маленькое отступление.
Многое в системе Linux может быть представлено в виде файла. Какие операции чаще совершаются с файлами — открытие, чтение, запись и закрытие. Также и с драйверами устройств, мы можем открыть, закрыть, прочитать и записать в устройство.
Поэтому в структуре file_operations, мы видим такие поля как: .read, .write, .open и .release — это базовые операции, которые может выполнять драйвер.
И сразу код:
Функция принимает два аргумента:
Главной функцией scull_open является инициализация устройства (если устройство открыто первый раз) и заполнение необходимых полей структур для его корректной работы.
Так как наше устройство ничего не делает, то нам и нечего инициализировать:)
Но чтобы создать видимость работы, мы выполним несколько действий:
В выше приведенном коде с помощью container_of мы получаем указатель на cdev типа struct scull_dev, используя inode->i_cdev. Полученный указатель записываем в поле private_data.
Далее, все еще проще, если файл открыт для записи — очистим его перед использованием и выведем сообщение о том, что устройство открыто.
Сейчас я постараюсь описать смысл использования функции read.
Так как наше устройство является символьным, то мы можем работать с ним как с потоком байтов. А что можно делать с потоком байтов? Правильно — читать. Значит, как понятно из названия функции, она будет читать из устройства байтики.
Когда вызывается функция чтения, ей передаются несколько аргументов, первый из них мы уже рассмотрели, теперь посмотрим на остальные.
buf — это указатель на строку, а __user говорит нам о том, что этот указатель находится в пространстве пользователя. Аргумент передает пользователь.
count — количество байтов, которые нужно прочитать. Аргумент передает пользователь.
f_pos — смещение. Аргумент передает ядро.
Т.е., когда пользователь хочет прочитать из устройства, он вызывает функцию read (не scull_read) при этом указывает буфер куда будет записана информация и количество читаемых байт.
Теперь немного подробнее рассмотрим код:
Первым делом идут проверки:
А вот и предмет разговора:
copy_to_user — копирует данные в buf (который находится в пространстве пользователя) из памяти, которую выделило ядро dptr->data[s_pos] размером count.
Если вам сейчас не понятны все эти переменные : s_pos, q_pos, item, rest — не беда, тут главное понять смысл функции read, а уже в 3 части статьи мы протестируем наш драйвер, и там уже будет понятно за что отвечает каждая из них. Но если вы хотите узнать об этом сейчас, вы всегда можете использовать printk (если вы понимаете, о чем я:)).
В виду того, что функция scull_write очень похожа на scull_read, и ее отличие от вышерассмотренной понятен из названия, я не буду расписывать эту функцию, а предлагаю вам осмыслить ее самостоятельно.
Все, мы написали полноценныйбесполезный драйвер, внизу будет приведен полный код, а в следующей статье мы его протестируем.
После того как я прочитал статью, то понял, что получилось слишком много кода и лишних функций, поэтому ниже приведен максимально простой вариант реализации данного драйвера.
Так сложились обстоятельства, что в данный момент у меня есть задачи по портированию драйверов устройств с одной версии ядра на другую. Интересно ли вам было бы прочитать как происходит этот процесс на конкретных примерах?
Если у вас уже есть такой опыт, вы можете поделиться им и написать мне, с какими вы столкнулись проблемами/ошибками при переносе драйверов устройств. А я в свою очередь постараюсь добавить ваш опыт в статью (обязательно укажу вас в ней).
Спасибо! :)
В предыдущей части мы рассмотрели базовые структуры, а также написали инициализацию и удаление устройства.
В данной статье мы добавим в наш драйвер функции открытия 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;
}
Функция принимает два аргумента:
- Указатель на структуру inode. Структура inode — это индексный дескриптор, который хранит информацию о файлах, каталогах и объектах файловой системы.
- Указатель на структуру 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;
}
Первым делом идут проверки:
- Если смещение больше, чем размер файла, то, по понятным причинам, читать мы больше не можем. Выведем ошибку и выйдем из функции.
- Если сумма текущего смещения и размера данных для считывания больше размера кванта, то мы корректируем размер данных, подлежащих считыванию, и рапортуем сообщение наверх.
А вот и предмет разговора:
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);
Опрос
Так сложились обстоятельства, что в данный момент у меня есть задачи по портированию драйверов устройств с одной версии ядра на другую. Интересно ли вам было бы прочитать как происходит этот процесс на конкретных примерах?
Если у вас уже есть такой опыт, вы можете поделиться им и написать мне, с какими вы столкнулись проблемами/ошибками при переносе драйверов устройств. А я в свою очередь постараюсь добавить ваш опыт в статью (обязательно укажу вас в ней).
Спасибо! :)
Комментарии (3)
Alexeynew Автор
14.10.2017 21:29Добрый вечер!
В начале первой части написано:
Цель данной статьи — показать принцип реализации драйверов устройств в системе Linux, на примере простого символьного драйвера.
Обычно драйвера устройств, как бы это странно не звучало, пишутся для конкретного устройства, но в данной статье показан драйвер для «фейковго устройства». Это сделано для того, чтобы показать принцип реализации драйверов устройств в системе Linux. Как вы можете видеть функции read и write просто осуществляют чтение и запись из/в выделенного потока байтов.
mwambanatanga