Linux驱动---sys文件系统自动添加设备节点、添加调试节点以及MISC设备驱动的解释

📅 发布时间:2026/8/6 12:43:47
Linux驱动---sys文件系统自动添加设备节点、添加调试节点以及MISC设备驱动的解释
目录一. sys 文件系统1.1 sys 文件系统介绍1.2 sys 文件系统的作用二. 自动创建设备节点2.1 创建和删除类2.1.1 创建类2.1.2 几个检查错误的函数1、ERR_PTR2、PTR_ERR3、IS_ERR2.1.3 删除类2.2 创建与删除设备节点2.2.1 创建设备节点2.2.2 删除设备节点三. 加入调试节点3.1 创建设备类与设备节点3.2 调用 device_create_file 函数创建设备的属性3.3 使用 __ATTR 填充属性结构体3.4 回收调试节点四. MISC 设备驱动4.1 设备类型的概念4.2 MISC 设备驱动介绍4.3 struct miscdevice结构体及其注册4.4 优势分析五. 注意事项5.1 区分 cdev 与 device5.2 内核层与用户层数据传递的三种方法5.3 Makefile一. sys 文件系统在之前的 LED 字符设备驱动中需使用命令“mknod /dev/led0 c 248 0”手动增加字符设备节点本节介绍如何自动创建设备节点1.1 sys 文件系统介绍sys目录系统启动以后此目录作为sysfs 文件系统的挂载点sysfs 是一个基于 ram 的文件系统也就是说它没有实际的存储设备。此目录是系统设备管理的重要目录通过一定的组织结构向用户提供详细的内核数据结构信息sysfs是Linux内核中一种特殊虚拟文件系统用来向用户空间提供内核设备和驱动程序的信息。它提供了一种统一的接口用于查看和操作设备、驱动程序、文件系统等内核对象。sysfs存放在内存中挂载在/sys目录下文件系统中的文件不对应硬盘上任何文件sys 目录中的文件如下所示/sys/devices表示系统中的物理设备每个子目录对应一个物理设备1.系统设备如CPU、系统内存等2.总线设备如PCI、 USB、SCSI等设备3.虚拟设备如虚拟网络设备/sys/bus表示系统总线类型(如PCIUSB等)每个子目录对应一个总线1.devices列出所有连接到该总线的设备2.drivers列出与该总线相关的所有驱动 程序3.drivers_autoprobe和drivers_probe 用于自动或手动驱动程序绑定4.uevent用于触发uevent事件/sys/class表示系统中的设备类型子目录按照设备类型分类1.net表示所有网络接口2.block表示所有块设备3.tty表示所有终端设备/sys/kernel表示内核参数和信息如调度器参数内核模块/sys/module表示加载的内核模块每个子目录对应一个模块/sys/block表示块设备如硬盘、 USB存储设备等1.2 sys 文件系统的作用sysfs在驱动中的作用通常包括1. 设备信息显示将内核对象设备、驱动、总线等的属性以文件形式展示在/sys目录下例如设备状态、配置参数、硬件拓扑等。用户可通过cat或者echo 命令直接读取/修改这些文件无需编写专用工具。2. 动态设备管理实时反映系统设备变化如热插拔目录结构随设备增删动态更新支持设备分类3. 驱动调试与配置提供寄存器访问接口允许直接读取/写入硬件寄存器4. 自动创建设备节点函数 class_create、函数 class_destroy、函数 device_create、函数 device_destroy首先介绍自动创建设备节点这个作用二. 自动创建设备节点2.1 创建和删除类2.1.1 创建类自动创建设备节点的工作是在驱动程序的入口函数中完成的一般在cdev_add 函数后面添 加自动创建设备节点相关代码。首先要创建一个class 类class 是个结构体定义在文件inclu de/linux/device.h里面。class_create 是类创建函数class_create 是个宏定义内容如下#define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, __key); \ }) struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key)根据上述代码将宏 class_create 展开以后内容如下作用创建一个类参数参数 owner 一般为THIS_MODULE参数 name 是类名字返回值是个指向结构体 class 的指针也就是创建的类使用示例注意要对返回值进行检查函数头文件为device.h//指向创建的类 static struct class *pclass NULL; //创建类 pclass class_create(THIS_MODULE, LED); if(IS_ERR(pclass)) { ret PTR_ERR(pclass); pr_err(class_create error, errno:%d\n, ret); return ret; }2.1.2 几个检查错误的函数1、ERR_PTR作用将负数错误码强制转换成指针类型2、PTR_ERR作用把错误指针还原成原本的负数错误码3、IS_ERR作用判断一个指针是不是错误封装指针返回true指针并非有效内存地址返回false正常合法指针可以安全解引用使用static inline void * __must_check ERR_PTR(long error) { return (void *) error; } static inline long __must_check PTR_ERR(__force const void *ptr) { return (long) ptr; } static inline bool __must_check IS_ERR(__force const void *ptr) { return IS_ERR_VALUE((unsigned long)ptr); }2.1.3 删除类卸载驱动程序的时候需要删除掉类类删除函数为 class_destroy函数原型如下参数 cls 就是要删除的类使用示例//回收类 class_destroy(pclass);2.2 创建与删除设备节点2.2.1 创建设备节点上一小节创建好类以后还不能实现自动创建设备节点还需要在这个类下创建一个设备节点。使用device_create 函数在类下面创建设备节点device_create 函数原型如下device_create 是个可变参数函数参数 class就是设备要创建哪个类下面参数 parent是父设备一般为 NULL也就是没有父设备参数 devt是设备号参数 drvdata是设备可能会使用的一些数据一般为 NULL参数 fmt是设备名字如果设置 fmtxxx 的话就会生成/dev/x xx 这个设备文件返回值就是创建好的设备使用示例注意参数四的名称会在 /dev/xxx 这个目录下出现也就是创建的设备节点的名称返回值需要判断//指向创建的设备节点 static struct device * pdevice NULL; //创建设备节点/dev/xxx pdevice device_create(pclass, NULL, dev_no, NULL, myled0); if(IS_ERR(pdevice)) { ret PTR_ERR(pdevice); pr_err(device_create error, errno: %d\n, ret); return ret; }这样就无需手动创建设备节点insmod 之后会自动创建设备节点到 /dev/xxx 下2.2.2 删除设备节点卸载驱动的时候需要删除掉创建的设备设备删除函数为 device_destroy函数原型如下参数 class是要删除的设备所处的类参数 devt是要删除的设备号。使用示例//回收设备节点 device_destroy(pclass, dev_no);三. 加入调试节点通过sys文件系统加入调试节点目的不需要通过编程查看sys下的文件就可以获得灯蜂鸣器的状态信息。同样也可以通过写入来改变灯蜂鸣器等的状态具体步骤如下3.1 创建设备类与设备节点//指向创建的类 static struct class *pclass NULL; //指向创建的设备节点 static struct device * pdevice NULL; //创建类 pclass class_create(THIS_MODULE, LED); if(IS_ERR(pclass)) { ret PTR_ERR(pclass); pr_err(class_create error, errno:%d\n, ret); return ret; } //创建设备节点/dev/xxx pdevice device_create(pclass, NULL, dev_no, NULL, myled0); if(IS_ERR(pdevice)) { ret PTR_ERR(pdevice); pr_err(device_create error, errno: %d\n, ret); return ret; }3.2 调用 device_create_file 函数创建设备的属性device_create_file 函数原型如下参数dev为指向设备的指针参数attr为指向设备属性结构体的指针返回值0为正非零为错/** * device_create_file - create sysfs attribute file for device. * dev: device. * attr: device attribute descriptor. */ int device_create_file(struct device *dev, const struct device_attribute *attr) { int error 0; if (dev) { WARN(((attr-attr.mode S_IWUGO) !attr-store), Attribute %s: write permission without store\n, attr-attr.name); WARN(((attr-attr.mode S_IRUGO) !attr-show), Attribute %s: read permission without show\n, attr-attr.name); error sysfs_create_file(dev-kobj, attr-attr); } return error; }struct device_attribute 结构体声明如下struct attribute { const char *name; umode_t mode; #ifdef CONFIG_DEBUG_LOCK_ALLOC bool ignore_lockdep:1; struct lock_class_key *key; struct lock_class_key skey; #endif }; struct device_attribute { struct attribute attr; ssize_t (*show)(struct device *dev, struct device_attribute *attr, char *buf); ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf, size_t count); };3.3 使用 __ATTR 填充属性结构体使用带参宏 _ATTR填充属性结构体。属性的名字(直接写str不用引号)读写权限读函数写函数宏的定义如下注意1、内核权限不能是 0666只能是 06642、show 与 store 函数需要自己实现这两个函数的返回值与参数都需要注意下面的使用示例有详细说明show的返回值需要返回值输出字符串的长度。store 的返回值要返回该函数形参的最后一个参数count否则会阻塞#define __ATTR(_name, _mode, _show, _store) { .attr {.name __stringify(_name), .mode VERIFY_OCTAL_PERMISSIONS(_mode) }, .show _show, .store _store, }3.4 回收调试节点使用函数device_remove_filevoid device_remove_file(struct device *dev, const struct device_attribute *attr)使用示例:static ssize_t myshow(struct device *dev, struct device_attribute *attr,char *buf); static ssize_t mystore(struct device *dev, struct device_attribute *attr,const char *buf, size_t count); //设备属性结构体 static struct device_attribute attr __ATTR(led_status, 0664, myshow, mystore); /** * brief 在命令行对设备属性进行读取时候调用此函数-读属性 * * param dev 本设备 * param attr 本设备的属性结构体暂时不使用 * param buf 返回给用户层命令行的字符串信息 * return ssize_t buf的字符串长度 */ static ssize_t myshow(struct device *dev, struct device_attribute *attr,char *buf) { ssize_t len 0; if(led_status LED_ON) { strcpy(buf, LED_ON\n); } else if(led_status LED_OFF) { strcpy(buf, LED_OFF\n); } len strlen(buf); return len; } /** * brief 当命令行向该属性写入指令时会调用此函数-写属性 * * param dev * param attr * param buf 命令行写入本属性的指令字符串 * param count 命令行的长度 * return ssize_t 返回count */ static ssize_t mystore(struct device *dev, struct device_attribute *attr,const char *buf, size_t count) { u32 v 0; v readl(pdata); if(0 strcmp(buf, 1\n)) //1:led on { v ~(1 3); writel(v, pdata); led_status LED_ON; } else if(0 strcmp(buf, 0\n)) //0:led off { v | (1 3); writel(v, pdata); led_status LED_OFF; } return count; }这样在insmod之后不仅会自动在/dev/xxx下创建设备节点而且在/sys/devices/virtual/LED/myled0会生成一个调试节点可以使用命令行指令对设备的状态进行读取和改变如下所示四. MISC 设备驱动4.1 设备类型的概念在Linux系统中通过 cat /proc/devices查看系统中所有加载的驱动字符设备驱动如下块设备驱动如下4.2 MISC 设备驱动介绍所有的 MISC 设备驱动的主设备号都为 10不同的设备使用不同的从设备号。随着 Linux 字符设备驱动的不断增加设备号变得越来越紧张尤其是主设备号MISC 设备驱动就用于解决此问题。MISC 设备会自动创建 cdev不需要手动创建同样也会自动创建设备节点不需要 mknod因此采用 MISC 设备驱动可以简化字符设备驱动的编写4.3 struct miscdevice结构体及其注册需要向 Linux 注册一个 miscdevice 设备miscdev ice 是一个结构体定义在文件 include/linux/miscdevice.h 中内容如下struct miscdevice { int minor; //子设备号 const char *name; //设备名称 const struct file_operations *fops; //驱动文件操作函数集合 struct list_head list; struct device *parent; struct device *this_device; const struct attribute_group **groups; const char *nodename; umode_t mode; };定义一个 MISC 设备(miscdevice 类型)以后需要设置minor、name 和 fops这三个成员变量。minor 表示子设备号MISC 设备的主设备号为 10这个是固定的需要用户指定子设备号一般使用宏MISC_DYNAMIC_MINOR来定义name 就是此 MISC 设备名字当此设备注册成功以后就会在/dev 目录下生成一个名为 nam e 的设备文件。fops 就是字符设备的操作集合MISC 设备驱动最终是需要使用用户提供的 fops 操作集合初始化miscdevice 类型示例#define DEV_NAME myled0 //杂项设备结构体 static struct miscdevice misc_dev { .minor MISC_DYNAMIC_MINOR, //这个宏为255但是次设备号不一定是255;自动获取次设备号 .name DEV_NAME, //设备节点名 和 /sys/class/misc/xxx 两者名称相同 .fops fops //杂项设备文件操作函数集合 };当设置好 miscdevice 以后就需要使用 misc_register 函数向系统中注册一个 MISC 设备此 函数原型如下函数参数和返回值含义如下misc要注册的 MISC 设备。返回值负数失败0成功。使用示例//注册杂项设备 ret misc_register(misc_dev); if(ret) { pr_err(misc_register error, errno:%d\n, ret); return ret; }当卸载设备驱动模块的时候需要调用 misc_deregister 函数来注销掉 MISC 设备函数原型如下函数参数和返回值含义如下misc要注销的 MISC 设备。返回值负数失败0成功4.4 优势分析以前需要调用一堆的函数去创建设备比如在以前的字符设备驱动中会使用如下几个函数完成设备创建过程alloc_chrdev_region(); /* 申请设备号 */ cdev_init(); /* 初始化 cdev */ cdev_add(); /* 添加 cdev */ class_create(); /* 创建类 */ device_create(); /* 创建设备 */以前注销设备驱动的时候需要调用一堆的函数去删除此前创建的 cdev、设备等等内容如下所示cdev_del(); /* 删除 cdev */ unregister_chrdev_region(); /* 注销设备号 */ device_destroy(); /* 删除设备 */ class_destroy(); /* 删除类 */而现在只需要misc_register 函数与misc_deregister 函数这两个函数即可完成设备查看五. 注意事项5.1 区分 cdev 与 device5.2 内核层与用户层数据传递的三种方法1、mmap 内存映射2、ioctnl 控制交互3、copy_to_user 与 copy_from_user 数据传递5.3 Makefile:直接赋值不会再变化了不会追加延时赋值$(MAKE)带参数的make可以进行参数传递-C进入相应的目录进入源码目录M表示模块在等号后面的路径存放