Back to Home

How to write your first Linux device driver

C · linux · drivers

How to write your first Linux device driver

Hello dear habrachitateli.

The purpose of this article is to show the principle of implementing device drivers in a Linux system, using an example of a simple character 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, as in six months I will speak 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 - Write a Makefile and test the device.

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

So, let's begin.

Preparatory work


UPD

Thanks to 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 stream of bytes.
An example 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 a 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 - the senior number (is unique in the system).
MINOR - minor number (not unique in the system).

There is a mechanism in the kernel 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,
};

There are special module_init / module_exit macros in the kernel 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 step in the preparatory work will be to include the header files.
A brief description is given below, but if you want to dig a little deeper, then welcome to the beautiful site: lxr

#include  /* Содержит функции и определения для динамической загрузки модулей ядра */
#include   /* Указывает на функции инициализации и очистки */
#include     /* Содержит функции регистрации и удаления драйвера */
#include   /* Содержит необходимые функции для символьного драйвера */
#include   /* Содержит функцию ядра для управления памятью */
#include  /* Предоставляет доступ к пространству пользователя */

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 of all, by calling alloc_chrdev_region we register the 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 exit the function. It should be noted that when developing a real device driver, you should always check the 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.

Allocate memory by calling kmalloc and be sure to check for a 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 will allocate a memory area and initialize it with zeros.

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

Continuing the initialization. The main function here is scull_setup_cdev, we will talk about it a little later. MKDEV is used to store senior and minor 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 a value or process an error and delete the device.

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

The scull_dev and cdev structures that implement the interface between our device and the kernel were presented above. The scull_setup_cdev function initializes and adds 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);
}

Delete


The scull_cleanup_module function is called when a device module is removed from the kernel.
The reverse process of initialization, we delete the structure of the devices, free the memory and delete the low and high 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  	
#include  	
#include  		
#include  	
#include  	
#include 
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 will listen to constructive criticism with pleasure and will wait for feedback.

If you find errors or I didn’t write the material correctly, please point me to this.
For a faster reaction, write to the PM.

Thanks!

Literature


  • Linux device drivers 3rd edition
  • Essential linux device drivers

Read Next