[Linux kernel]How to make a simple character device drivers

I. Overview

In the Linux world there are categories kind of device drivers: character device drivers and block device drivers.

This division is done base on speed, volume and way of organizing the data to be transferred from the device to the system and vice versa.

In the character device drivers,  there are slow devices, which manage a small amount of data and access to data does not require frequent seek requires. Examples are devices such as keyboard, mouse, serial ports, sound card, joystick. In general, operations with these devices(read, write) are performed sequentially byte to byte.

The block device drivers includes devices where data volume is large, data is organized on blocks, and search is common. Examples of devices that fall into this category are hard drives, cdroms, ram disks, magnetic tape drives. For these devices, reading and writing is done at the data block level.

For the two types of device drivers, the Linux kernel offers different APIs. If for device devices system calls go directly to device drivers, in case of block device devices, the drivers do not work directly with system calls. In the case of block devices, communication between the user-space and the block device driver is mediated by the file management subsystem and the block device subsystem . The role of these subsystems is to prepare the device driver’s necessary resources (buffers), to keep the recently read data in the cache buffer, and to order the read and write operations for performance reasons.

II. Majors and minors

In Linux, the devices traditionally had a unique, fixed identifier associated with them. This tradition is preserved in Linux, although identifiers can be dynamically allocated (for compatibility reasons, most drivers still use static identifiers). The identifier consists of two parts: major and minor. The first part identifies the device type (IDE disk, SCSI disk, serial port, etc.) and the second one identifies the device (first disk, second serial port, etc.). Most times, the major identifies the driver, while the minor identifies each physical device served by the driver. In general, a driver will have a major associate and will be responsible for all minors associated with that major.

character device driver

As can be seen from the example above, device-type information can be found using the ls command. The special character files are identified by the c character in the first column of the command output, and the block type by the character b. In columns 5 and 6 of the result you can see the major, respectively the minor for each device.

Certain major identifiers are statically assigned to devices (in the Documentation/devices.txt file from the kernel sources). When choosing the identifier for a new device, you can use two methods: static (choose a number that does not seem to be used already) or dynamically. In /proc/devices are the loaded devices, along with the major identifier.

To create a device type file, use the mknod command; the command receives the type (block or character), major and minor of the device (mknod name type major minor). Thus, if you want to create a character device named mycdev with the major 42 and minor 0, use the command:

$ mknod /dev/mycdev c 42 0

To create the block device with the name mybdev with the 240 and minor 0 command used will be:

$ # mknod /dev/mybdev b 240 0

Next, we’ll refer drivers for character devices.

III. Data structures for a character device

In the kernel, a character-type device is represented by struct cdev, a structure used to register it in the system. Most driver operations use three important structures:  struct file_operationsstruct file and struct inode.

struct file_operations

As mentioned above, the device drivers receive unaltered system calls made by users over device-type files. Consequently, implementation of a character device drivers means implementing the system calls specific to files: openclosereadwritelseekmmap, etc. These operations are described in the fields of the file_operations structure:

  1. #include <linux/fs.h>
    
    struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        [...]
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        [...]
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        [...]
    

    It can be noticed that the signature of the function differs from the system call that the user uses. The operating system sits between the user and the device driver to simplify implementation in the device driver.

    open does not receive the parameter path or the various parameters that control the file opening mode. Similarly, readwritereleaseioctllseek do not receive as a parameter a file descriptor. Instead, these routines receive as parameters two structures: file and inode. Both structures represent a file, but from different perspectives.

    Most parameters for the presented operations have a direct meaning:
    • file and inode identifies the device type file;
    • size is the number of bytes to be read or written;
    • offset is the displacement to be read or written (to be updated accordingly);
    • user_buffer user buffer from which it reads / writes;
    • whence is the way to seek (the position where the search operation starts);
    • cmd and arg are the parameters sent by the users to the ioctl call (IO control).

    inode and file structures

    An inode represents a file from the point of view of the file system. Attributes of an inode are the size, rights, times associated with the file. An inode uniquely identifies a file in a file system.

    The file structure is still a file, but closer to the user’s point of view. From the attributes of the file structure we list: the inode, the file name, the file opening attributes, the file position. All open files at a given time have associated a file structure.

    To understand the differences between inode and file, we will use an analogy from object-oriented programming: if we consider a class inode, then the files are objects, that is, instances of the inode class. Inode represents the static image of the file (the inode has no state ), while the file represents the dynamic image of the file (the file has state).

    Returning to device drivers, the two entities have almost always standard ways of using: the inode is used to determine the major and minor of the device on which the operation is performed, and the file is used to determine the flags with which the file was opened, but also to save and access (later) private data.

    The file structure contains, among many fields:

    • f_mode, which specifies read FMODE_READ (FMODE_READ) or write (FMODE_WRITE);
    • f_flags, which specifies the file opening flags (O_RDONLYO_NONBLOCKO_SYNCO_APPENDO_TRUNC, etc.);
    • f_op, which specifies the operations associated with the file (pointer to the file_operations structure );
    • private_data, a pointer that can be used by the programmer to store device-specific data; The pointer will be initialized to a memory location assigned by the programmer.
    • f_pos, the offset within the file

    The inode structure contains, among many information, an i_cdev field, which is a pointer to the structure that defines the character device (when the inode corresponds to a character device).

IV. Writing a simple character device driver

#include<linux/init.h>
#include<linux/module.h>
#include<linux/device.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/uaccess.h>

#define DEVICE_NAME "simple_character_device_driver"
#define CLASS_NAME "simple_character_device_driver"
#define SUCCESS 0
#define BUF_LEN 256

MODULE_LICENSE("GPL");
MODULE_AUTHOR("SanDan");

static int Major;
static int Device_Open = 0;
static char msg[256];
static char *msg_Ptr;

static struct class* char_class;
static struct device* char_device;

int init_module(void);
void cleanup_module(void);
static int dev_open(struct inode*, struct file*);
static int dev_release(struct inode*, struct file*);
static ssize_t dev_read(struct file*, char *, size_t, loff_t *);
static ssize_t dev_write(struct file *, const char*, size_t, loff_t *);

static struct file_operations fops = 
{
	.open = dev_open,
	.read = dev_read,
	.write = dev_write,
	.release = dev_release
};
 
int init_module(void)
{
	Major = register_chrdev(0, DEVICE_NAME, &fops);
	if(Major < 0)
	{
		printk(KERN_INFO"Registering the character device failed with %d\n", Major);
		return Major;
	}
	printk(KERN_INFO"Register device %d successfully",Major);

	char_class = class_create(THIS_MODULE, CLASS_NAME);
	
	if(IS_ERR(char_class))
	{
		printk(KERN_ALERT "%s Cannot register device class \n",__func__);
		unregister_chrdev(Major, DEVICE_NAME);
		return PTR_ERR(char_class);
	}
	
	printk(KERN_INFO "Create successfully class");
	
	char_device = device_create(char_class, NULL, MKDEV(Major, 0), NULL, DEVICE_NAME);
	
	if(IS_ERR(char_device))
	{
		printk(KERN_ALERT "%s Cannot create device \n",__func__);
		unregister_chrdev(Major, DEVICE_NAME);
		return PTR_ERR(char_device);
	}

	printk(KERN_INFO "Create successfully device");

	return 0;
}

void cleanup_module(void)
{
	unregister_chrdev(Major, DEVICE_NAME);
	printk(KERN_INFO "Cleanup");
}	

static int dev_open(struct inode *inode, struct file *file)
{
	static int counter = 0;
	if(Device_Open) return -EBUSY;
	Device_Open++;
	msg_Ptr = msg;
	try_module_get(THIS_MODULE);
	return SUCCESS;
}

static int dev_release(struct inode *inode, struct file *file)
{
	Device_Open--;
	module_put(THIS_MODULE);
	return 0;

}
static ssize_t dev_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
{
	int bytes_read = 0;
	if(*msg_Ptr == 0) return 0;
	while(length && *msg_Ptr)
	{
		put_user(*(msg_Ptr++), buffer++);
		length--;
		bytes_read++;
	}
	 return bytes_read;
}

static ssize_t dev_write(struct file *flip, const char* buff, size_t len, loff_t * off)
{
	copy_from_user(msg, buff, strlen(buff));
	return strlen(msg);
}

V. Writing makefile

obj-m:=device_char.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

VI. Loading and unloading a character device driver

Due to that the character device driver is a kernel module, so in order to loading and unloading, we follow step by step as a kernel module.

For detail:

How to make a simple linux kernel module

Add Comment