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

“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 非空,则自动调用 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 文档指出:

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

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

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”清理的资源转换为基于作用域的清理,要么不转换任何一个。