手工打造一把锁

接上篇(https://yq.aliyun.com/articles/59034 ),我们知道了lock的意义。回到之前的多线程加法操作,你也可以通过pthread提供的互斥锁来保证结果是正确的。但互斥锁本身是如何保证原子性的呢?当然首先获得锁的操作需要是一个指令,而不能用加载-比对-存储这种类似的三条指令;其次,如果是多核系统上,这个指令本身还需要加上lock前缀。x86提供了cmpxchg指令(http://x86.renejeschke.de/html/file_module_x86_id_41.html ),这类指令有个名词,就是CAS(Compare-and-swap, https://en.wikipedia.org/wiki/Compare-and-swap )便是许多锁操作实现的基础。一个最简单的实现方案大概如下(x86-64, gcc):


typedef volatile unsigned long lock_t;

void foobar_lock(lock_t *lock)
{
    unsigned long old   = 0;
    unsigned long new = 1;
    unsigned char result;
    while (1) {
        asm volatile(
            "lock \n"
            "cmpxchgq  %3, %1 \n"
            "sete      %0\n"
            : "=a" (result) : "m" (*lock), "a" (old), "r"(new) : "cc", "memory");
        if (result) {
            return;
        }
        sched_yield();
    }
}

void foobar_unlock(lock_t *lock)
{
    *lock = 0;
}

cmpxchg会比较rax寄存器的内容,和指定内存的内容,如果相等,会设置ZF=1,并将内存的内容改写为指定的其他内容;否则,将设置ZF=0,并将指定内存位置的内容复制到rax寄存器。而这些是在一条指令内完成的。

对于互斥锁的实现而言,初始为0,已锁为1,通过比较锁所在内存和0,相等则将其设置为1,并设置ZF=1,表示加锁成功。然后我们通过sete来获得ZF的值来判断锁的结果,失败则继续尝试。

之所以需要将0和1先存储到unsigned long,而不是"a"(0)这种立即数的形式,因为发现即便在64位平台,对于这种形式,gcc分配的寄存器是eax,而不是rax。

然后写一段测试代码,代码和上篇文章类似,结果正常:

手工打造一把锁

如果你把lock前缀去掉,同时通过taskset设置单个核心,那么也没有问题。

但是如果去掉了lock前缀,同时又允许多个核心运行,有可能结果小于正确数字,有可能程序hang住。前者可以理解,由于没有lock前缀,会产生两个线程都认为自己加锁成功了,进而同时进入了临界区;后者的情况相对复杂,通过gdb attach到那个进程去,会发现两个线程内部,看到的锁所在的内存,都是1,然额还不知道这是缓存一致性导致的问题,还是乱序执行导致的问题。

上一篇:通过Python实现简单的tail -f功能


下一篇:QTP的那些事--通过WMI获取session中的用户信息的验证码