include/linux/cleanup.h 功能与用途分析
一、总体定位
这是 Linux 内核提供的**基于作用域的自动资源清理(scope-based cleanup)**基础设施头文件。它借助 GCC/Clang 的 __attribute__((cleanup)) 变量属性,让编译器在变量离开作用域时自动调用清理函数,从而系统性地消除内核代码中常见的 goto err 手动解绕(unwind)模式及其引发的资源泄漏。其设计理念类似 C++ 的 RAII(Resource Acquisition Is Initialization,资源获取即初始化),但以纯 C 宏实现。
二、解决的核心问题
传统内核错误处理依赖 goto 逐级释放资源:
- 新增一个资源获取点,就要在所有
goto标签间插入对应释放,容易遗漏或顺序错乱; - 释放顺序必须是 LIFO(后获取先释放),手工维护困难。
cleanup.h 把「释放动作」绑定到「变量作用域」,由编译器保证:变量出作用域即触发清理,多个变量按定义逆序清理(GCC 语义),天然满足 LIFO。
三、两大能力族
1. 资源自动释放(__free 族)
| 宏 | 作用 |
|---|---|
DEFINE_FREE(name, type, free) |
定义一个清理器 __free_##name,free 表达式中用 _T 引用变量 |
__free(name) |
变量属性,绑定作用域清理 |
no_free_ptr(p) |
类似非原子 xchg(p, NULL),取出指针并置空以抑制 清理(带 __must_check,防止意外泄漏) |
return_ptr(p) |
return no_free_ptr(p),返回资源且不释放 |
retain_and_null_ptr(p) |
资源已移交他人(消费成功)后置空,不再释放也不返回 |
关键实现细节:DEFINE_FREE 的 free 表达式刻意包含 NULL 判断 (如 if (_T) kfree(_T))。这样在成功路径 return_ptr 把指针置空后,编译器通过值传播 + 死代码消除,可将整段清理调用彻底优化掉,退化成一条 return p。
2. 锁 / 通用析构(guard / CLASS 族)
底层是通用的构造-析构类机制:
DEFINE_CLASS(name, type, exit, init, args...):为某类型定义构造器与析构器;CLASS(name, var)(args):声明一个受作用域管理的实例;EXTEND_CLASS/EXTEND_CLASS_COND:派生出带条件的变体。
在其之上封装出锁专用接口:
| 宏 | 用途 |
|---|---|
DEFINE_GUARD(name, type, lock, unlock) |
定义一个锁 guard(lock/unlock 为语句) |
DEFINE_GUARD_COND |
定义条件锁变体(如 trylock、lock_interruptible) |
guard(name)(obj) |
匿名 guard,持锁至当前语句块结束 |
scoped_guard(name, args) { } |
具名 scope,锁生命周期绑定紧随的复合语句;条件锁失败时跳过循环体 |
scoped_cond_guard(name, fail, args) { } |
条件锁失败时执行 fail |
ACQUIRE(name, var) / ACQUIRE_ERR(name, &var) |
具名 guard + 错误码提取,用于需要检查加锁结果的条件锁场景 |
针对无原生类型(RCU、preempt)或需要「胖指针」(spin_lock_irqsave 要保存 flags)的锁,提供:
DEFINE_LOCK_GUARD_0(无 lock 对象)、DEFINE_LOCK_GUARD_1(单 lock 对象)、DEFINE_LOCK_GUARD_1_COND(条件变体);这些会生成一个内含type *lock及附加字段(如 flags)的class_##name##_t结构体。
四、用法示例
1. __free 自动释放内存
c
DEFINE_FREE(kfree, void *, if (_T) kfree(_T))
struct obj *alloc_obj(...)
{
struct obj *p __free(kfree) = kmalloc(sizeof(*p), GFP_KERNEL);
if (!p)
return NULL; /* 出作用域自动 kfree(p) */
if (!init_obj(p))
return NULL; /* 同样自动 kfree(p) */
return_ptr(p); /* 成功:抑制清理并返回 p */
}
成功路径上 return_ptr(p) 先把 p 置空再返回,清理被抑制;配合 free 表达式里的 NULL 判断,编译器可将整段清理优化为一条 return p。
2. __free 释放设备引用
c
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));
if (!dev)
return -ENODEV; /* 自动 pci_dev_put(dev) */
3. guard:持锁到语句块结束
c
DEFINE_GUARD(pci_dev, struct pci_dev *,
pci_dev_lock(_T), pci_dev_unlock(_T))
func(struct pci_dev *dev)
{
if (cond) {
guard(pci_dev)(dev); /* 此处 pci_dev_lock() */
...
} /* 离开 if 块自动 pci_dev_unlock() */
}
注意锁只持有到 if 块结束,而非整个 func()。
4. scoped_guard:显式绑定作用域
c
scoped_guard(mutex, &lock) {
/* 临界区,退出该复合语句时自动解锁 */
do_something();
}
5. 条件锁:ACQUIRE + ACQUIRE_ERR
c
DEFINE_GUARD(pci_dev, struct pci_dev *,
pci_dev_lock(_T), pci_dev_unlock(_T))
DEFINE_GUARD_COND(pci_dev, _try, pci_dev_trylock(_T))
int func(struct pci_dev *dev)
{
ACQUIRE(pci_dev_try, lock)(dev);
int rc = ACQUIRE_ERR(pci_dev_try, &lock);
if (rc)
return rc; /* 加锁失败,携带错误码返回 */
/* 此处已持锁,出作用域自动解锁 */
return 0;
}
6. scoped_cond_guard:条件锁失败即跳错误处理
scoped_cond_guard(name, fail, args...) 也用于条件锁,但把「加锁失败」的分支交给 fail 语句处理,随后的复合语句是加锁成功后的临界区:
c
/* mutex_lock_interruptible 失败返回 -EINTR 等错误 */
scoped_cond_guard(mutex_intr, return -EINTR, &lock) {
/* 加锁成功的临界区,退出该块自动解锁 */
do_something();
}
/* 若加锁失败,则执行 fail(此处 return -EINTR),不进入临界区 */
与 ACQUIRE + ACQUIRE_ERR 相比,scoped_cond_guard 更紧凑,适合失败时只做固定动作(如直接返回)的场景;而 ACQUIRE_ERR 能拿到具体错误码,适合需要区分错误值的场景。
7. 胖指针锁:DEFINE_LOCK_GUARD_1
针对需要保存额外状态(如 spin_lock_irqsave 的 flags)的锁,用带类型的 guard:
c
DEFINE_LOCK_GUARD_1(spinlock_irqsave, spinlock_t,
spin_lock_irqsave(_T->lock, _T->flags),
spin_unlock_irqrestore(_T->lock, _T->flags),
unsigned long flags)
scoped_guard(spinlock_irqsave, &my_lock) {
/* flags 保存在 guard 结构体中,退出时自动 restore */
}
五、条件锁的错误传递机制
条件锁通过 IS_ERR 指针编码结果:
__DEFINE_GUARD_LOCK_PTR生成lock_ptr(加锁失败返回 NULL)与lock_err(返回 0 或负错误码);scoped_guard的循环条件__guard_ptr(&scope) || !__is_cond_ptr(name)保证:无条件锁必然执行循环体,条件锁则在加锁失败时跳过;ACQUIRE_ERR本质是对 guard 指针做PTR_ERR转换。
六、对静态分析(Context Analysis)的支持
文件末尾的 DECLARE_LOCK_GUARD_1_ATTRS / WITH_LOCK_GUARD_1_ATTRS 及 __no_context_analysis 标记,用于解决一个矛盾:cleanup 把锁包进结构体并经辅助函数传递,遮蔽了锁别名,编译器的加锁/解锁静态检查(__acquires / __releases)无法跨函数分析。解决办法是引入一个「什么都不做、仅带 _unlock 属性」的别名变量,在作用域结束时告诉编译器此处发生了解锁,从而让 Sparse/Clang 的上下文分析仍能工作。
七、使用约束(文档中明确的注意事项)
- 变量定义即赋值 :使用
__free()时应在同一语句定义并初始化,不要在函数顶部集中声明。因为清理按定义逆序执行,若把obj __free(...) = NULL放在guard(mutex)之前,会导致obj的清理在锁已释放后 才执行(文档中!!标注的 bug)。正确写法是先guard,再struct object *obj __free(remove_free) = alloc_add();。 - 不与
goto混用 :goto可跨作用域跳转,与作用域清理语义冲突。同一函数要么全部转成 cleanup,要么都不用。
八、典型收益
- 消除样板式
goto解绕代码,减少泄漏面; - 由编译器强制保证 LIFO 释放顺序;
- 成功路径经优化后零运行时开销;
- 驱动代码(内核主体)是主要受益方,文档即以 PCI 驱动的
pci_dev_put/pci_dev_unlock为例。
九、一句话概括
这是内核的「C 语言 RAII」头文件,用编译器的 cleanup 属性把资源释放与锁释放自动绑定到变量作用域,替代易错的 goto 手动清理。