📜 ⬆️ ⬇️

How to write your first Linux device driver

Hello, dear habrachiteli.

The purpose of this article is to show the principle of implementation of device drivers on a Linux system, using the example of a simple symbolic driver.

For me, the main goal is to summarize and form the basic knowledge for writing future kernel modules, as well as gain experience in presenting technical literature to the public, since in six months I will be performing with my graduation project (yes, I am a student).
')
This is my first article, please do not judge strictly!

PS

It turned out too many letters, so I decided to divide the article into three parts:

Part 1 - Introduction, initialization and cleaning of the kernel module.
Part 2 - Functions open, read, write and trim.
Part 3 - We write Makefile and test the device.

Before the introduction, I want to say that the basic things will be described here, more detailed information will be presented in the second and last part of this article.

So, let's begin.

Preparatory work


UPD.

Thanks Kolyuchkin for clarifying.

A character driver (Char driver) is a driver that works with character devices.
Character devices are devices that can be accessed as a byte stream.
An example of a character device is / dev / ttyS0, / dev / tty1.

UPD.

To the question about kernel verification:
~$ uname -r 4.4.0-93-generic 

The driver represents each character device with the scull_dev structure, and also provides the cdev interface to the kernel.

 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; 

The device will present a linked list of pointers, each of which points to the scull_qset structure.

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

For clarity, look at the picture.

image

To register a device, you need to set special numbers, namely:

MAJOR - senior number (is unique in the system).
MINOR - low number (not unique in the system).

The kernel has a mechanism that allows you to register specialized numbers manually, but this approach is undesirable and it is better to politely ask the kernel to dynamically allocate them for us. Sample code will be below.

After we have determined the numbers for our device, we need to establish a connection between these numbers and driver operations. This can be done using the file_operations structure.

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

The kernel has special module_init / module_exit macros that indicate the path to the module initialization / deletion functions. Without these definitions, initialization / deletion functions will never be called.

 module_init(scull_init_module); module_exit(scull_cleanup_module); 

Here we will store basic information about the device.

 int scull_major = 0; /* MAJOR */ int scull_minor = 0; /* MINOR */ int scull_nr_devs = 1; /*    */ int scull_quantum = 4000; /*     */ int scull_qset = 1000; /*    */ 

The final stage of the preparatory work will be to connect the header files.
A brief description is given below, but if you want to dig deeper, then welcome to the wonderful site: 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> /*      */ 

Initialization


Now let's look at the device initialization function.

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

First, by calling alloc_chrdev_region, we register a range of device symbol numbers and specify the device name. After calling MAJOR (dev) we get the major number.
Next, the returned value is checked, if it is an error code, then we exit the function. It is worth noting that when developing a real device driver, you should always check return values, as well as pointers to any elements (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); 

If the returned value is not an error code, we continue to perform initialization.

We allocate memory by making a call to the kmalloc function and be sure to check the pointer to NULL.

UPD

It is worth mentioning that instead of calling the two functions kmalloc and memset, you can use one call to kzalloc, which allocates a memory area and initializes it to zero.

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

We continue initialization. The main function here is scull_setup_cdev, we will talk about it a little later. MKDEV is used to store older and younger device numbers.

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

We return the value or process the error and delete the device.

 return 0; fail: scull_cleanup_module(); return rv; } 

Above, we presented the scull_dev and cdev structures that implement the interface between our device and the kernel. The scull_setup_cdev function initializes and adds a structure to the system.

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

Deletion


The scull_cleanup_module function is called when a device module is removed from the kernel.
Reverse initialization process, remove device structures, free up memory, and delete junior and major numbers allocated by the kernel.

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

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


I am pleased to hear constructive criticism and will wait for feedback.

If you have found errors or I have not correctly stated the material, please point me to this.
For a faster reaction, write to the LAN.

Thank!

Literature


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


All Articles