2026/5/26 19:09:53
网站建设
项目流程
阜阳网站建设阜阳,wordpress 仿凡客,镇江网站排名优化,网站访问大小Linux设备模型笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正#xff01;#xff01;#xff01;在开始之前先讲一下一级指针#xff0c;二级指针和指针数组。举例#xff0c;一级指针是 in…Linux设备模型笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正在开始之前先讲一下一级指针二级指针和指针数组。举例一级指针是 int a10; int *pa; 那么对于p里面存的就是变量a的地址*p则是解引用得到a的值为10。举例指针数组和二级指针有如下代码先不需要管细节理解是咋样就好#include stdio.h #include 一些其他的头文件 struct attribute { const char *name; int mode; }; int main() { // 1. 定义两个属性对象 struct attribute attr1 {version, 0444}; struct attribute attr2 {max_devs, 0444}; // 2. 指针数组一级指针的数组以 NULL 结尾 struct attribute *attr_arr[] {attr1, attr2, NULL}; // 3. 二级指针指向指针数组的首地址 struct attribute **pp attr_arr; // 遍历判断二级指针指向的一级指针是否为 NULL while (*pp ! NULL) { // 解引用二级指针拿到一级指针的值判断是否为 NULL printf(属性名%s权限%o\n, (*pp)-name, (*pp)-mode); pp; // 二级指针移动到下一个数组元素 } return 0; }我们可以看到attr_arr是一个存储多个一级指针的一级指针数组。指针数组的结束都放NULL。数组的名字是第一个数组元素的地址下标为0而这个元素是个一级指针又因为二级指针是一个指向一级指针的指针所以指针数组可以隐式转换为二级指针。然后下方把指针数组赋值给二级指针这个二级指针pp存的是指针数组第一个元素的地址然后*pp对二级指针解引用得到的就是指针数组的第一个首元素。后续出现指针的操作这个操作是根据指针的类型大小占据的字节进行。例如在代码中pp之后会指向指针数组第2个元素下标为1因为pp的时候加的是struct attribute类型大小占据的字节所以会跳到下一个数组元素。Linux设备模型Linux设备模型用来管理所有硬件设备、驱动程序并建立它们之间的关联关系同时对外用户空间提供标准化的交互接口如 sysfs。关键组成是总线-驱动-设备。作用有管理硬件与驱动的生命周期抽象硬件的共性简化驱动开发向用户空间开放标准化接口实现热插拔在根文件系统中有个/sys文件目录里面记录各个设备之间的关系。/sys/bus目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目录结构 每个子目录(总线类型)下包含两个子目录——devices和drivers文件夹其中devices下是该总线类型下的所有设备 而这些设备都是符号链接它们分别指向真正的设备(/sys/devices/下)。而drivers下是所有注册在这个总线上的驱动每个driver子目录下 是一些可以观察和修改的driver参数。/sys/devices目录下是全局设备结构体系包含所有被发现的注册在各种总线上的各种物理设备。一般来说 所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices是内核对系统中所有设备的分层次表达模型 也是/sys文件系统管理设备的最重要的目录结构。/sys/class目录下则是包含所有注册在kernel里面的设备类型这是按照设备功能分类的设备模型 我们知道每种设备都具有自己特定的功能比如鼠标的功能是作为人机交互的输入按照设备功能分类无论它 挂载在哪条总线上都是归类到/sys/class/input下。在总线上管理着两个链表分别管理着设备和驱动当我们向系统注册一个驱动时便会向驱动的管理链表插入我们的新驱动 同样当我们向系统注册一个设备时便会向设备的管理链表插入我们的新设备。在插入的同时总线会执行一个bus_type结构体中match的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字存在名字相同的设备/驱动便成功匹配)。 在匹配成功的时候会调用驱动device_driver结构体中probe方法(通常在probe中获取设备资源具体的功能可由驱动编写人员自定义) 并且在移除设备或驱动时会调用device_driver结构体中remove方法。我要开始讲废话了这里干看看不懂可以往下翻直奔代码部分看完代码再回来看。在内核中使用结构体bus_type来表示总线如下所示1、name :指定总线的名称当新注册一种总线类型时会在/sys/bus目录创建一个新的目录目录名就是该参数的值2、drv_groups、dev_groups、bus_groups :分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys目录下在以文件的形式存在对于驱动而言在目录/sys/bus/bus-name/driver/driver-name存放了设备的默认属性设备则在目录/sys/bus/bus-name/devices/driver-name中。这些文件一般是可读写的用户可以通过读写操作来获取和设置这些attribute的值。3、match :当向总线注册一个新的设备或者是新的驱动时会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备4、uevent :总线上的设备发生添加、移除或者其它动作时就会调用该函数来通知驱动做出相应的对策。5、probe :当总线将设备以及驱动相匹配之后执行该回调函数,最终会调用驱动提供的probe函数。6、remove :当设备从总线移除时调用该回调函数7、suspend、resume :电源管理的相关函数当总线进入睡眠模式时会调用suspend回调函数而resume回调函数则是在唤醒总线的状态下执行8、pm :电源管理的结构体存放了一系列跟总线电源管理有关的函数与device_driver结构体中的pm_ops有关9、p :该结构体用于存放特定的私有数据其成员klist_devices和klist_drivers记录了挂载在该总线的设备和驱动上图是注册和注销总线的函数。在内核使用device结构体来描述我们的物理设备如下图1、init_name :指定该设备的名称总线匹配时一般会根据比较名字来进行配对2、parent :表示该设备的父对象前面提到过旧版本的设备之间没有任何关联引入Linux设备模型之后设备之间呈树状结构便于管理各种设备3、bus :表示该设备依赖于哪个总线当我们注册设备时内核便会将该设备注册到对应的总线。4、of_node :存放设备树中匹配的设备节点。当内核使能设备树总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后将匹配的节点保存到该变量。5、platform_data :一个指针用于保存具体的平台相关的数据。具体的driver模块可以将一些私有的数据暂存在这里需要使用的时候再拿出来因此设备模型并不关心该指针得实际含义。6、driver_data :同上驱动层可通过dev_set/get_drvdata函数来获取该成员7、class :指向了该设备对应类开篇我们提到的触摸鼠标以及键盘等设备对于计算机而言他们都具有相同的功能都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备如input、leds、pwm等目录;8、dev :dev_t类型变量字符设备章节提及过它是用于标识设备的设备号该变量主要用于向/sys目录中导出对应的设备。9、release :回调函数当设备被注销时会调用该函数。如果我们没定义该函数时移除设备时会提示“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。9、group :指向struct attribute_group类型的指针指定该设备的属性10、p :是私有数据结构指针该指针中会保存子设备链表、用于添加到bus/driver/prent等设备中的链表头等等。上图是设备注册和注销的函数。在内核中使用device_driver结构体来描述我们的驱动如下所示1、name :指定驱动名称总线进行匹配时利用该成员与设备名进行比较2、bus :表示该驱动依赖于哪个总线内核需要保证在驱动执行之前对应的总线能够正常工作3、suppress_bind_attrs :布尔量用于指定是否通过sysfs导出bind与unbind文件bind与unbind文件是驱动用于绑定/解绑关联的设备。4、owner :表示该驱动的拥有者一般设置为THIS_MODULE5、of_match_table :指定该驱动支持的设备类型。当内核使能设备树时会利用该成员与设备树中的compatible属性进行比较。6、remove :当设备从操作系统中拔出或者是系统重启时会调用该回调函数7、probe :当驱动以及设备匹配后会执行该回调函数对设备进行初始化。通常的代码都是以main函数开始执行的但是在内核的驱动代码都是从probe函数开始的。8、group :指向struct attribute_group类型的指针指定该驱动的属性上图是驱动注册和注销的函数。整体大致流程如下正片开始------属性文件属性文件即 sysfs 中的属性文件是 Linux 内核与用户空间交互的核心接口之一核心好处是让内核态与用户态之间的 “数据交换、状态控制” 变得标准化、安全且灵活具体体现在以下几点实现 “内核 - 用户空间” 的安全交互标准化交互方式降低开发 / 使用成本灵活暴露内核状态与配置支持自动化与调试属性文件分为总线属性文件、设备属性文件、驱动属性文件。最关键的基础是attribute属性文件。attribute属性文件/sys目录有各种子目录以及文件前面讲过当我们注册新的总线、设备或驱动时内核会在对应的地方创建一个新的目录目录名为各自结构体的name成员如果是注册总线则会有/sys/bus/bus_name 。如果是设备则会有/sys/bus/bus_name/devices/device_name。如果是驱动则会有/sys/bus/bus_name/ drivers /driver_name。每个子目录下的文件都是内核导出到用户空间用于控制我们的设备的。内核中以attribute结构体来描述/sys目录下的文件如下所示后续我们会经常用到这个结构体相关的内容。总线属性文件总线属性文件的结构体是struct bus_attribute里面包含struct attribute用于描述当前要注册的总线属性文件的名称同时有show回调函数通过cat操作总线的属性文件则会触发。Store回调函数则通过echo操作总线的属性文件则会触发。BUS_ATTR是一个宏定义但是他相当于一个初始化struct bus_attribute的操作。创建结构体并且对里面的变量赋值。bus_attr_##_name代表bus_attr_和_name会拼接成一个字符串bus_attr_是固定前缀不可替换。_name是可以替换的具体看你__ATTR(_name, _mode, _show, _store)的_name填写什么如果是aaa则最后struct bus_attribute结构体的名称将为bus_attr_aaa。_name填写属性文件的名称versiondebug等_mode填写权限一般写0644或者0444。_show则是我们驱动开发自定义的函数_store同理。但是BUS_ATTR只是初始化结构体并赋值没有真正把文件创建到/sys对应目录中下方的bus_create_file和bus_remove_file分别负责创建属性文件到对应目录如/sys/bus/bus_name的目录下创建和从对应目录删除文件。设备属性文件结构和总线属性文件结构基本一样。按照总线文件属性的理解方式来理解设备属性文件就可以。但是创建属性文件的目录可能是/sys/bus/bus_name/devices/device_name。驱动属性文件依旧是结构体结构差不多。按照总线文件属性的理解方式来理解设备属性文件就可以。但是创建属性文件的目录可能是/sys/bus/bus_name/drivers/ drivers _name。并且我们发现这个驱动的宏定义实现和上述两个的不一样。这里只传了name但是以前的都是传4个参数。实际上系统会自己处理完我们只传_name就可以需要注册的驱动属性文件名称。_mode根据调用宏来决定是0644.0444还是0244那_show和_store回调函数呢那如何设置这两个参数呢在写驱动代码时只需要你提供xxx_store以及xxx_show这两个函数 并确保两个函数的xxx和DRIVER_ATTR类型的宏定义中名字是一致的即可。举例version_show和version_store我自己定义了同时DRIVER_ATTR_RW(version)。这样_name是version_mode是0644_show_store分别是version_show和version_store。代码举例讲解这里没有写全头文件我晚点再整理一整份可供测试的代码简单总结就是创建、导出属性文件、注册。一、初始化自定义总线int xbus_match(struct device *dev, struct device_driver *drv) { printk(%s-%s\n,__FILE__, __func__); if(!strncmp(dev_name(dev), drv-name, strlen(drv-name))){ printk(dev drv match\n); return 1; } return 0; } static struct bus_type xbus { .name xbus, .match xbus_match, }; EXPORT_SYMBOL(xbus);1、__FILE__是一整个当前文件的路径完整的__func__是当前的函数是什么2、xbus要导出不然device和driver的文件没办法绑定总线3、获取设备dev形参名称不能用dev-init_nameinit_name 是 struct device 中用于初始化的临时名称字段仅在设备初始化阶段有效设备注册完成device_register后init_name 会被内核标记为无效比如置为 NULL。而 bus_match 执行时该字段可能已失效 / 被清空且内核规范明确禁止在初始化后使用 init_name。二、导出总线属性文件static char *bus_name xbus; ssize_t xbus_test_show(struct bus_type *bus, char *buf) { return sprintf(buf, %s\n, bus_name); } BUS_ATTR(xbus_test, S_IRUSR, xbus_test_show, NULL); 三、注册总线 static __init int xbus_init(void) { printk(xbus init\n); bus_register(xbus); bus_create_file(xbus, bus_attr_xbus_test); return 0; } module_init(xbus_init); static __exit void xbus_exit(void) { printk(xbus exit\n); bus_remove_file(xbus, bus_attr_xbus_test); bus_unregister(xbus); } module_exit(xbus_exit); MODULE_AUTHOR(embedfire); MODULE_LICENSE(GPL);bus_register会创建/sys/bus/xbus/和/sys/bus/xbus/ueventbus_create_file会在对应目录下创建总线属性文件注销的时候先removefile在unregister四、使用insmod命令加载xbus内核模块sudo insmod xbus.ko五、定义新的设备extern struct bus_type xbus; void xdev_release(struct device *dev) { printk(%s-%s\n, __FILE__, __func__); } static struct device xdev { .init_name xdev, .bus xbus, .release xdev_release, };release函数一定要实现如果不实现内核会在设备销毁时触发崩溃。struct device 中的 release 函数是内核设备模型的强制要求核心原因与 kobject内核对象的生命周期管理有关。struct device 内部包含 struct kobject kobj而 kobject 是内核对象模型的核心采用引用计数kref 管理生命周期。当设备被注册device_register、被其他对象引用时引用计数增加当引用计数减为 0 时内核会调用 kobject 关联的 release 函数即 device-release完成最终的资源清理如果此时release没有被实现那就糟糕了。且代码的release仅供测试并不全面且不规范。struct device 中init_name在设备初始化完全后会被置为null。挂载对应总线绑定release函数。六、导出设备属性文件unsigned long id 0; ssize_t xdev_id_show(struct device *dev, struct device_attribute *attr, char *buf) { return sprintf(buf, %d\n, id); } ssize_t xdev_id_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { kstrtoul(buf, 10, id); return count; } DEVICE_ATTR(xdev_id, S_IRUSR|S_IWUSR, xdev_id_show, xdev_id_store);kstrtoul(buf, 10, id);第一个参数是从形参获得的一个缓冲区存储着用户的echo的输入第二个参数是填写进制此处10进制。第三个参数是把buf内容转换后10进制存在第三个参数中。xdev_id_show 中的 buf是内核在处理用户读操作如cat时自动分配的内核态缓冲区。xdev_id_store 中的 buf是内核将用户写操作如echo传入的数据从用户态拷贝到内核态的缓冲区。七、注册设备static __init int xdev_init(void) { printk(xdev init\n); device_register(xdev); device_create_file(xdev, dev_attr_xdev_id); return 0; } module_init(xdev_init); static __exit void xdev_exit(void) { printk(xdev exit\n); device_remove_file(xdev, dev_attr_xdev_id); device_unregister(xdev); } module_exit(xdev_exit); MODULE_AUTHOR(embedfire); MODULE_LICENSE(GPL);device_register第一把设备添加到 xbus 总线的设备链表中成为 xbus 总线下的一个设备且关联设备与总线的核心回调第二创建设备根目录/sys/devices/xdev/创建总线关联的符号链接/sys/bus/xbus/devices/xdev/ → 指向 /sys/devices/xdev/让总线能找到该设备device_create_file为已注册的 xdev 设备在其 sysfs 目录下创建指定的 xdev_id 属性文件dev_attr_xdev_id因为后缀是xdev_id。注销的时候先removefile在unregister。八、使用insmod命令加载xdev内核模块sudo insmod xdev.ko九、定义新的驱动extern struct bus_type xbus; int xdrv_probe(struct device *dev) { printk(%s-%s\n, __FILE__, __func__); return 0; } int xdrv_remove(struct device *dev) { printk(%s-%s\n, __FILE__, __func__); return 0; } static struct device_driver xdrv { .name xdev, .bus xbus, .probe xdrv_probe, .remove xdrv_remove, };驱动probe函数将会在设备去驱动匹配的时候被调用。十、导出驱动属性文件char *name xdrv; ssize_t drvname_show(struct device_driver *drv, char *buf) { return sprintf(buf, %s\n, name); } DRIVER_ATTR_RO(drvname);1、_mode是0444。2、DRIVER_ATTR_RO(drvname);执行绑定show函数的时候一定要前缀与drvname一致。且该代码没有实现store因为是roreadonly。十一、注册驱动static __init int xdrv_init(void) { printk(xdrv init\n); driver_register(xdrv); driver_create_file(xdrv, driver_attr_drvname); return 0; } module_init(xdrv_init); static __exit void xdrv_exit(void) { printk(xdrv exit\n); driver_remove_file(xdrv, driver_attr_drvname); driver_unregister(xdrv); } module_exit(xdrv_exit); MODULE_AUTHOR(embedfire); MODULE_LICENSE(GPL);1、driver_register第一校验驱动的合法性检查驱动关联的总线如 xbus是否已注册驱动的核心回调如 probe/remove是否合法。第二把驱动添加到 xbus 总线的驱动链表中xbus-p-drivers成为 xbus 总线下的一个驱动关联驱动与总线的匹配规则即定义的 xbus_match 函数为后续设备 - 驱动匹配做准备。第三创建驱动根目录/sys/bus/xbus/drivers/xdrv/名称为驱动的 name 字段。第四创建标准控制文件自动生成 bind/unbind/uevent 文件bind手动绑定设备到驱动写入设备名触发unbind手动解绑设备与驱动uevent驱动的热插拔事件文件2、 driver_create_file为已注册的 xdev 设备在其 sysfs 目录下创建指定的属性文件。3、 注销的时候先removefile在unregister。十二、使用insmod命令加载xdrv内核模块sudo insmod xdrv.ko使用命令 dmesg | tail 来查看模块加载过程的打印信息当我们加载完设备和驱动之后总线开始进行匹配执行match函数 发现这两个设备的名字是一致的就将设备和驱动关联到一起最后会执行驱动的probe函数。