Devres - 管理设备资源
Tejun Heo teheo@suse.de
首稿日期:2007年1月10日
1. 简介
在尝试将libata转换为使用iomap时,出现了devres。每个iomapped地址应该在驱动程序分离时保留和取消映射。例如,一个普通的SFF ATA控制器(即,传统的PCI IDE)在本地模式下使用5个PCI BAR,所有这些BAR都应该被维护。
与许多其他设备驱动程序一样,libata低级驱动程序在->remove和->probe失败路径上存在足够的错误。嗯,是的,这可能是因为libata低级驱动程序开发人员都是懒散的一群,但是所有低级驱动程序开发人员都是如此吗?在花费一天的时间与没有文档或有缺陷的文档进行调试之后,如果最终工作正常,那就好了。
由于某种原因,低级驱动程序没有像核心代码那样受到足够的关注或测试,并且在驱动程序分离或初始化失败时的错误并不经常发生,以至于不容易被注意到。初始化失败路径更糟糕,因为它的访问次数要少得多,但需要处理多个入口点。
因此,许多低级驱动程序最终会在驱动程序分离时泄漏资源,并且在->probe()中实现的故障路径会泄漏资源,甚至在发生故障时导致oops。iomap会增加这种情况。msi和msix也是如此。
2. Devres
devres基本上是与struct device关联的任意大小内存区域的链表。每个devres条目都与一个释放函数关联。devres可以通过多种方式释放。无论如何,在驱动程序分离时都会释放所有的devres条目。在释放时,会调用关联的释放函数,然后释放devres条目。
使用devres,为设备驱动程序常用的资源创建了托管接口。例如,使用dma_alloc_coherent()获取一致的DMA内存。托管版本称为dmam_alloc_coherent()。它与dma_alloc_coherent()相同,只是使用它分配的DMA内存是托管的,并且将在驱动程序分离时自动释放。实现如下所示:
struct dma_devres { size_t size; void *vaddr; dma_addr_t dma_handle; }; static void dmam_coherent_release(struct device *dev, void *res) { struct dma_devres *this = res; dma_free_coherent(dev, this->size, this->vaddr, this->dma_handle); } dmam_alloc_coherent(dev, size, dma_handle, gfp) { struct dma_devres *dr; void *vaddr; dr = devres_alloc(dmam_coherent_release, sizeof(*dr), gfp); ... /* 像往常一样分配DMA内存 */ vaddr = dma_alloc_coherent(...); ... /* 在dr中记录size、vaddr和dma_handle */ dr->vaddr = vaddr; ... devres_add(dev, dr); return vaddr; }
如果驱动程序使用dmam_alloc_coherent(),无论初始化在中途失败还是设备被分离,该区域都将被释放。如果大多数资源都使用托管接口获取,驱动程序的初始化和退出代码可以简化得多。初始化路径基本上如下所示:
my_init_one() { struct mydev *d; d = devm_kzalloc(dev, sizeof(*d), GFP_KERNEL); if (!d) return -ENOMEM; d->ring = dmam_alloc_coherent(...); if (!d->ring) return -ENOMEM; if (check something) return -EINVAL; ... return register_to_upper_layer(d); }
退出路径如下:
my_remove_one() { unregister_from_upper_layer(d); shutdown_my_hardware(); }
如上所示,通过使用devres,低级驱动程序可以大大简化。复杂性从维护较少的低级驱动程序转移到维护较好的高层。而且,由于初始化失败路径与退出路径共享,两者都可以得到更多的测试。
请注意,当将当前的调用或赋值转换为托管的devm_版本时,您需要检查内部操作(如分配内存)是否失败。托管资源仅涉及这些资源的释放-所有其他所需的检查仍然由您完成。在某些情况下,这可能意味着引入以前在使用托管的devm_调用之前不必要的检查。
3. Devres组
可以使用devres组对devres条目进行分组。当释放组时,将释放所有包含的普通devres条目和正确嵌套的组。一个用途是在失败时回滚一系列获取的资源。例如:
if (!devres_open_group(dev, NULL, GFP_KERNEL)) return -ENOMEM; acquire B; if (failed) goto err; acquire B; if (failed) goto err; ... devres_remove_group(dev, NULL); return 0; err: devres_release_group(dev, NULL); return err_code;
由于资源获取失败通常意味着探测失败,像上面的结构通常在中间层驱动程序(例如libata核心层)中很有用,其中接口函数在失败时不应具有副作用。对于LLD,大多数情况下只需返回错误代码即可。
每个组由void *id
标识。它可以通过将@id参数明确指定为devres_open_group()来显式指定,也可以通过将NULL作为@id传递来自动创建,就像上面的示例一样。在这两种情况下,devres_open_group()都会返回组的id。返回的id可以传递给其他devres函数以选择目标组。如果将NULL传递给这些函数,将选择最新打开的组。
例如,您可以执行以下操作:
int my_midlayer_create_something() { if (!devres_open_group(dev, my_midlayer_create_something, GFP_KERNEL)) return -ENOMEM; ... devres_close_group(dev, my_midlayer_create_something); return 0; } void my_midlayer_destroy_something() { devres_release_group(dev, my_midlayer_create_something); }
4. 详细信息
devres条目的生命周期始于devres分配并在释放或销毁(删除和释放)时结束-没有引用计数。
devres核心保证了所有基本devres操作的原子性,并支持单实例devres类型(原子查找并添加(如果未找到))。除此之外,同步并发访问分配的devres数据是调用者的责任。这通常不是问题,因为总线操作和资源分配已经完成了这项工作。
有关单实例devres类型的示例,请阅读lib/devres.c中的pcim_iomap_table()。
如果给出正确的gfp掩码,所有devres接口函数都可以在没有上下文的情况下调用。
5. 开销
每个devres的簿记信息与请求的数据区一起分配。在关闭调试选项的情况下,簿记信息在32位机器上占用16字节,在64位机器上占用24字节(三个指针舍入到ull对齐)。如果使用单链表,可以减少为两个指针(32位机器上为8字节,64位机器上为16字节)。
每个devres组占用8个指针。如果使用单链表,可以减少为6个指针。
在具有两个端口的ahci控制器上,经过简单转换后,32位机器上的内存空间开销在300到400字节之间(我们当然可以在libata核心层中投入更多的努力)。
6. 接口列表
https://www.kernel.org/doc/html/v6.6/driver-api/driver-model/devres.html#list-of-managed-interfaces