GC竞争问题

阅读Lua GC源码中,我们就提到过一个细节,所有带有__gc函数的对象,在第一轮GC循环中只会执行__gc函数,直到第二轮GC才真正清除。

一直没有找到必须这样做的场景,直到最近我发生了一例GC竞争的bug之后,才恍然大悟。回过头想想,其实在我之前翻译Barry Hayes大神的一篇论文里也早都提到过,只不过当时例子是释放OS资源,而场景也太过抽象,才没有引起我的注意。下面来看一个MWE。

我已经尽可能的精简代码,然而还是需要170+LOC。

在这个例子里,实现了一个链表管理,所有的link和node结构均交由Lua GC来自动管理内存。

由于Lua GC不能分析C结构之间的引用关系,因此所有的node(userdata)必须通过一个Table来保持引用,以防GC误回收。这也是为什么在luaopen_link函数中,我们为所有函数绑定了一个相同的UpVal(Table)。

下面来分析为什么会有竞争的发生。

首先这个UpVal是全局的,也就是会与luaVM同生共死。所以`UpVal中node对象的生命周期` >= `link的生命周期`。

因此竞争问题只会出现在UpVal的生命周期与link的生命周期一同结束时。假设UpVal和link的生命周期都在GC 循环C1中结束。没有机制能保证UpVal先于或后于link死亡,因此node对象的__gc和link.free在这一周期是竞争执行的。这就是RACE1和RACE2都需要将buff置为NULL的原因,只要有一方不置为NULL另一方就有可能出现double free。

之所以置NULL不会有memory corruption问题. 是因为在Lua GC实现中,所有带__gc函数的对象,在当前GC循环死亡后并不会立即释放内存,而是会等到下一轮GC循环才会真正释放。换句话说只要在本轮GC循环中,不管什么时间访问操作node指针都是有效的。

反过来讲,由于在释放过程中可能存在竞争或释放过程中循环依赖的情况。GC模块要保证在执行__gc函数过程中,所有需要的数据都是有效的,就必须要延迟一个GC循环来回收内存。

ps.其实上面的竞争问题可以通过其他手段解决,比如为link设置一个__gc函数来释放buff, 移除掉node对象中__gc函数的释放buff行为,但是不管怎么样竞争场景确实存在,比如两个user data相互引用,并被同一个Table持有等。

发表评论

five × = twenty