📜 ⬆️ ⬇️

How to write your first Linux device driver. Part 2

Hello to the readers!

In the previous section, we looked at the basic structures, as well as the initialization and deletion of the device.

In this article, we will add the scull_open read / write function, scull_read / scull_write read / write function to our driver, and get the first working device driver.
')


I want to thank all users who have read, liked and commented on my previous article. Special thanks for clarifying Kolyuchkin and dlinyj .



Last time there was a proposal not to examine in detail the insides of each function, so in this article I will try to present them in a broader sense.

Go to the point!

In the previous article, we did not consider one function that is part of scull_cleanup_module, namely scull_trim. As you can see in the function, there is a loop that simply goes through the linked list and returns memory to the kernel. We will not focus our attention here. The main thing to come!

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


Before considering the function sull_open, I would like to make a small digression.

Much of the Linux system can be represented as a file. What operations are performed with files more often - opening, reading, writing and closing. Also with device drivers, we can open, close, read and write to the device.

Therefore, in the file_operations structure, we see such fields as: .read, .write, .open, and .release are the basic operations that the driver can perform.

Scull_open function


And immediately the code:

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

The function takes two arguments:

  1. A pointer to an inode structure. An inode structure is an inode that stores information about files, directories, and file system objects.
  2. A pointer to the file structure. The structure that is created by the kernel each time the file is opened contains the information needed by the upper levels of the kernel.

The main function of scull_open is to initialize the device (if the device is opened for the first time) and fill in the necessary fields of the structures for its correct operation.
Since our device does nothing, we have nothing to initialize :)

But to create the appearance of work, we will perform several actions:

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

In the above code, using container_of, we get a pointer to cdev of type struct scull_dev, using inode-> i_cdev. The resulting pointer is written in the private_data field.

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

Further, it is still easier if the file is open for writing - we will clear it before use and display the message that the device is open.

Scull_read function


Function code
 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; } 


Now I will try to describe the meaning of using the read function.
Since our device is symbolic, we can work with it as a stream of bytes. And what can you do with the stream of bytes? Correct - read. So, as is clear from the name of the function, it will read from the baytik device.

When the read function is called, several arguments are passed to it, the first of which we have already considered, now we will look at the rest.

buf is a pointer to a string, and __user tells us that this pointer is in user space. The argument is passed by the user.
count - the number of bytes to read. The argument is passed by the user.
f_pos - offset. The argument passes the core.

That is, when the user wants to read from the device, he calls the read function (not scull_read) while indicating the buffer where the information will be written and the number of bytes read.
Now let's take a closer look at the code:

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

The first step is to check:

  1. If the offset is greater than the file size, then, for obvious reasons, we can no longer read. We will display an error and exit the function.
  2. If the sum of the current offset and the size of the data to be read is larger than the size of the quantum, then we adjust the size of the data to be read and report the message to the top.

Here is the subject of conversation:

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

copy_to_user - copies data to buf (which is in user space) from the memory allocated by the kernel dptr-> data [s_pos] size count .

If all these variables are not clear to you now: s_pos, q_pos, item, rest - it doesn't matter, here the main thing is to understand the meaning of the read function, and in 3 parts of the article we will test our driver, and there it will be clear what each of them is responsible for. But if you want to know about it now, you can always use printk (if you know what I mean :)).

Scull_write function


In view of the fact that the scull_write function is very similar to scull_read, and its difference from the above one is clear from the name, I will not describe this function, but I suggest you consider it on your own.

Function code
 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; } 


Everything, we wrote a full-fledged useless driver, the full code will be given below, and in the next article we will test it.

Full code
 #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); 


After I read the article, I realized that I got too much code and unnecessary functions, so the simplest version of the implementation of this driver is given below.

Simplified code
 #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); 


Poll


So there were circumstances that at the moment I have the task of porting device drivers from one kernel version to another. Would you be interested in reading how this process takes place with specific examples?

If you already have this experience, you can share it and write to me about the problems / errors you encountered when migrating device drivers. And I, in turn, will try to add your experience to the article (I will definitely show you in it).
Thank! :)

Source: https://habr.com/ru/post/338118/


All Articles