基于作用域的清理辅助函数

“goto error” 模式因引入微妙的资源泄漏而臭名昭著。在已经有多个解缠条件的代码路径中添加新的资源获取约束是繁琐且容易出错的。“cleanup” 辅助函数使编译器能够帮助处理这种繁琐的工作,并有助于维护 LIFO(后进先出)解缠顺序,以避免意外泄漏。

由于驱动程序构成了内核代码库的大部分,这里有一个使用这些辅助函数清理 PCI 驱动程序的示例。清理的目标是使用 goto 来解缠设备引用(pci_dev_put())或在返回之前解锁设备 (pci_dev_unlock()) 的情况。

DEFINE_FREE() 宏可以安排在关联变量超出作用域时删除 PCI 设备引用

DEFINE_FREE(pci_dev_put, struct pci_dev *, if (_T) pci_dev_put(_T))
...
struct pci_dev *dev __free(pci_dev_put) =
        pci_get_slot(parent, PCI_DEVFN(0, 0));

如果 **dev** 在 **dev** 超出作用域(自动变量作用域)时为非 NULL,则以上代码将自动调用 pci_dev_put()。如果函数想要在发生错误时调用 pci_dev_put(),但在成功时返回 **dev** (即不释放它),它可以这样做

return no_free_ptr(dev);

...或者

return_ptr(dev);

DEFINE_GUARD() 宏可以安排在调用 guard() 的作用域结束时释放 PCI 设备锁

DEFINE_GUARD(pci_dev, struct pci_dev *, pci_dev_lock(_T), pci_dev_unlock(_T))
...
guard(pci_dev)(dev);

guard() 辅助函数获得的锁的生命周期遵循自动变量声明的作用域。以下面的例子为例

func(...)
{
        if (...) {
                ...
                guard(pci_dev)(dev); // pci_dev_lock() invoked here
                ...
        } // <- implied pci_dev_unlock() triggered here
}

请注意,锁在“if ()”块的其余部分而不是“func()”的其余部分中被持有。

现在,当函数使用 __free() 和 guard(),或 __free() 的多个实例时,变量定义顺序的 LIFO 顺序很重要。GCC 文档说

“当同一作用域中的多个变量具有清理属性时,在退出作用域时,它们的关联清理函数将按照定义的相反顺序(最后定义,首先清理)运行。”

当解缠顺序很重要时,需要将变量定义在函数作用域中间,而不是在文件的顶部。以下面的例子为例,注意“!!”突出显示的错误

LIST_HEAD(list);
DEFINE_MUTEX(lock);

struct object {
        struct list_head node;
};

static struct object *alloc_add(void)
{
        struct object *obj;

        lockdep_assert_held(&lock);
        obj = kzalloc(sizeof(*obj), GFP_KERNEL);
        if (obj) {
                LIST_HEAD_INIT(&obj->node);
                list_add(obj->node, &list):
        }
        return obj;
}

static void remove_free(struct object *obj)
{
        lockdep_assert_held(&lock);
        list_del(&obj->node);
        kfree(obj);
}

DEFINE_FREE(remove_free, struct object *, if (_T) remove_free(_T))
static int init(void)
{
        struct object *obj __free(remove_free) = NULL;
        int err;

        guard(mutex)(&lock);
        obj = alloc_add();

        if (!obj)
                return -ENOMEM;

        err = other_init(obj);
        if (err)
                return err; // remove_free() called without the lock!!

        no_free_ptr(obj);
        return 0;
}

通过更改 init() 以按此顺序调用 guard() 并定义 + 初始化 **obj** 来修复该错误

guard(mutex)(&lock);
struct object *obj __free(remove_free) = alloc_add();

鉴于在函数顶部定义的变量使用“__free(...) = NULL”模式会带来这种潜在的相互依赖性问题,建议始终在一个语句中定义和赋值变量,并且在使用 __free() 时不要在函数顶部对变量定义进行分组。

最后,鉴于清理辅助函数的好处是删除了 “goto”,并且 “goto” 语句可以在作用域之间跳转,因此期望永远不要在同一个函数中混合使用 “goto” 和清理辅助函数。即,对于给定的例程,将所有需要 “goto” 清理的资源转换为基于作用域的清理,或者一个都不转换。