ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux 内核即插即用(Plug and Play)层完全指南:sysfs 接口、资源管理与 PnP 驱动开发

Linux 内核即插即用(Plug and Play)层完全指南:sysfs 接口、资源管理与 PnP 驱动开发 Linux 内核即插即用Plug and Play层完全指南sysfs 接口、资源管理与 PnP 驱动开发【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxLinux Plug and PlayPnP为传统 ISA 总线设备以及那些无法自行配置资源的legacy硬件提供了一整套检测与资源配置机制。Linux 内核的 PnP 子系统主要位于 drivers/pnp通过统一的 PnP Layer 汇聚 PNPBIOS、ISAPNP、ACPI 等不同协议并借助 sysfs 暴露用户态接口。阅读本文后你将掌握如何通过 sysfs 激活/禁用 PnP 设备、理解pnp_reserve_*内核参数的底层实现并能够按照官方文档的规范编写一个完整的 PnP 驱动。本文内容以内核文档 Documentation/admin-guide/pnp.rst 为主体骨架并结合当前仓库中 drivers/pnp 下的真实源码core.c、driver.c、manager.c、interface.c、resource.c等进行深度印证与扩充。一、概述什么是 Linux Plug and PlayPlug and Play 提供了一种检测并为 legacy 设备或其他不可配置设备设置资源的手段。所谓 legacy 设备指那些没有现代总线自动配置能力如 PCI 的 BAR 机制但仍需要占用 I/O 端口、IRQ、DMA 通道或内存区域的传统设备典型如串口、并口、软驱控制器。Linux Plug and Play Layer 负责把这些服务提供给兼容的驱动。从源码结构看PnP 子系统的整体架构分为四层均位于 drivers/pnp/PnP 协议Protocolsisapnp/、pnpbios/、pnpacpi/三个子目录分别对应 ISAPNP、PNPBIOS、ACPI 三种协议实现PnP Layer统一层core.c 中的设备/协议注册与生命周期管理driver.c 中的驱动模型集成与 ID 匹配资源管理Resource Managermanager.c 负责资源的自动分配、冲突检测、激活与禁用用户接口User Interfaceinterface.c 将设备属性暴露到 sysfs。在 core.c 中有一个关键注释与全局标志/* * ACPI or PNPBIOS should tell us about all platform devices, so we can * skip some blind probes. ISAPNP typically enumerates only plug-in ISA * devices, not built-in things like COM ports. */ int pnp_platform_devices; EXPORT_SYMBOL(pnp_platform_devices);也就是说当 ACPI 或 PNPBIOS 存在时内核可以跳过部分盲探测blind probe因为平台设备已经被完整枚举而 ISAPNP 通常只枚举插入式 ISA 设备内建的 COM 口等不在此列。二、sysfs 用户接口Linux PnP 的用户接口为不支持 Linux Plug and Play 的 legacy 驱动或用户级驱动提供了一种激活 PnP 设备的手段。该接口已完全集成进 sysfs。2.1 每个设备目录下的三个标准文件在每个 PnP 设备的 sysfs 目录中除了标准的 sysfs 文件之外还会额外创建以下三个文件见 interface.c 中注册的属性组pnp_dev_attrs文件作用对应实现id显示该设备支持的一系列 EISA ID 列表interface.c 中的id_show()options显示该设备可能的资源组合dependent setsinterface.c 中的options_show()resources显示当前已分配的资源并允许修改资源interface.c 中的resources_show()/resources_store()值得注意的是resources是读写属性DEVICE_ATTR_RW而id、options是只读属性DEVICE_ATTR_RO。在较新的内核版本中resources_show()还会先输出一行state active或state disabledinterface.c随后逐条列出io、irq、dma、mem、bus资源。2.2 激活设备在设备目录下执行# echo auto resources这会调用自动资源配置系统pnp_auto_config_dev()来激活设备。从源码看interface.c 中auto的处理逻辑是先pnp_init_resources(dev)清空已有资源表再调用pnp_auto_config_dev(dev)依次尝试各 dependent set直至成功分配全部资源。2.3 手动激活设备# echo manual depnum mode resources参数含义depnum—— 配置号dependent set 编号mode—— 取值static或dynamicstatic表示针对下次启动持久化配置dynamic表示立即生效本次运行。需要指出的是在当前版本内核的 interface.c 中resources_store()实际支持的关键字为disable、activate、fill、auto、clear、get、setinterface.c其中fill/auto均会触发pnp_auto_config_dev()而set支持直接指定io start-end、mem start-end、irq n、dma n、bus start-end等资源值比文档描述的manual语法提供了更细粒度的控制手段如echo set irq 5 io 0x3f0-0x3f5 resources。这也是文档编写年代与现行实现的一个差异点实际使用时以resources_store()源码为准。2.4 禁用设备# echo disable resources对应 interface.c 中调用pnp_disable_dev(dev)。底层行为见 manager.c先通过pnp_stop_dev()让协议层执行disable操作然后清理自动分配的资源表使这些资源可以被其他设备复用。2.5 实战示例激活软驱控制器文档给出了完整的操作流程这里结合源码补充说明每一步的输出含义# cd /driver/bus/pnp/devices/00:0f注意文档中的路径以 2002 年左右的 sysfs 布局为基准如今设备通常位于/sys/bus/pnp/devices/下例如/sys/bus/pnp/devices/00:0f且设备目录名由%02x:%02x格式的协议号:设备号构成见 core.c 中dev_set_name(dev-dev, %02x:%02x, ...)。# cat name PC standard floppy disk controller # cat resources DISABLED在旧版内核中未激活设备会显示DISABLED字符串。当前版本则会显示state disabled。随后查看该设备支持的资源组合# cat options Dependent: 01 - Priority acceptable port 0x3f0-0x3f0, align 0x7, size 0x6, 16-bit address decoding port 0x3f7-0x3f7, align 0x0, size 0x1, 16-bit address decoding irq 6 dma 2 8-bit compatible Dependent: 02 - Priority acceptable port 0x370-0x370, align 0x7, size 0x6, 16-bit address decoding port 0x377-0x377, align 0x0, size 0x1, 16-bit address decoding irq 6 dma 2 8-bit compatible这段输出正是 interface.c 中options_show()的格式化结果pnp_print_port()输出port min-max, align ..., size ..., N-bit address decodingpnp_print_irq()输出 IRQ 位图及其触发方式标志High-Edge / Low-Edge / High-Level / Low-Level / optionalpnp_print_dma()输出 DMA 通道、位宽8-bit / 16-bit与传输类型compatible / type-A / type-B / type-F 等见 interface.c。激活设备# echo auto resources验证是否已激活# cat resources io 0x3f0-0x3f5 io 0x3f7-0x3f7 irq 6 dma 2这里可以看到auto自动配置从Dependent: 01中选中了端口基址0x3f0、align 0x7、size 0x6的组合最终分配得到io 0x3f0-0x3f50x3f0 6 字节 - 1 0x3f5这验证了 manager.c 中pnp_assign_port()的分配算法从min开始按align步进递增直到通过pnp_check_port()的冲突检测为止。2.6 IRQ/DMA 的分配优先级自动配置并不是随机挑选资源。manager.c 中内置了一张 i386 平台常用的 IRQ 优先级表/* IRQ priority: this table is good for i386 */ static unsigned short xtab[16] { 5, 10, 11, 12, 9, 14, 15, 7, 3, 4, 13, 0, 1, 6, 8, 2 };pnp_assign_irq()会先检查 IRQ 位图中是否存在 16 以上的扩展中断再按xtab的优先级顺序逐一尝试配合pnp_check_irq()做冲突检测manager.c。DMA 通道也有类似的优先级表{1, 3, 5, 6, 7, 0, 2, 4}manager.c。若某资源是可选的IORESOURCE_IRQ_OPTIONAL且无法分配会以禁用状态加入资源表而不是报错manager.c。2.7 整套自动配置流程pnp_auto_config_devpnp_auto_config_dev()manager.c的完整流程如下检查协议是否支持配置pnp_can_configure()不支持则返回-ENODEV先尝试 dependent set 0pnp_assign_resources(dev, 0)失败则依次尝试 set 1、2、... 直到dev-num_dependent_sets全部失败则打印unable to assign resources并返回错误。激活操作pnp_activate_dev()manager.c则在此基础上叠加若设备已激活直接返回 0否则先自动分配资源再调用pnp_start_dev()内部调用dev-protocol-set(dev)真正写入硬件成功后将dev-active置 1。三、内核启动参数pnp_reserve_*为了把某些资源预留给传统设备例如不希望 PnP 自动配置触碰特定 IRQ 或 I/O 区域内核提供了一组启动参数。它们定义于 drivers/pnp/resource.cstatic int pnp_reserve_irq[16] {[0 ... 15] -1 }; /* reserve (dont use) some IRQ */ static int pnp_reserve_dma[8] {[0 ... 7] -1 }; /* reserve (dont use) some DMA */ static int pnp_reserve_io[16] {[0 ... 15] -1 }; /* reserve (dont use) some I/O region */ static int pnp_reserve_mem[16] {[0 ... 15] -1 }; /* reserve (dont use) some memory region */命令行格式如下pnp_reserve_irqirq1[,irq2] .... pnp_reserve_dmadma1[,dma2] .... pnp_reserve_ioio1,size1[,io2,size2] .... pnp_reserve_memmem1,size1[,mem2,size2] ....参数解析通过__setup()宏注册的早期引导回调完成resource.c例如/* format is: pnp_reserve_ioio1,size1[,io2,size2] .... */ static int __init pnp_setup_reserve_io(char *str) { int i; for (i 0; i 16; i) if (get_option(str, pnp_reserve_io[i]) ! 2) break; return 1; } __setup(pnp_reserve_io, pnp_setup_reserve_io);各参数的上限分别为IRQ 最多 16 个、DMA 最多 8 个、I/O 区域最多 16 个每区域占用两个槽位起始地址 大小、内存区域最多 16 个。数组初始值为 -1表示未设置。这些保留值在自动配置的冲突检测中被实际使用resource.c、resource.c、resource.c、resource.c当候选 I/O 端口区间与保留区间重叠ranged_conflict()、候选内存区间重叠、候选 IRQ/DMA 与保留列表相等时分配器会直接拒绝该候选从而实现保留语义。典型用法示例pnp_reserve_irq3,5 # 保留 IRQ 3 与 5 pnp_reserve_dma1 # 保留 DMA 通道 1 pnp_reserve_io0x378,8 # 保留 0x378 起始、长度 8 的 I/O 区间 pnp_reserve_mem0xd0000,0x10000 # 保留内存区间四、统一 PnP LayerUnified Plug and Play Layer所有 PnP 驱动、协议与服务在一个中央位置汇合即 Plug and Play Layer。该层负责在 PnP 驱动与 PnP 协议之间交换信息并自动把命令转发给正确的协议从而显著降低 PnP 驱动的编写难度。4.1 PnP Layer 对外提供的核心函数函数说明源码位置pnp_get_protocol协议引用计数加一drivers/pnp/core.cpnp_put_protocol协议引用计数减一drivers/pnp/core.cpnp_register_protocol注册一个新的 PnP 协议core.cpnp_register_driver将 PnP 驱动加入 PnP Layer含驱动模型集成成功返回 0失败返回负错误码driver.cpnp_unregister_driver从 PnP Layer 移除 PnP 驱动driver.cpnp_register_driver()的实现非常简洁——它把pnp_driver包装进内核通用struct device_driver挂到pnp_bus_type总线上然后调用标准的driver_register()这就是驱动模型集成的实质driver.cint pnp_register_driver(struct pnp_driver *drv) { drv-driver.name drv-name; drv-driver.bus pnp_bus_type; return driver_register(drv-driver); }pnp_register_protocol()会为每个新协议分配未使用的最小编号并注册一个名为pnp%d的协议设备core.c。4.2 驱动绑定与 probe 流程PnP 驱动与设备的匹配依赖 EISA ID。pnp_bus_match()调用match_device()后者用驱动id_table中的每一项与设备 ID 链表做compare_pnp_id()比较driver.c。比较规则很有特点ID 必须恰好 7 个字符3 位厂商码 4 位功能码字符串ANYDEVS匹配一切设备前 3 个字符必须完全一致后 4 个字符则忽略大小写且字符X可作为通配符。对应地pnp_uevent()会为每个设备 ID 生成MODALIASpnp:dID的 uevent 环境变量driver.c供 udev 等用户态工具做模块自动加载。pnp_device_probe()driver.c的流程为先pnp_device_attach()把设备状态从PNP_READY改为PNP_ATTACHED若设备尚未激活且驱动未声明PNP_DRIVER_RES_DO_NOT_CHANGE标志则自动调用pnp_activate_dev()激活最后匹配 ID 并调用驱动的.probe()。pnp_device_remove()则是对称地调用.remove()并在设备仍激活时除非驱动要求保持资源不变调用pnp_disable_dev()driver.c。五、PnP 协议Protocols5.1 当前计算世界中可用的 PnP 协议文档列举了三种主流协议PNPBIOS用于系统设备如串口serial和并口parallel。实现在 drivers/pnp/pnpbios/core.c、bioscalls.c、rsparser.c、proc.cISAPNP为 ISA 总线提供 PnP 支持。实现在 drivers/pnp/isapnp/core.c、compat.c、proc.cACPI除众多其他用途外ACPI 也提供系统级设备的信息。实现在 drivers/pnp/pnpacpi/core.c、rsparser.c。关于 ACPI 协议文档明确写道它指 ACPI旨在取代 PNPBIOS。当时 Linux PnP 尚未支持将 ACPI 协议接入 PnP Layer但计划在不久的将来实现。——而从当前仓库源码看pnpacpi目录已经成为完整的协议实现drivers/pnp/pnpacpi/core.c即这一计划早已落地。5.2 Linux PnP 协议的要求协议必须使用 EISA ID协议必须把设备的当前配置告知 PnP Layer设置资源的能力set是可选的但更受青睐preferred。5.3 协议相关函数函数说明pnp_add_device把 PnP 设备加入 PnP Layer只有当pnp_dev结构体中所有需要的值都设置完毕后才可调用。见 core.c它还会打印一条含全部 ID 的调试信息pnp_init_device初始化 PnP 结构体pnp_remove_device从 PnP Layer 移除设备若设备仍在使用中则失败会自动释放设备及其相关结构占用的内存pnp_add_id为指定设备添加一个 EISA ID 到其支持的 ID 列表协议开发者可参考一个现成协议的实现来学习文档推荐了/drivers/pnp/pnpbios/core.c即仓库中的 drivers/pnp/pnpbios/core.c。5.4 设备枚举与 ID 链设备从协议侧被加入时pnp_alloc_dev()会为设备分配struct pnp_dev设置dma_mask DMA_BIT_MASK(24)并立刻调用pnp_add_id()挂上第一个 ID设备名格式化为%02x:%02x协议号:设备号core.c。随后__pnp_add_device()把设备加入全局链表pnp_global和协议的devices链表并device_register()到驱动模型core.c。六、Linux PnP 驱动开发本节面向 PnP 驱动开发者给出新式New Way与旧式Old Way两种开发路径。6.1 新式写法The New Way第一步定义支持的 EISA ID 列表static const struct pnp_id pnp_dev_table[] { /* Standard LPT Printer Port */ {.id PNP0400, .driver_data 0}, /* ECP Printer Port */ {.id PNP0401, .driver_data 0}, {.id } };注意字符X可以在功能码部分后四个字符中用作通配符例如/* Unknown PnP modems */ { PNPCXXX, UNKNOWN_DEV },这一通配符语义与 driver.c 中compare_func()的实现完全对应ida[i] ! X idb[i] ! X toupper(ida[i]) ! toupper(idb[i])才判定不匹配即任何一方的X都能命中任意字符。可选地还可以定义 PnP 卡cardID 列表static const struct pnp_id pnp_card_table[] { { ANYDEVS, 0 }, { , 0 } };第二步可选定义 probe 与 remove 函数如果驱动已经有可靠的资源检测手段例如parport_pc驱动可以不定义这两个函数static int serial_pnp_probe(struct pnp_dev * dev, const struct pnp_id *card_id, const struct pnp_id *dev_id) { . . . static void serial_pnp_remove(struct pnp_dev * dev) { . . .文档建议参考 drivers/serial/8250_pnp.c 获取更多信息。需要说明的是在现行内核驱动模型中pnp_driver.probe回调的签名已经演化为int (*probe)(struct pnp_dev *dev, const struct pnp_device_id *dev_id)见 include/linux/pnp.h 与 driver.c 中pnp_drv-probe(pnp_dev, dev_id)的调用方式card_id参数已不再传递。第三步创建驱动结构体static struct pnp_driver serial_pnp_driver { .name serial, .card_id_table pnp_card_table, .id_table pnp_dev_table, .probe serial_pnp_probe, .remove serial_pnp_remove, };约束name与id_table不能为 NULL。这与 driver.c 中match_device()的逻辑一致——若id_table为空则直接返回 NULL无法匹配任何设备。第四步注册驱动static int __init serial8250_pnp_init(void) { return pnp_register_driver(serial_pnp_driver); }6.2 旧式写法The Old Way为方便把 ISAPNP 驱动迁移到新框架内核提供了一系列兼容函数。文档明确指出它们只应作为临时方案使用。核心函数如下struct pnp_dev *pnp_find_dev(struct pnp_card *card, unsigned short vendor, unsigned short function, struct pnp_dev *from)该函数的实现位于 drivers/pnp/isapnp/compat.c。它先把 16 位的 vendor/function 数值转换为 7 字符 EISA ID 字符串pnp_convert_id()compat.c然后按两种模式查找card NULL从全局设备链表pnp_global中查找可指定from作为遍历起点便于迭代查找多个匹配设备card ! NULL在该卡card的devices链表内查找。匹配时使用compare_pnp_id()且当目标 ID 等于ISAPNP_ANY_ID即任意 ID 通配时也视为匹配。这套兼容接口的目标是让老 ISAPNP 驱动以最小改动接入新框架但长期维护仍应迁移到新式写法。七、结语Linux PnP 子系统通过协议 统一层 资源管理器 sysfs 接口的分层设计把 PNPBIOS、ISAPNP、ACPI 等异构协议的差异封装在 PnP Layer 之后既向用户态提供了id、options、resources三个标准的 sysfs 操作入口又向内核驱动提供了以 EISA ID 匹配为核心的统一开发框架。实际操作中你可以通过echo auto resources一键激活 legacy 设备、通过pnp_reserve_*启动参数为传统设备预留资源开发驱动时只需按定义pnp_id表 → 可选定义 probe/remove → 填充pnp_driver结构体 →pnp_register_driver()注册四步走即可让驱动接入这套自动检测与资源配置机制。深入阅读 drivers/pnp 目录下的源码特别是 manager.c资源分配与激活、interface.csysfs 接口和 driver.c驱动模型集成能够帮助你准确理解每个操作背后的真实调用链。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表