Creating and Managing Character Device Files
Creating and managing character device files in Linux is an essential skill for developers working in the realm of device drivers. Character devices are fundamental components that allow applications to interact with hardware via a character stream, meaning data is transferred one character at a time. In this article, we’ll delve into the process of creating character device files, understanding major and minor numbers, and utilizing mdev for device management.
Understanding Character Devices
Character devices differ from block devices in that they handle data streams one character at a time. Devices like keyboards, mice, and serial ports fall into this category. The Linux kernel interfaces with these devices through character device files located in the /dev directory.
Major and Minor Numbers
Each character device in Linux is identified by a unique combination of a major number and a minor number.
Major Number
The major number indicates the driver associated with the device. It tells the Linux kernel which driver to load to handle requests coming from that device. In systems like /dev/sda, the 'sda' is associated with the major number mapped to the block device driver responsible for it.
Minor Number
The minor number differentiates between the various devices handled by a particular driver. For instance, within the same driver (major number), you might have multiple devices such as /dev/ttyS0 and /dev/ttyS1, which could correspond to the first and second serial ports on your machine.
Allocating Major and Minor Numbers
You can allocate major and minor numbers using several methods. For a newly created driver, it is usually best to dynamically allocate a major number instead of hardcoding a static value. Here's how you can do it:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#define DEVICE_NAME "my_char_device"
static int majorNumber;
static int __init my_driver_init(void) {
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
if (majorNumber < 0) {
printk(KERN_ALERT "Failed to register a major number\n");
return majorNumber;
}
printk(KERN_INFO "Registered correctly with major number %d\n", majorNumber);
return 0;
}
static void __exit my_driver_exit(void) {
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "Goodbye from the LKM!\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
In the code snippet above, register_chrdev dynamically allocates a major number. The 0 in the function call is a request for an available major number.
Creating Character Device Files
Once you’ve created your character device driver, you need to create the corresponding character device file in the /dev directory. This can be done either manually using the mknod command or automatically through udev or mdev.
Creating a Device File Manually
You can create a device file using mknod. The syntax is as follows:
mknod /dev/my_char_device c <major_number> <minor_number>
For example, if your major number is 240 and your minor number is 0, you would execute:
mknod /dev/my_char_device c 240 0
Automatic Device File Creation
For more dynamic management, most modern Linux systems utilize udev or mdev to handle device file creation automatically based on hardware changes and device presence.
Using mdev
mdev is a simpler alternative to udev and can be configured to manage devices in a lightweight manner. To use mdev, ensure it is installed on your system and configured properly in /etc/mdev.conf.
Created device files can be defined in the mdev.conf file like so:
# Match the device name (/dev/my_char_device) and set permissions.
my_char_device root:root 660
Important Functionality in the Character Device File
When creating a character device driver, you'll need to define several file operations that allow user-space applications to interact effectively with your device.
static struct file_operations fops = {
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
Each of these operations would have a corresponding implementation in your driver that dictates how your driver behaves when a user-space application reads from or writes to the device file.
Example: Simple Character Device Read/Write
Let's put together a basic example illustrating how to handle reading from and writing to a character device.
#include <linux/uaccess.h> // for copy_to_user and copy_from_user
#define BUF_LEN 80
static char message[BUF_LEN] = {0};
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
int bytes_read = 0; // number of bytes read
if (*message == 0) {
return 0; // end of buffer
}
while (len && *message) { // while there's still data to read
put_user(*(message++), buf++);
len--;
bytes_read++;
}
return bytes_read; // return number of bytes read
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
int i = 0;
while (len && i < BUF_LEN - 1) { // leave space for null terminator
get_user(message[i], buf++);
len--;
i++;
}
message[i] = '\0'; // null terminate the message
return i; // return number of bytes written
}
In this example, we create two functions: my_read and my_write, allowing user-space applications to write to and read from the character device.
Conclusion
Creating and managing character device files is a critical task in Linux driver development. By understanding the roles of major and minor numbers, along with the necessary functions for character devices, you can effectively develop drivers that allow direct interaction between user-space applications and hardware.
Using tools like mdev, you can automate the creation of these device files ensuring they’re always ready when the hardware is present. With practice, creating and managing character devices will become an intuitive part of your Linux driver toolkit. Happy coding!