Linux下杂项设备驱动的编写
文章目录
- 一 什么是杂项设备
- 优点
- 缺点
- 二 杂项设备相关接口
- 1. 描述杂项设备的结构体
- 2. 杂项设备的注册与卸载
一 什么是杂项设备
杂项设备驱动(Miscellaneous Device Drivers)是 Linux 内核中一种用于处理那些不能简单归到已明确定义的标准设备类型(如字符设备、块设备、网络设备等)的简单设备的驱动机制。
优点
- 节省主设备号资源:杂项设备的主设备号固定为10,系统中的所有杂项设备都共享这一主设备号,通过不同的次设备号来区分各个具体的设备。相比普通字符设备,无论其主设备号是静态分配还是动态分配,都会消耗一个独立的主设备号,杂项设备避免了主设备号的浪费,尤其在系统中有大量简单设备需要驱动时,能显著节省内核的设备号资源。
- 使用便捷:对于功能相对简单的设备,开发杂项设备驱动比普通字符设备驱动更简单。使用普通字符设备驱动时,若要导出操作接口给用户空间,需注册字符驱动并创建字符设备类以在
/dev
下生成设备节点;而杂项设备驱动只需将设备的基本信息通过struct miscdevice
结构体传递给misc_register
函数即可完成注册,自动在/dev
目录下生成设备节点,方便用户空间程序访问设备。
缺点
- 功能相对有限:杂项设备驱动框架相对简单,适用于功能单一的设备。对于复杂设备,如需要高级的中断处理、多线程并发访问、复杂的设备状态管理等功能,杂项设备驱动可能无法满足需求,需要使用更复杂的字符设备或块设备驱动框架来实现。
二 杂项设备相关接口
1. 描述杂项设备的结构体
在Linux内核源码include/linux/miscdevice.h
中
描述杂项设备的结构体
struct miscdevice {
// 用于指定杂项设备的次设备号。杂项设备共享主设备号(固定为10),通过不同的次设备号来区分各个具体的杂项设备个体。
// 可以使用MISC_DYNAMIC_MINOR这个宏来表示让内核动态分配次设备号,这样内核会自动找到一个合适的、未被使用的次设备号分配给该杂项设备;
int minor;
// 代表杂项设备的名称,这个名称会在 /dev 目录下显示为对应的设备节点名称(如果设备成功注册到内核)。
// 例如,若name被设置为 "my_misc_device",那么在 /dev 目录下就会生成名为 /dev/my_misc_device 的设备节点,用户空间的程序可以通过这个节点来访问对应的杂项设备。
const char *name;
// 内核会调用在fops指向的结构体中定义的.open函数来处理打开操作。
const struct file_operations *fops;
// 这是一个链表节点结构体,用于将所有已注册的杂项设备连接成一个链表。内核通过维护这个链表(通常称为misc_list)来管理所有的杂项设备,
struct list_head list;
// 指向该杂项设备所属的父设备结构体的指针(如果存在父设备的话)。在设备层次结构中,有些设备可能是作为另一个设备的子设备存在的,
struct device *parent;
// 指向该杂项设备自身对应的struct device结构体,它是在内核中描述设备的一个通用结构体,包含了设备的众多属性信息,
struct device *this_device;
// 便于在用户空间通过合适的接口(如sysfs文件系统)来查看和修改设备相关属性。
const struct attribute_group **groups;
// 指定设备节点的名称,在某些情况下可以和name有所不同,不过通常如果不特殊设置,它的值默认会和name一致,也就是 /dev 目录下设备节点的显示名称。
const char *nodename;
// 代表设备节点的权限模式,用于设置该杂项设备在 /dev 目录下对应的设备节点的访问权限,
// 例如规定哪些用户或用户组可以对设备进行读、写、执行等操作,其值通常是一些权限标志位的组合(如S_IRUSR表示用户可读权限、S_IWUSR表示用户可写权限等),
umode_t mode;
};
2. 杂项设备的注册与卸载
杂项设备的注册接口int misc_register(struct miscdevice *misc);
int misc_register(struct miscdevice *misc)
{
// 用于存储设备号(包括主设备号和次设备号组合而成的完整设备号)
dev_t dev;
// 用于记录函数执行过程中出现的错误码,初始化为0表示无错误
int err = 0;
// 判断是否采用动态分配次设备号的方式,通过比较传入的misc结构体中的minor成员是否等于MISC_DYNAMIC_MINOR宏定义的值来确定
bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR);
// 初始化misc结构体中的链表节点list,将其设置为空链表头,这个链表用于将该杂项设备链接到内核维护的杂项设备链表中
INIT_LIST_HEAD(&misc->list);
// 获取互斥锁misc_mtx,用于保证在多线程或多核环境下对杂项设备注册相关操作的互斥访问,避免并发冲突
mutex_lock(&misc_mtx);
// 如果采用动态分配次设备号的方式
if (is_dynamic) {
// 在misc_minors位图中查找第一个值为0的位(从低位到高位查找),返回其索引值i
// misc_minors是一个用于记录次设备号分配情况的位图,每位对应一个次设备号是否已被使用
// DYNAMIC_MINORS表示可动态分配的次设备号范围数量
int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);
// 如果找到的可用位索引超出了可动态分配的次设备号范围,说明没有可用的次设备号可分配了
if (i >= DYNAMIC_MINORS) {
// 设置错误码为 -EBUSY,表示资源繁忙(这里指次设备号资源不足)
err = -EBUSY;
// 直接跳转到out标签处,进行后续的错误处理和资源释放操作(如解锁互斥锁等)
goto out;
}
// 根据找到的可用位索引计算出实际分配的次设备号,计算方式为从最大可分配次设备号开始倒推
misc->minor = DYNAMIC_MINORS - i - 1;
// 将找到的可用位(对应刚分配的次设备号)在位图中标记为已使用(置为1)
set_bit(i, misc_minors);
} else {
// 定义一个指向struct miscdevice结构体的指针c,用于遍历已注册的杂项设备链表
struct miscdevice *c;
// 遍历内核维护的杂项设备链表misc_list,通过list_for_each_entry宏来依次获取链表中的每个杂项设备结构体指针c
list_for_each_entry(c, &misc_list, list) {
// 检查当前遍历到的已注册杂项设备c的次设备号是否和要注册的杂项设备misc的次设备号相同
if (c->minor == misc->minor) {
// 如果次设备号冲突,设置错误码为 -EBUSY,表示设备号已被占用,注册失败
err = -EBUSY;
// 跳转到out标签处进行错误处理和资源释放等操作
goto out;
}
}
}
// 根据杂项设备固定的主设备号(MISC_MAJOR)和当前要注册的杂项设备的次设备号(misc->minor)生成完整的设备号dev
dev = MKDEV(MISC_MAJOR, misc->minor);
// 创建一个与该杂项设备对应的struct device结构体实例,用于在内核中表示该设备实体
// 并关联设备类(misc_class)、父设备(misc->parent)、设备号(dev)以及设备属性组(misc->groups)等信息
// 设备节点名称会根据misc->name来设置,最终生成在 /dev 目录下对应的设备节点
misc->this_device =
device_create_with_groups(misc_class, misc->parent, dev,
misc, misc->groups, "%s", misc->name);
// 检查设备创建操作是否成功,如果返回值是一个错误指针(表示出错)
if (IS_ERR(misc->this_device)) {
// 如果采用的是动态分配次设备号的方式
if (is_dynamic) {
// 根据当前分配的次设备号计算出对应的在位图中的索引位置i
int i = DYNAMIC_MINORS - misc->minor - 1;
// 检查索引位置是否在合法的可动态分配次设备号范围内,并且确保索引非负
if (i < DYNAMIC_MINORS && i >= 0)
// 如果满足条件,将对应位(即刚分配的次设备号对应的位)在位图中清除(置为0),表示该次设备号重新变为可用状态
clear_bit(i, misc_minors);
// 将次设备号重新设置为MISC_DYNAMIC_MINOR,表示恢复到未分配的动态分配状态
misc->minor = MISC_DYNAMIC_MINOR;
}
// 获取设备创建操作返回的错误码,通过PTR_ERR宏将错误指针转换为对应的错误码值
err = PTR_ERR(misc->this_device);
// 跳转到out标签处进行后续错误处理(如解锁互斥锁等)
goto out;
}
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
// 将当前要注册的杂项设备添加到杂项设备链表misc_list的头部,这样后续注册的设备可以“覆盖”之前的默认设置(可能在某些特定的设备查找或使用逻辑中有此需求)
list_add(&misc->list, &misc_list);
out:
// 释放之前获取的互斥锁misc_mtx,确保互斥访问的资源被正确释放,使其他线程或操作可以继续进行杂项设备注册相关操作
mutex_unlock(&misc_mtx);
// 返回最终的错误码,若为0表示注册成功,小于0表示注册过程中出现错误,具体错误码对应不同的错误原因
return err;
}
杂项设备的卸载接口void misc_deregister(struct miscdevice *misc)
以下是添加注释后的 `void misc_deregister(struct miscdevice *misc)` 函数代码,注释详细说明了函数内部每一步操作的目的和作用,有助于理解杂项设备注销的具体流程:
```c
void misc_deregister(struct miscdevice *misc)
{
// 根据杂项设备的次设备号计算其在用于记录次设备号分配情况的位图(misc_minors)中的对应索引位置i
// 计算方式为从可动态分配的次设备号范围最大值(DYNAMIC_MINORS)开始倒推,以便后续进行相关位操作(如清除已使用标记等)
int i = DYNAMIC_MINORS - misc->minor - 1;
// 检查杂项设备的链表节点(misc->list)是否为空链表,如果为空则打印警告信息(通过WARN_ON宏实现),并直接返回,不进行后续注销操作。
// 正常情况下,已注册的设备在注销时其链表节点不应为空,这里的检查用于捕获可能出现的异常情况,避免后续对空链表进行非法操作。
if (WARN_ON(list_empty(&misc->list)))
return;
// 获取互斥锁(misc_mtx),用于保证在多线程或多核环境下对杂项设备注销相关操作的互斥访问,避免并发冲突,确保操作的原子性和数据一致性。
mutex_lock(&misc_mtx);
// 将当前要注销的杂项设备从内核维护的杂项设备链表(misc_list)中移除,通过调用list_del函数实现,断开与链表中其他设备的连接。
list_del(&misc->list);
// 销毁与该杂项设备对应的设备节点,通过调用device_destroy函数,传入设备类(misc_class)以及由杂项设备的主设备号(MISC_MAJOR)和次设备号(misc->minor)组成的设备号(MKDEV函数用于生成设备号)来完成操作。
// 这一步操作会清理与该设备节点相关的内核资源,如释放相关的内存空间、取消设备节点在文件系统中的注册等。
device_destroy(misc_class, MKDEV(MISC_MAJOR, misc->minor));
// 检查计算得到的索引位置i是否在合法的可动态分配次设备号范围内(即小于DYNAMIC_MINORS且大于等于0),如果满足条件,
// 则将misc_minors位图中对应位(即该杂项设备次设备号对应的位)清除(置为0),表示该次设备号重新变为可用状态,可供后续其他设备动态分配使用。
if (i < DYNAMIC_MINORS && i >= 0)
clear_bit(i, misc_minors);
// 释放之前获取的互斥锁(misc_mtx),确保互斥访问的资源被正确释放,使其他线程或操作可以继续进行杂项设备相关操作(如其他设备的注册或注销等)。
mutex_unlock(&misc_mtx);
}
```![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3f7f53a58515414b83a577675df3a0ec.gif)
## 三 简单杂项设备的驱动编写
```c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/kernel.h>
// 设备打开操作对应的回调函数
static int misc_open(struct inode* inode, struct file* file)
{
printk(KERN_INFO "misc_dev is opened\n");
// 打印调用堆栈,用于调试,查看函数是如何被调用的
dump_stack();
return 0;
}
// 设备读取操作对应的回调函数
// 当用户空间程序尝试从设备读取数据时(例如通过 read 系统调用),内核会调用该函数
static ssize_t misc_read(struct file* file, char __user* buf, size_t size, loff_t* off)
{
// 假设每次读取 10 个字节的数据(实际情况中应根据设备具体功能来确定读取的数据量)
char data[10] = "example";
int data_size = sizeof(data);
int ret;
if (size < data_size) {
// 如果用户空间传入的缓冲区大小小于我们要读取的数据大小,返回 -EINVAL 表示参数无效错误
return -EINVAL;
}
// 将数据从内核空间复制到用户空间缓冲区,使用 copy_to_user 函数进行拷贝,它会返回未成功拷贝的字节数
ret = copy_to_user(buf, data, data_size);
if (ret!= 0) {
// 如果有字节未成功拷贝,说明出现了错误,可能是用户空间缓冲区不可写等原因,返回 -EFAULT 表示地址错误
return -EFAULT;
}
printk(KERN_INFO "misc_dev is read\n");
// 返回实际读取的字节数
return data_size;
}
// 设备写入操作对应的回调函数
// 实际中要根据设备功能实现将用户空间 buf 中的数据正确写入设备等操作,如果写入过程出现错误,应返回相应的负数错误码
// 例如,如果设备不支持写入操作,可返回 -EACCES 表示权限错误等
static ssize_t misc_write(struct file* file, const char __user* buf, size_t size, loff_t* off)
{
if (size > 100) {
// 如果写入的数据长度大于我们限定的最大值(这里假设为 100 字节,实际按设备情况定),返回 -EINVAL 表示参数无效错误
return -EINVAL;
}
printk(KERN_INFO "misc_dev is written\n");
// 返回实际写入的字节数,这里简单返回传入的 size,表示写入了这么多字节(实际要根据真实写入设备的字节数来返回)
return size;
}
// 设备关闭操作对应的回调函数
// 当用户空间程序关闭之前打开的设备节点(通过 close 系统调用)时,内核会调用这个函数
static int misc_release(struct inode* inode, struct file* file)
{
return 0;
}
// 定义设备操作函数结构体,将上述定义的各个设备操作函数(open、read、write、release 等)组合在一起
//.owner 字段被设置为 THIS_MODULE,表明该操作接口所属的模块,这对于内核进行模块引用计数等相关管理很重要
static struct file_operations misc_op = {
.owner = THIS_MODULE,
.open = misc_open,
.read = misc_read,
.write = misc_write,
.release = misc_release
};
// 定义杂项设备结构体,用于描述杂项设备相关的关键信息
//.minor 字段设置为 MISC_DYNAMIC_MINOR,意味着将由内核动态分配次设备号来区分该杂项设备
//.name 字段指定设备名称为 "misc_device",这样在 /dev 目录下对应的设备节点就会以此名称显示(实际使用时用户空间程序可以通过 /dev/misc_device 来访问该设备,前提是设备注册成功)
//.fops 字段指向前面定义的 misc_op 结构体,也就是将设备的操作接口关联到这个杂项设备上
static struct miscdevice misc_dev = {
// 动态分配次设备号
.minor = MISC_DYNAMIC_MINOR,
// 杂项设备名称
.name = "misc_device",
// 文件接口集
.fops = &misc_op
};
// 内核模块的初始化函数,用 __init 标记表示它是在模块加载时执行的初始化代码
// 在函数内部,首先调用 misc_register 函数尝试将前面定义的杂项设备 misc_dev 注册到内核中
// 如果注册失败(返回值小于 0),则通过 printk 函数打印出更详细的注册失败提示信息,包括错误码对应的具体错误描述(使用 PTR_ERR 宏转换错误码为可读的错误信息),并返回 -1 表示模块初始化失败
// 若注册成功,则打印出注册成功的消息,并返回 0,表示模块初始化顺利完成,此时对应的设备节点就会出现在 /dev 目录下,可供用户空间程序访问
static int __init misc_init(void)
{
// 返回值用于判断调用是否出错
int ret = 0;
ret = misc_register(&misc_dev);
if (ret < 0) {
printk(KERN_ERR "The registration of miscellaneous devices failed: %s\n",
(char *)PTR_ERR((void *)ret));
return -1;
}
printk(KERN_INFO "The registration of miscellaneous devices sucess.\n");
return 0;
}
// 内核模块的退出函数,用 __exit 标记表示它是在模块卸载时执行的代码
// 在函数内部,调用 misc_deregister 函数来注销之前注册的杂项设备 misc_dev,释放相关的内核资源
// 然后通过 printk 函数打印出注销成功的提示信息,同样也可以添加更详细的日志信息,比如释放了哪些资源等(这里暂未详细体现)
static void __exit misc_exit(void)
{
// 注册杂项设备
misc_deregister(&misc_dev);
printk(KERN_INFO "misc_deregister suceed\n");
return;
}
// 通过 module_init 宏指定了内核模块的初始化函数为 misc_init,也就是在模块加载时会自动执行 misc_init 函数进行杂项设备的注册等初始化操作
module_init(misc_init);
// 通过 module_exit 宏指定了模块的退出函数为 misc_exit,当模块卸载时会执行 misc_exit 函数进行设备注销等清理操作
module_exit(misc_exit);
// 声明该内核模块采用的许可证类型为 GPL(通用公共许可证),这是遵循 Linux 内核开发规范的必要声明,告知内核该模块的许可相关信息
MODULE_LICENSE("GPL");