Linux设备注册是驱动开发中的核心环节,其本质是将硬件设备抽象为Linux内核可管理的设备对象,并建立与驱动的关联,从而实现用户空间对设备的访问,整个过程依托Linux设备模型展开,涉及设备号分配、设备结构体初始化、设备添加到系统模型等多个步骤,以下从设备模型基础、字符设备注册流程、其他设备类型注册及注意事项等方面详细说明。
Linux设备模型基础
Linux设备模型采用“总线(Bus)—设备(Device)—驱动(Driver)”三层架构,通过总线匹配设备与驱动,当设备注册到总线上时,驱动会通过总线提供的match函数判断是否支持该设备,若支持则调用probe函数完成设备初始化,常见的总线类型有platform总线(用于嵌入式平台设备)、PCI总线(用于PCI设备)、USB总线(用于USB设备)等,设备注册的核心是将设备信息加入内核设备链表,并暴露相应的sysfs接口,方便用户空间查看和管理。
字符设备注册详细流程
字符设备是Linux中最常见的设备类型(如串口、LED灯等),其注册流程主要包括设备号分配、cdev结构体初始化、设备添加及设备节点创建四个步骤。
设备号分配
设备号是内核识别设备的唯一标识,由主设备号(major)和次设备号(minor)组成,主设备号标识设备类型,次设备号标识同一类型下的不同设备,设备号分配分为静态分配和动态分配两种方式。
-
静态分配:通过
register_chrdev_region
函数手动指定设备号范围,适用于设备号已知且固定的场景。dev_t dev_num; int major = 100, minor = 0; int dev_count = 1; // 设备数量 int ret = register_chrdev_region(MKDEV(major, minor), dev_count, "my_char_dev"); if (ret < 0) { printk(KERN_ERR "Failed to register device numbern"); return ret; }
其中
MKDEV(major, minor)
用于将主次设备号合并为dev_t
类型,第三个参数为设备名(会在/proc/devices中显示)。 -
动态分配:通过
alloc_chrdev_region
函数让内核自动分配可用设备号,适用于设备号不确定的场景。dev_t dev_num; int ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev"); if (ret < 0) { printk(KERN_ERR "Failed to allocate device numbern"); return ret; } major = MAJOR(dev_num); // 获取分配的主设备号 minor = MINOR(dev_num); // 获取分配的次设备号
设备号分配函数对比:
| 函数名 | 功能 | 参数说明 | 返回值 |
|———————-|————————–|————————————————————————–|—————–|
| register_chrdev_region | 静态分配设备号 | dev_t from(起始设备号)、count(设备数量)、const char name(设备名) | 成功0,失败负错误码 |
| alloc_chrdev_region | 动态分配设备号 | dev_t dev(输出分配的设备号)、int baseminor(起始次设备号)、count、name | 成功0,失败负错误码 |
cdev结构体初始化与添加
cdev(character device)结构体是字符设备的核心,用于描述字符设备的属性和操作方法,注册设备前需初始化cdev并关联文件操作集合(file_operations
)。
-
初始化cdev:
struct cdev my_cdev; my_cdev.owner = THIS_MODULE; // 模块所属,防止模块卸载后驱动仍被调用 cdev_init(&my_cdev, &fops); // fops为file_operations结构体,定义设备的读写等操作
file_operations
是关键结构体,需实现read
、write
、open
、release
等成员函数,const struct file_operations fops = { .owner = THIS_MODULE, .read = my_dev_read, // 读函数 .write = my_dev_write, // 写函数 .open = my_dev_open, // 打开函数 .release = my_dev_release, // 释放函数 };
-
添加cdev到系统:
使用cdev_add
函数将初始化后的cdev添加到内核设备链表,完成设备注册。ret = cdev_add(&my_cdev, MKDEV(major, minor), 1); if (ret < 0) { unregister_chrdev_region(MKDEV(major, minor), 1); // 失败时释放设备号 printk(KERN_ERR "Failed to add cdevn"); return ret; }
创建设备类与设备节点
设备注册后,需在sysfs中创建设备类(class)和设备(device)对象,并自动生成设备节点(如/dev/my_char_dev),供用户空间访问。
-
创建设备类:设备类是sysfs中设备的逻辑集合,用于管理同类设备。
struct class *my_class = class_create(THIS_MODULE, "my_char_class"); if (IS_ERR(my_class)) { cdev_del(&my_cdev); // 失败时删除cdev unregister_chrdev_region(MKDEV(major, minor), 1); printk(KERN_ERR "Failed to create classn"); return PTR_ERR(my_class); }
-
创建设备对象:设备对象代表具体设备,关联设备号和设备类。
struct device *my_device = device_create(my_class, NULL, MKDEV(major, minor), NULL, "my_char_dev"); if (IS_ERR(my_device)) { class_destroy(my_class); // 失败时销毁类 cdev_del(&my_cdev); unregister_chrdev_region(MKDEV(major, minor), 1); printk(KERN_ERR "Failed to create devicen"); return PTR_ERR(my_device); }
执行成功后,
/dev
目录下会生成设备节点,用户可通过open
、read
等系统调用操作设备。
其他设备类型注册简介
除字符设备外,Linux还支持块设备(如硬盘)和网络设备,其注册流程与字符设备类似,但涉及不同的内核接口。
-
块设备注册:通过
alloc_blkdev_region
分配设备号,初始化gendisk
结构体(描述块设备属性),调用add_disk
将设备添加到系统,块设备支持随机访问,需实现make_request
等操作函数。 -
平台设备注册:用于嵌入式系统中的平台设备(如I2C、SPI设备),通过
platform_device_register
注册,需定义platform_device
结构体,包含设备名称、资源(地址、中断号)等信息,驱动通过platform_driver_register
注册,match函数通过设备名称与设备匹配。 -
PCI设备注册:内核已实现PCI总线驱动,设备注册由PCI控制器自动完成,驱动只需调用
pci_register_driver
注册PCI驱动,通过pci_device_id
表匹配设备。
设备注销流程
设备注销是注册的逆过程,需释放已分配的资源,避免内存泄漏,顺序与注册相反:先删除设备对象和类,再删除cdev,最后释放设备号。
void __exit my_dev_exit(void) { device_destroy(my_class, MKDEV(major, minor)); // 删除设备对象 class_destroy(my_class); // 销毁设备类 cdev_del(&my_cdev); // 删除cdev unregister_chrdev_region(MKDEV(major, minor), 1); // 释放设备号 } module_init(my_dev_init); module_exit(my_dev_exit);
代码示例与注意事项
以下是一个简单的字符设备注册模板(关键步骤):
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #define DEVICE_NAME "my_char_dev" #define CLASS_NAME "my_class" static dev_t dev_num; static struct cdev my_cdev; static struct class *my_class; static int my_dev_open(struct inode *inode, struct file *file) { printk(KERN_INFO "Device openedn"); return 0; } static ssize_t my_dev_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) { printk(KERN_INFO "Device readn"); return 0; } static const struct file_operations fops = { .owner = THIS_MODULE, .open = my_dev_open, .read = my_dev_read, }; static int __init my_dev_init(void) { // 动态分配设备号 if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) { printk(KERN_ERR "Failed to allocate device numbern"); return -1; } // 初始化cdev cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE; // 添加cdev if (cdev_add(&my_cdev, dev_num, 1) < 0) { unregister_chrdev_region(dev_num, 1); printk(KERN_ERR "Failed to add cdevn"); return -1; } // 创建类和设备 my_class = class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(my_class)) { cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_class); } device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME); printk(KERN_INFO "Device registered successfullyn"); return 0; } static void __exit my_dev_exit(void) { device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO "Device unregisteredn"); } module_init(my_dev_init); module_exit(my_dev_exit); MODULE_LICENSE("GPL");
注意事项:
- 错误处理:每一步注册操作都可能失败,需检查返回值并释放已分配资源(如设备号、cdev等)。
- 并发安全:设备操作函数(如
read
、write
)可能被多进程并发调用,需使用互斥锁(mutex
)或自旋锁(spinlock
)保护共享数据。 - 设备号管理:静态分配需确保设备号未被占用(可通过
cat /proc/devices
查看),动态分配需处理分配失败的情况。 - 模块引用计数:
file_operations
中的owner
必须设置为THIS_MODULE
,防止模块卸载后驱动函数仍被调用。
相关问答FAQs
Q1:为什么设备注册后无法在/dev下看到设备文件?
A:可能原因包括:
- 未创建设备类或设备对象:检查是否调用了
class_create
和device_create
,且返回值是否成功。 - udev服务未运行:设备节点的创建依赖udev(或systemd-udevd),需确保服务正在运行(
systemctl status udev
)。 - 设备名称冲突:
device_create
中的设备名可能与已有设备重复,导致创建失败。 - 模块未加载:设备注册在模块初始化时执行,需确保模块已成功加载(
lsmod
查看)。
Q2:设备号分配失败(alloc_chrdev_region
返回负值)如何解决?
A:设备号分配失败通常是因为系统中可用设备号不足,解决方法:
- 检查当前设备号使用情况:
cat /proc/devices
查看已分配的主设备号,避免冲突。 - 扩展设备号范围:动态分配时,
baseminor
参数可指定起始次设备号(通常为0),若失败可尝试调整count
(设备数量)或更换设备名。 - 释放闲置设备号:若某些设备号已被分配但不再使用,可检查对应驱动是否正确卸载,必要时手动释放(需谨慎操作)。
- 使用静态分配:若动态分配持续失败,可尝试手动指定未使用的主设备号(如
cat /proc/devices
中的空闲号)。
原创文章,发布者:酷番叔,转转请注明出处:https://cloud.kd.cn/ask/36203.html