Overview
Character devices allow byte-by-byte communication between user-space applications and kernel drivers. They are commonly used for devices like serial ports, sensors, and custom hardware interfaces. The Linux kernel provides mechanisms for registering, managing, and interacting with character devices via a device file in /dev
.
Registering a Character Device
To register a character device, the driver needs to:
1. Allocate a Major and Minor Number:
- Each character device is identified by a major number (device type) and a minor number (specific device). The major number indicates the driver associated with the device, while the minor number is used to differentiate between multiple devices handled by the same driver. If major and minor numbers are repeated, it can cause conflicts and lead to incorrect device identification. To avoid this, the kernel provides
alloc_chrdev_region
, a function to dynamically allocate major and minor numbers, ensuring uniqueness. These numbers are used in the/dev
directory to associate device files with their corresponding drivers. - Use
alloc_chrdev_region
to dynamically allocate a major number.
dev_t dev;
int result;
// kernel/fs/char_dev.c
// int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
result = alloc_chrdev_region(&dev, 0, 1, "my_char_device");
if (result < 0) {
pr_err("Failed to allocate major number\n");
return result;
}
pr_info("Device registered with major %d, minor %d\n", MAJOR(dev), MINOR(dev));
2. Initialize and Register the Device:
- Define a
cdev
structure and initialize it with file operations. - Use
cdev_add
to register the device with the kernel.
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
result = cdev_add(&my_cdev, dev, 1);
if (result < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(dev, 1);
return result;
}
3. Create a Device File (Optional):
- Creating a device file in
/dev
is optional because character devices can be accessed directly using their major and minor numbers through system calls or user-space libraries, bypassing the need for a device file. However, creating a file in/dev
makes interaction more user-friendly by providing a standard interface. - To interact with a character device without creating a device file, you can use system calls like
mknod
to create a temporary device node or interact with the device directly using its major and minor numbers programmatically. - Use
class_create
anddevice_create
to automatically create a device file in/dev
.
struct class *my_class;
my_class = class_create(THIS_MODULE, "my_device_class");
if (IS_ERR(my_class)) {
pr_err("Failed to create class\n");
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, dev, NULL, "my_char_device");
File Operations
Character devices are controlled through a set of file operations defined in a struct file_operations
. These operations determine how the device responds to system calls like open
, read
, write
, and ioctl
.
1. Define File Operations:
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
char data[] = "Hello from kernel!\n";
size_t datalen = strlen(data);
if (*offset >= datalen)
return 0;
if (len > datalen - *offset)
len = datalen - *offset;
if (copy_to_user(buf, data + *offset, len))
return -EFAULT;
*offset += len;
return len;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
char kbuf[128];
if (len > sizeof(kbuf) - 1)
len = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, buf, len))
return -EFAULT;
kbuf[len] = '\0';
pr_info("Data from user: %s\n", kbuf);
return len;
}
static int my_open(struct inode *inode, struct file *file) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file) {
pr_info("Device closed\n");
return 0;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
.open = my_open,
.release = my_release,
};
2. Explanation of Generic Operations:
open
: Called when the device is accessed usingopen()
. Used for initialization.release
: Called when the device is closed.read
: Transfers data from the kernel to user-space.write
: Transfers data from user-space to the kernel.
Cleaning Up
When unloading the driver, release allocated resources:
static void __exit my_exit(void) {
device_destroy(my_class, dev);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
pr_info("Character device unregistered\n");
}
module_exit(my_exit);
Example User-Space Interaction
Write a user-space program to interact with the character device:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[128];
fd = open("/dev/my_char_device", O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return -1;
}
// Write to device
write(fd, "Hello, kernel!", 14);
// Read from device
read(fd, buffer, sizeof(buffer));
printf("Data from device: %s\n", buffer);
close(fd);
return 0;
}
Best Practices
- Error Handling:
- Validate user input and handle edge cases.
- Ensure proper cleanup in case of initialization failures.
- Use Helper Functions:
- Use
alloc_chrdev_region
instead of hardcoding major numbers. copy_to_user
andcopy_from_user
are essential for secure data exchange between kernel space and user space. These functions ensure that the kernel does not directly access user-space memory, which could lead to security risks and undefined behavior due to memory access violations. Instead, they provide controlled, validated mechanisms for copying data. Alternatively, functions likememdup_user
can be used when copying large blocks of data into kernel space. These methods ensure safe interaction between different memory domains, protecting system stability.
- Use
- Device Permissions:
- Set appropriate file permissions on the device file to avoid unauthorized access.
- Documentation:
- Provide clear documentation for user-space developers on how to interact with the device.