LWN:引用计数的调试机制!
关注了就能看到更多这么棒的文章哦~
A reference-count tracking infrastructure
By Jonathan Corbet
December 6, 2021
DeepL assisted translation
https://lwn.net/Articles/877603/
引用计数(reference count)是一种非常常用的机制,用来跟踪(track)当前的计算系统中某个对象的生命周期(life cycle)。只要这个对象的每个使用者都能做到准确加、减引用计数来正确维护这个引用,那么该对象只要还有用户需要的情况下就能确保一直存在,并在最后一个使用者完成使用后被正常销毁。不过,这句话中提到的 "正确" 是非常重要的,如果出现引用计数使用错误的情况,那么事情就不那么理想了。网络领域的开发人员 Eric Dumazet 正在开发一个引用计数的 tracking 系统,可以用来发现在网络子系统中出现的这类 bug,并且希望有一天也能用来帮助整个内核检测这种问题。
由于引用计数本身是匿名的,所以引用技术的操作中的错误就很难被发现。比如说,虽然可以比较容易地确认出这个对象的某个使用者在不再需要该对象的时候忘记了释放对它的引用(也就是没有递减引用技术),但是通常没有什么简单的办法知道具体是哪个使用者。因此,内核最终会有一个无法释放的、未被使用的对象,但是没有办法知道引用计数机制是因为什么而失效了,甚至没有办法知道具体是哪个 reference 被丢失了。如果有一种方法可以确定一个对象的几十个引用中的哪一个被遗忘了导致的泄露,那么就会很容易来找到这个存在 bug 的引用释放部分的代码。
Dumazet 的 patch set 通过创建一个跟踪(tracking)机制来解决这个问题,该机制基本上是把使用引用计数的地方包起来。使用这个引用计数跟踪机制的第一步,就是添加这个 tracker 本身,这是使用下面这个 ref_tracker_dir 结构来完成的:
struct ref_tracker_dir {
#ifdef CONFIG_REF_TRACKER
spinlock_t lock;
unsigned int quarantine_avail;
refcount_t untracked;
struct list_head list; /* List of active trackers */
struct list_head quarantine; /* List of dead trackers */
#endif
};
这个结构不包含引用计数本身,相反,它要被添加到包含了我们感兴趣的那个引用计数的对象之中。正如人们可以预料到的那样,它的第一个使用场景就是在到庞大的 net_device 结构中去,这个结构是用来管理网络设备的。这些结构在网络子系统的许多地方都被引用到,很容易出现引用计数错误。引用的获取和释放非常频繁,以至于都专门定制了一种 per-CPU 的引用计数机制。万一发生了一个引用泄露,就会导致系统无法移除一个网络设备,这进而又会阻碍 container 或虚拟机的清理工作。为了减少查出这些错误耗费的力气,Dumazet 的 patch set 首先在 net_device 结构中加入了一个 ref_tracker_dir 结构。这个结构被初始化(通过调用 ref_tracker_dir_init() 来初始化)时 list 是空的,quarantine_avail(会在后面再介绍)设置为调用者所提供的值。
要想使引用计数跟踪机制正常工作,每一个获取或释放引用的代码都必须要告诉 tracker 机制这一点。当引用计数操作已经封装在其他一些函数中的时候,这一点相对容易做到。否则可能需要对代码进行相当多的改动。每当代码需要获取这个对象的引用时,它应该调用:
int ref_tracker_alloc(struct ref_tracker_dir *dir, struct ref_tracker **trackerp,
gfp_t gfp_flags);
dir 参数是指向添加到引用计数对象中的那个 ref_tracker_dir 结构。这个函数会分配一个 ref_tracker 结构来跟踪这个特定的引用,使用这里提供的 gfp_flags,并将其地址存储在 *trackerp 中。返回值是 0 或错误代码(如果无法分配成功 ref_tracker 结构的话很可能是返回-ENOMEM)。
ref_tracker_alloc() 将把这个新的结构添加到 ref_tracker_dir 结构的 list 中,从而意味着已经获取了一个引用。不过,要想真正获取有价值的信息的话,这个跟踪机制必须要以某种方式来记录下来这个特定的引用是在哪里被获取的。获取引用的函数的名字虽然也会有用,但其实真正出错的地方是在函数调用栈往上走基层的位置,所以还需要更多的信息来确定问题的真正来源。这个追踪机制中使用了一个名为 "stackdepot "的内核调试功能,它能够生成并保存完整的 stack trace,stackdepot 严格来说是没有文档的,可以查看 lib/stackdepot.c 的源代码来了解它。通过存储完整的 stack trace,这个引用技术追踪机制才有能力真正揭示引用计数问题真正来自哪里。
当一个引用被释放时,必须要调用:
int ref_tracker_free(struct ref_tracker_dir *dir, struct ref_tracker **trackerp);
这个函数会做不少事情。首先将 trackerp 指向(间接指向)的 tracker 跟踪器从 list 中移除。跟踪器内部会标记为已被释放,但该结构本身不会被立即释放,而是添加到 quarantine list (隔离列表)中,并将当前的 stack trace 存储下来。这样做是为了能捕捉那些 double-free 类型的 bug。如果 ref_tracker_free() 再次调用的时候是使用了一个被标记为已经被释放的 ref_tracker 结构,那么会立即生成一份报告,显示与分配事件和两个释放事件等操作相关的 stack trace 信息。
对于那些很繁忙的对象,quarantine list 很容易增长的太大了,所以上面提到的 quarantine_avail 数值就是被用来限制这个 list 的长度的。每当一个跟踪器被添加到 quarantine 区域时,就会检查这个数字,如果 quarantine_avail 为零,quarantine list 中存放最长时间的那个 track 就会被释放;不为零的话则对这个数字减一。quarantine_avail 的初始值是在 ref_tracker_dir 初始化时指定的,对于网络设备来说可以初始设置是允许存放 128 条。
double-free 的 bug 可以被立即捕获出来,但引用泄露的情况则只有在释放引用计数对象的时候才能被检测到。当内核被要求移除一个网络设备时,它会一直等待到该设备上的引用计数达到零。而如果有引用泄漏的话,就不会达成这个条件,,直到系统被重新启动才能完成释放。当然,到了那个时候,之前保存的关于哪个引用被泄露的信息早已不复存在,这是在引用计数跟踪机制出现之前的现实情况。任何对对象的引用,如果没有被释放的话,仍然会存在对应的 active ref_tracker 结构。直接调用一下 ref_tracker_dir_print() 就可以把所有跟泄露的引用相关的 stack trace 都打印到系统日志里。
这种机制的优势很明显:它应该能够找出引用计数的错误,简化了调试这些错误的步骤。但是另一方面来说,这是一个相当耗费资源的机制,不适合在生产系统中使用。这个实现还需要将引用计数 tracking 机制要专门针对每个想要使用它的子系统中都进行修改,而且代码变动可能还不小;在 Dumazet 的 23 个 patch 之中,有 21 个是专门用于对 net_device 结构进行检测的。如果能直接、透明地基于 refcount_t 类型来开发出一个调试机制的话,那会更加可行、更少干扰,但它无法配合与网络设备中使用的 one-off (一次性)机制共同使用。
因此,这项工作还不是一个通用的引用计数调试工具,但它是朝着这个方向迈出的重要一步。这些拼图就在那里等待着有足够动力的开发者来把它们拼成更加通用的功能。同时,现在的代码应该已经可以帮助减少网络子系统的代码中的引用计数 bug 数量了,这是一个很好的开始。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~