Implementation of simple character device files (including reading, writing and logical sorting of files)

The principle and implementation of simple character device files

    • Preface
    • 1. Header file
    • 2. Global variable definition
    • 3. Entity functions for device operations
      • 3.1 open method
      • 3.2 release method
      • 3.3 write method
      • 3.4 read method
    • 4 Device file structure
    • 5. Initialization and destruction of equipment
      • 5.1 chrdev_init
      • 5.2 chrdev_exit
    • 6. Module initialization and uninstallation
    • 7. Character device reading and writing – logical sorting
      • 7.2 The relationship between the three
      • 7.3 Benefits of device files
      • 7.4 Reading and writing character files (logical sorting)
    • 8. Summary

Foreword

Here is a simple source code implementation of character device file as an example to make a careful analysis.

This article uses a total score method. First, I will show you the overall logic diagram, then introduce the source code, and finally sort out the overall logic based on the diagram.

1. Header file

Listed below are all standard header files in the Linux kernel, including various functions and functions commonly used in kernel development. This includes support for key functions such as Module Initialization, File Operation, Memory Management, and Character Device.

Each line of commented functions belongs to the corresponding kernel library. I will not introduce the use of each function first, but will analyze and introduce it in detail later.

#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h> // file_operations
#include <linux/uaccess.h> // copy_to-user copy_from_user
#include <linux/cdev.h> // cdev_init cdev_add cdev_del
#include <linux/slab.h> // kmalloc kfree

2. Global variable definition

#define MYNMAJOR 240 // Major device number
#define MYNAME "my_chrdev" // device name
char kbuf[100]; //Define a cache array for the kernel to receive data
static struct cdev *char_dev; // Define a character device structure
static int major; // Used to save the major device number
  • kbuf: This array is allocated in the kernel and is accessible throughout the lifetime of the kernel module.
  • struct cdev type: A structure representing a character device through which the operation of the character device can be managed.

3. Entity functions for device operations

These functions have a standard division into open, release, read, and write methods.
Users implement it according to specific needs. In fact, the implemented functions are .open, .release, .read, and corresponding to the character driver. The function of write method. Establish the connection between the four major methods of device files and the four major methods of device drivers in the structure.
(Keep reading, I also insisted on writing it down).

3.1 open method

This function is the “open” function in the character device driver, which is used to handle the operation when the user space program opens the device file. The method name usually takes the format of driver_name_open.

  • struct inode *inode: represents the index node of the device file;
  • struct file *file: represents the file descriptor of the device file.

Usually implemented within this function: Device initialization, Allocating resources, Setting status and other preparations.

static int my_chrdev_open(struct inode *inode, struct file *file)
{<!-- -->
// code part for opening the device
printk(KERN_INFO "test_module_open\\
");
return 0;
}

A return value of 0 indicates that the device was successfully opened; a non-zero return indicates an error in opening the device, and this error code will be passed to the user space program.

3.2 release method

Corresponding to the open function, release is used to handle the operation when the user space program closes the device file. It is usually executed when the device file is closed.

Within this function, it is usually necessary to implement: Clean resources, Close device, Save state and other functions.

static int my_chrdev_release(struct inode *inode, struct file *file)
{<!-- -->
// code part for release the device
printk(KERN_INFO "test_chrdev_release\\
");
return 0;
}

A return value of 0 indicates that the device was successfully released; a non-zero return indicates a device release error, and this error code will be passed to the user space program.

3.3 write method

The function of this function is to write user space data to the device’s data buffer and may trigger hardware-related operations.

  • struct file *file: This is a structure pointer representing the file descriptor of the open device file.
  • const char __user *buf: This is the user-provided buffer in which data will be written to the device.
  • size_t size: This is the number of bytes the user wants to write.
  • loff_t *opps: This is a pointer representing the current writing position, used to track the writing position in the file.
static ssize_t my_chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *opps)
{<!-- -->
int ret = -1;
printk(KERN_INFO "my_chrdev_write\\
");
\t
// The memset function sets the contents of the buffer `kbuf` to 0 so that the previous data can be cleared before writing new data.
memset(kbuf, 0, sizeof(kbuf));
\t
//Copy the user-provided data from the `buf` buffer in user space to the `kbuf` buffer in kernel space just defined
ret = copy_from_user(kbuf, buf, size);
if (ret){<!-- -->
printk(KERN_ERR "copy_from_user fail...\\
");
return -EINVAL;
}
\t
printk(KERN_INFO "copy_from_user success...\\
");
// real sense: we will write some code for operating the hardware, based on the above data
//coding...
return size;
}

In actual applications, you may need to perform some specific operations based on the written data, such as controlling the behavior of hardware devices based on data.

3.4 read method

The function of this function is to read data from the device’s data buffer kbuf into the user-provided buffer and update the current read position for the next read.

  • struct file *file: Structure pointer representing the file descriptor of the opened device file;
  • char __user *buf: This is the buffer provided by user space, and data will be copied to this buffer;
  • size_t size: This is the number of bytes requested by the user to read;
  • loff_t *ppos: This represents a pointer to the current read position, used to track the read position in the file.
static ssize_t my_chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{<!-- -->
int ret = -1;
printk(KERN_INFO "my_chrdev_read\\
");
\t
// Calculate the number of remaining bytes from the current reading position to the end of the data
    int remaining_bytes = strlen(kbuf) - *ppos;

// If the number of remaining bytes is less than or equal to 0, it means there is no remaining data to read, and the function returns 0.
    if (remaining_bytes <= 0)
    printk(KERN_ERR "no remaining_bytes need to be read\\
");
            return 0;
// Prevent the user from reading data bytes beyond the remaining readable data bytes in the buffer.
// Because the user can set how many bytes to read each time
    if (size > remaining_bytes)
            size = remaining_bytes;
//Copy data from kbyf in kernel space to user-provided buf buffer
    if(copy_to_user(buf, kbuf + *ppos, size) != 0)
    printk(KERN_ERR "copy_to_user fail\\
");
            return -EFAULT;
\t
// Update the read pointer so that it points to the next location to be read (it feels like the pointer is stored in the kernel, because it is a pointer, so it will be updated synchronously)
    *ppos + = size;

    printk(KERN_ERR "copy_to_user success...\\
");
    return size;
}

copy_to_user(buf, kbuf + *ppos, size)

  • kbuf + *ppos: means offset the read pointer to the head of the remaining unread bytes
  • size: Indicates that starting from the kbuf + *ppos position, size-sized bytes need to be copied to buf.

4 Device file structure

This code defines a file_operation built-in type structure, my_modules_fops, which is used to associate the character device operation function with the character device driver.

  • .owner = THIS_MODULE: This field is used to specify the module owner, usually THIS_MODULE is used to indicate that the current module is based on this file operation structure;
  • .open = my_chrdev_open: Associate the open operation of the character device with the customized my_chrdev_open method in the source code. When the user space uses the open system call, the kernel will call the my_chrdev_open function;
  • .release = my_chrdev_release: Character device release function, which associates the character device release operation release with the customized my_chrdev_release method in the source code. When the user space uses the release system call, the kernel will call the my_chrdev_release function;
  • .read = my_chrdev_read: This line associates the read operation of the character device with a custom function my_chrdev_read. When a user space program reads data from this character device using the read system call, the kernel calls the my_chrdev_read function to perform the read operation.
  • .write = my_chrdev_write: This line associates the write operation of the character device with a custom function my_chrdev_write. When a user space program uses the write system call to write data to this character device, the kernel calls the my_chrdev_write function to perform the write operation.
static const struct file_operations my_modules_fops = {<!-- -->
.owner = THIS_MODULE, // means it is a driver
.open = my_chrdev_open, // build the link between real function(my_chrdev_open) with device operation api(.open)
.release = my_chrdev_release, // the same
.read = my_chrdev_read,
.write = my_chrdev_write,
};

5. Initialization and destruction of equipment

5.1 chrdev_init

Character device creation and preparation, including:

  • Device number assignment
  • Character device memory allocation
  • Character device initialization
  • Add a character device to the kernel’s character device list
static int __init chrdev_init(void)
{<!-- -->
int ret;
int devNo; // Save the device number (major device number)
// 1. Apply for a device number from the system
// devNo device number address, the minor device number starts from 0, 1 is the number of device numbers to be allocated, CharDriver is the device name
ret = alloc_chrdev_region( & amp;devNo, 0, 1, "CharDriver");
if (ret < 0){<!-- -->
printk(KERN_ERR "Failed to allocate device number\\
");
return ret;
}

// 2. If you want to modify the device number, you can use the following two lines of code
// Otherwise, it is not needed because devNo is already an MKDEV device number.
major = MAJOR(devNo);
devNo = MKDEV(major, 0); //
\t
// 3. Allocate character device file memory
// kzalloc is used to allocate kernel memory and initialize the allocated memory area to zero
char_dev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (!char_dev){<!-- -->
// If the device memory allocation error occurs, release the device number resource.
unregister_chrdev_region(devNo, 1);
return 0;
}

// 4. Initialize character device structure
// Initialize the character device structure in char_dev memory and associate the device file operation my_modules_fops with it
//The purpose is to tell the kernel how to handle the operation of character devices
cdev_init(char_dev, &my_modules_fops);

// 5. Add this device to the kernel's character device list
//And tell the kernel that the specific primary and secondary device number is devNo and the number of devices is 1
ret = cdev_add(char_dev, devNo, 1);
if (ret < 0){<!-- -->
unregister_chrdev_region(devNo, 1);
printk(KERN_ERR "Failed to add character device\\
");
return ret;
}

printk(KERN_INFO "Character driver loaded\\
");
return 0;
}

5.2 chrdev_exit

  • cdev_del(char_dev);: This line of code removes the previously added character device from the kernel. The cdev_del function is used to delete the registration of a character device to ensure that operation requests for the device are no longer accepted.

  • kfree(char_dev);: This line of code releases the memory occupied by the previously allocated character device structure char_dev. This is to prevent memory leaks and ensure that all allocated memory is freed when the module is unloaded.

  • unregister_chrdev_region(MKDEV(major, 0), 1);: This line of code unregisters the previously assigned device number. It uses the unregister_chrdev_region function to release the previously allocated device number resource. The parameter MKDEV(major, 0) is used to build the correct device number, and 1 means releasing a device number.

static void __exit chrdev_exit(void)
{<!-- -->
//del character divice
cdev_del(char_dev);

kfree(char_dev);

// release device number
unregister_chrdev_region(MKDEV(major, 0), 1);

printk(KERN_INFO "Character driver unloaded\\
");
}

6. Module initialization and uninstallation

module_init(chrdev_init);: This line of code is used to specify the initialization function of the module. When the module is loaded, the kernel calls the chrdev_init function, which is the entry point for module initialization.

module_exit(chrdev_exit);: This line of code is used to specify the uninstall function of the module. When the module is unloaded, the kernel will call the chrdev_exit function, which is the entry point for module unloading.

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");

7. Character device reading and writing – logical sorting

I think we can talk about it from two perspectives: the computer perspective and the user perspective.

– Computer perspective: Devices can be understood as some resources of the computer (printers, monitors, GPU graphics cards, etc.), and each device should correspond to a device driver Program, which is saved in the operating system through two linked lists.
Every time you add a device, you need to install a corresponding driver; similarly, every time you install a driver, you need to add a device.

That is: the operating system issues instructions to control the device through the driver.

– User perspective: A device file is a file associated with a device driver that interacts with the device driver so that users and applications can access the hardware device without knowing the detailed implementation of the kernel driver. .

In Unix/Linux, device files usually exist in the /dev directory and are named according to the device type and number. For example, /dev/sda may represent the first hard disk in the system, and /dev/ttyUSB0 may represent a USB serial device.

Here comes the point

We check the disk usage in the system by executing df -h

Oh, you can see that both sdas are located in the dev directory. Note that we are looking for disk usage, and the terminal further tells us disk usage by telling us the usage of /dev/sda1.

Isn’t this right? “In Linux, everything is a file“.

It turns out that the so-called “all files” refer to the concept that these device resources correspond to device files in Linux. This made me realize it directly. (If you don’t understand, it is recommended to watch this content again and again)

When we operate disks, printers, or other hardware devices, the operating system provides users with a corresponding device file (device node) to facilitate users to indirectly operate these hardware devices.

7.2 The relationship between the three

The logic here can be shown as the following structure diagram

Device files are bound to kernel drivers, which means they actually operate the hardware device through the kernel driver. Users and applications can open, read, write, and close device files, and these operations trigger corresponding kernel driver operations.

7.3 Benefits of device files

Summary: Device files provide a standardized way to access hardware devices and shield the complexity of the underlying hardware.

7.4 Reading and writing character files (logical sorting)


(1) Device driver registration

Three elements of device driver:

  • Device number devNo: major device number and minor device number
  • File operation structure file_operation: implementation of entity functions corresponding to four system calls
  • Character device structure cdev: memory application, structure initialization, registration in the kernel

(2) Loading the driver

insmod charDriver.ko # Install the compiled character driver module

module_init(chrdev_init)—->chrdev_init

(3) Write operation

echo "hello Drivers!" > /dev/charDriver
  • .open —-> my_chrdev_read
  • .write —-> my_chrdev_write
  • .release –>my_chrdev_release

(4) Read operation

cat /dev/charDriver
  • .open —-> my_chrdev_read
  • .read —-> my_chrdev_read
  • .release –>my_chrdev_release

(5) Uninstall the driver

rmmod charDriver # Uninstall the specified driver module

module_exit(chrdev_exit)—->chrdev_exit

8. Summary

I feel like I learned operating systems in vain before. It seems that understanding something still requires a certain amount of time and practice.

The stuff is relatively basic, and the writing is voluminous and casual. If you have any questions, you can leave a message for discussion. ==!