Devres - 管理设备资源 【ChatGPT】

发布时间 2023-12-10 13:05:46作者: 摩斯电码

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