代码之家  ›  专栏  ›  技术社区  ›  Joseph Garvin

如何有效地调试共享内存中的引用计数问题?

  •  5
  • Joseph Garvin  · 技术社区  · 15 年前

    假设在共享内存中有一个引用计数的对象。引用计数表示使用对象的进程数,进程负责通过原子指令递增和递减计数,因此引用计数本身也在共享内存中(它可能是对象的字段,或者对象可能包含指向该计数的指针,如果有建议,我愿意接受它们)协助解决这个问题)。有时,一个进程会有一个错误,阻止它减少计数。如何尽可能容易地找出哪个进程不递减计数?

    我想到的一个解决方案是给每个进程一个uid(可能是它们的pid)。然后当进程减少时,它们将其uid推送到存储在引用计数旁边的链接列表中(我选择了一个链接列表,因为您可以使用 CAS )当您想要调试时,您有一个特殊的进程,它会查看共享内存中仍然存在的对象的链接列表,而列表中没有的应用程序的uid就是那些还没有减少计数的应用程序。

    这个解决方案的缺点是它有O(N)内存使用,其中N是进程数。如果使用共享内存区域的进程数量很大,并且您有大量对象,那么这很快就会变得非常昂贵。我怀疑可能有一个中间的解决方案,通过使用部分固定大小的信息,您可以通过某种方式缩小可能进程的列表来帮助调试,即使您无法确定单个进程。或者,如果您可以只在一个进程没有减量的情况下检测哪个进程没有减量(即无法处理2个或多个进程未能减量计数的检测),这可能仍然是一个很大的帮助。

    (对于这个问题有更多的“人工”解决方案,比如确保所有的应用程序都使用同一个库来访问共享内存区域,但是如果共享区域被视为二进制接口,并且不是所有的进程都是由您编写的应用程序,那么这将超出您的控制范围。另外,即使所有的应用程序都使用同一个库,一个应用程序可能在库外有一个bug,以防止它减少计数的方式损坏内存。是的,我使用的是不安全的语言,如C/C++;

    编辑:在单进程情况下,您将拥有控制权,因此可以使用 RAII (在C++中)。

    5 回复  |  直到 15 年前
        1
  •  7
  •   caf    15 年前

    每个对象只能使用一个额外的整数。

    将整数初始化为零。当一个进程增加对象的引用计数时,它将其PID转换为整数:

    object.tracker ^= self.pid;
    

    当进程减少引用计数时,它也会这样做。

    如果引用计数保留为1,则跟踪整数将等于进程的PID,该进程递增但不递减。


    这是因为xor是交换的( (A ^ B) ^ C = A ^ (B ^ C) ,因此,如果一个进程使用它自己的PID对跟踪器执行偶数次XORing,这与XORing相同。 PID ^ PID -这是零,不影响跟踪器值。

    您也可以使用无符号值(定义为换行而不是溢出)-在递增使用计数时添加PID,在递减时减去PID。

        2
  •  1
  •   Will    15 年前

    从根本上讲,共享内存共享状态不是一个健壮的解决方案,我不知道如何使它健壮。

    最后,如果一个进程退出,那么它的所有非共享资源都将由操作系统清除。顺便说一下,这是使用进程(fork())而不是线程取得的巨大胜利。

    但是,共享资源不是。其他人打开的文件句柄显然没有关闭,而且… 共享内存 .只有在最后一个共享资源的进程退出后,才会关闭共享资源。

    假设您在共享内存中有一个PID列表。一个进程可以扫描这个列表以查找僵尸,但是PIDS可以被重用,或者应用程序可能挂起而不是崩溃,或者…

    我的建议是在每个进程之间使用管道或其他消息传递原语(有时有一个自然的主从关系,其他时候都需要与所有人交谈)。然后,您可以利用操作系统在进程死亡时关闭这些连接,这样在这种情况下,您的对等方就会收到信号。此外,还可以使用ping/pong超时消息来确定对等机是否挂起。

    如果在分析之后,发送这些消息中的实际数据效率太低,那么只要将控制通道保持在操作系统清除的某种流上,就可以使用共享内存作为有效负载。

        3
  •  1
  •   Daniel Earwicker    15 年前

    最有效的资源所有权跟踪系统甚至不使用引用计数,更不用说引用持有者列表了。它们只包含关于内存中可能存在的每个数据类型的布局的静态信息,以及每个函数的堆栈框架的形状,并且每个对象都有一个类型指示器。因此,调试工具可以扫描每个线程的堆栈,并递归地跟踪对对象的引用,直到它具有内存中所有对象的映射以及它们如何相互引用为止。当然,具有这种功能的系统也有自动垃圾收集功能。他们需要来自编译器的帮助来获得关于对象和堆栈帧布局的所有信息,并且这些信息实际上不能在所有情况下可靠地从C/C++中获得(因为对象引用可以存储在联盟中,等等),它们在运行时比引用计数更好地执行。

    根据您的问题,在“退化”的情况下,除了堆栈上的局部变量之外,进程的所有(或几乎所有)状态都将保存在共享内存中。在这一点上,您将在一个进程中拥有与多线程程序完全相同的功能。或者换句话说,共享足够内存的进程开始变得与线程不可区分。

    这意味着您不需要指定问题的“多进程、共享内存”部分。当任何人试图使用引用计数时,都会面临同样的问题。那些使用线程(或者不受限制地使用共享内存;同样的事情)的人面临另一组问题。把两个放在一起,你就会有一个痛苦的世界。

    一般来说,如果可能的话,最好不要在线程之间共享可变对象。具有引用计数的对象是可变的,因为可以修改该计数。换句话说,您正在(有效的)线程之间共享可变对象。

    我想说的是,如果您对共享内存的使用足够复杂,需要类似于GC的东西,那么您几乎得到了这两个世界中最糟糕的一个:没有过程隔离的优势,过程创建的代价是昂贵的。实际上,您已经编写了一个多线程应用程序,在该应用程序中,您正在线程之间共享可变对象。

    本地套接字是一个非常跨平台和非常快速的进程间通信API;它是唯一一个在所有unice和windows上基本相同的接口。所以考虑使用它作为一个最小的通信通道。

    顺便问一下,您是否在保存引用的进程中一致地使用智能指针?这是你唯一的希望得到参考计数,甚至一半正确。

        4
  •  0
  •   Artyom    15 年前

    使用以下

    int pids[MAX_PROCS]
    int counter;
    

    增量

    do
       find i such pid[i]=0  // optimistic
    while(cas[pids[i],0,mypid)==false)
    my_pos = i;
    atomic_inc(counter)
    

    减量

    pids[my_pos]=0
    atomic_dec(counter);
    

    所以您知道所有使用这个对象的进程

    MAX_PROCS 足够大,免费搜索 随机放置,因此如果进程数量显著低于 马克斯普罗普斯 搜索 会很快的。

        5
  •  0
  •   Ritsaert Hornstra    15 年前

    除了自己动手:你还可以使用一些工具,比如aqtime,它有一个引用计数的memchecker。