读 Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation 这篇论文时看到了一种实现 GIL 的方式。
先介绍一下为什么 Valgrind 需要 GIL。Valgrind instrument 二进制文件并且执行采用的方式叫做 Disassemble-and-resynthesise (D&R)。其大概过程如下:
- 将原二进制代码反汇编到中间代码
- Valgrind 插件对得到的中间代码插入做检查的中间代码
- 对中间代码做优化之后,生成 native 代码
由于中间代码是 RISC like 的,一条 CISC 的读写指令操作可能对应多条中间代码,最后生成的对应原先读写指令的 native code 有多条指令组成。在多线程的情况下这会使得读写操作不再是原子性的。(即使在单核机器上也会有这个问题,因为线程可能在执行一条读指令的过程中被调度走。)
Valgrind 解决这个问题的方式是让多线程程序在任何时候都只有一个线程在执行,这就需要一个类似 Python/Ruby 解释器里 GIL 的东西,在 Valgrind 中这个机制实现很巧妙。
Valgrind 利用一个 pipe 来实现这个机制。尝试拿锁的线程都尝试从全局的 pipe 中读,由于 pipe 中没有内容可读而 block 住。锁拥有者在解锁时往 pipe 中写一个字节,OS 会保证只有一个线程被唤醒,读出 pipe 中内容然后往下执行,其他线程则继续 sleep。这个方案里究竟那个线程被唤醒还是由 OS 决定,但可以严格保证任何时候都只有一个线程执行。(用 pthread mutex 的话还是会在 spin 的时候有一些线程同时执行,虽然对 Valgrind 的应用场景来说应该没有什么问题。)
Valgrind 的论文还是挺有意思的。虽然也是 binary translation,但它没有使用 code chaining,而是用手写的汇编来提高从 translated code 执行跳出后再次进入 translated code 的速度;翻译的单元也不是通常使用的 basic block 而是 super block。论文里提到的很多机制现在实现方式都是已经修改过多次,写出这样一个成熟的工具真是非常不容易。