在 Linux 内核中,字符设备是一种常见的设备类型,广泛用于与用户空间进行数据交互。以下是详细的步骤,教你如何编写一个简单的 Linux 字符设备驱动,并在系统中生成对应的设备文件节点。
1. 字符设备驱动的基本框架
一个基本的字符设备驱动需要以下几个关键部分:
- 设备号的分配和注册
- 字符设备结构的初始化和注册
- 文件操作(file_operations)的实现
- 设备节点的自动或手动创建
- 模块的初始化和退出函数
以下是一个完整的示例代码,逐步讲解每个部分。
2. 示例代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_char_class"
static int major; // 主设备号
static struct cdev my_cdev; // 字符设备结构
static struct class *my_class; // 设备类
static struct device *my_device; // 设备
// 打开设备
static int my_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device opened\n");
return 0;
}
// 关闭设备
static int my_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device closed\n");
return 0;
}
// 读取设备
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off)
{
char *data = "Hello from kernel\n";
size_t data_len = strlen(data);
if (*off >= data_len)
return 0; // 已读取到文件末尾
if (len > data_len - *off)
len = data_len - *off;
if (copy_to_user(buf, data + *off, len))
return -EFAULT;
*off += len;
return len;
}
// 写入设备
static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *off)
{
char kbuf[128];
if (len > sizeof(kbuf) - 1)
len = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, buf, len))
return -EFAULT;
kbuf[len] = '\0';
printk(KERN_INFO "Received: %s\n", kbuf);
return len;
}
// 文件操作结构体
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
// 模块初始化函数
static int __init my_init(void)
{
dev_t dev;
int ret;
printk(KERN_INFO "Initializing the character device driver\n");
// 动态分配设备号
ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev);
// 初始化字符设备
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
// 注册字符设备
ret = cdev_add(&my_cdev, dev, 1);
if (ret < 0) {
unregister_chrdev_region(dev, 1);
printk(KERN_ERR "Failed to register character device\n");
return ret;
}
// 创建设备类
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_ERR "Failed to create device class\n");
return PTR_ERR(my_class);
}
// 创建设备节点
my_device = device_create(my_class, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_ERR "Failed to create device\n");
return PTR_ERR(my_device);
}
printk(KERN_INFO "Device registered with major number %d\n", major);
return 0;
}
// 模块退出函数
static void __exit my_exit(void)
{
dev_t dev = MKDEV(major, 0);
printk(KERN_INFO "Removing the character device driver\n");
// 销毁设备节点
device_destroy(my_class, dev);
// 销毁设备类
class_destroy(my_class);
// 删除字符设备
cdev_del(&my_cdev);
// 释放设备号
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "Device driver removed\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
3. 代码说明
3.1 设备号的分配
- 使用
alloc_chrdev_region()
动态分配设备号,主设备号存储在major
中。 - 也可以使用
register_chrdev_region()
手动指定设备号,但动态分配更灵活。
3.2 字符设备的初始化和注册
- 使用
cdev_init()
初始化字符设备结构体my_cdev
,并绑定文件操作fops
。 - 使用
cdev_add()
将字符设备注册到内核。
3.3 文件操作的实现
my_open()
和my_release()
是设备打开和关闭的回调函数。my_read()
实现从内核向用户空间读取数据,使用copy_to_user()
传递数据。my_write()
实现从用户空间向内核写入数据,使用copy_from_user()
接收数据。
3.4 设备节点的自动创建
- 使用
class_create()
创建设备类。 - 使用
device_create()
创建设备节点,设备节点会自动出现在/dev/
目录下(例如/dev/my_char_dev
)。 - 设备节点依赖于
udev
或mdev
,确保系统支持自动创建设备节点。
3.5 模块的初始化和退出
- 在
my_init()
中完成设备号分配、字符设备注册、设备节点创建等操作。 - 在
my_exit()
中清理资源,包括销毁设备节点、设备类、字符设备和释放设备号。
4. 编译和测试
4.1 编写 Makefile
obj-m += my_char_dev.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
4.2 编译和加载模块
-
编译模块:
make
编译后会生成
my_char_dev.ko
文件。 -
加载模块:
sudo insmod my_char_dev.ko
-
查看设备节点:
ls /dev/my_char_dev
如果一切正常,设备节点
/dev/my_char_dev
应该已经创建。 -
查看内核日志:
dmesg | tail
4.3 测试设备
-
读取设备:
cat /dev/my_char_dev
输出:
Hello from kernel
。 -
写入设备:
echo "Test message" > /dev/my_char_dev
查看内核日志(
dmesg
),可以看到写入的内容。 -
卸载模块:
sudo rmmod my_char_dev
5. 注意事项
-
权限问题:
- 设备节点默认权限可能需要调整,可以使用
chmod
修改:sudo chmod 666 /dev/my_char_dev
- 或者在代码中通过
device_create()
的参数设置权限。
- 设备节点默认权限可能需要调整,可以使用
-
调试:
- 使用
printk()
输出调试信息,查看内核日志(dmesg
)。 - 如果设备节点未创建,检查
udev
或mdev
是否正常工作。
- 使用
-
错误处理:
- 确保在初始化失败时正确清理资源,避免内存泄漏或设备号冲突。
-
内核版本兼容性:
- 不同内核版本的 API 可能有变化,建议参考内核文档或使用
compat.h
处理兼容性问题。
- 不同内核版本的 API 可能有变化,建议参考内核文档或使用
6. 手动创建设备节点(可选)
如果 device_create()
无法自动创建设备节点,可以手动创建:
-
查看主设备号:
cat /proc/devices | grep my_char_dev
假设主设备号为
250
。 -
手动创建节点:
sudo mknod /dev/my_char_dev c 250 0 sudo chmod 666 /dev/my_char_dev
7. 总结
通过以上步骤,你可以:
- 编写一个简单的字符设备驱动。
- 实现基本的读写功能。
- 自动或手动创建设备节点。
- 测试设备的读写功能。