pthread的lowlevellock
pthread的lowlevellock是futex的最简单的锁应用。也是pthread其它同步原语最基本的锁。lowlevellock提供(或实现)了三种锁(方法),一是基于0或1的互斥的锁规则,二是基于robust futex定义的锁规则,三是用于condition重新对临界区上锁的操作。
lowlevellock使用的是non-pi futex。futex的锁规则由用户空间来定义,这里的用户空间是glibc。
normal lowlevellock 定义的0或1互斥的futex锁规则为:
0,锁无持有者。
1,锁有持有者。
2,锁竞争。
两个锁操作 __lll_lock 和 __lll_unlock。
上锁操作 __lll_lock:
使用atomic_compare_and_exchange尝试对futex上锁,如果这时锁状态为0,完成上锁,futex标记状态为1;否则必须标记futex为锁竞争状态,转而进入锁竞争等待,调用futex系统调用的futex_wait操作进行排队。因为用户空间并不知道内核的futex队列中是否还有其它锁竞争的任务在等待,所以系统调用阻塞唤醒回到用户空间,对futex尝试上锁,必须以锁竞争状态来上锁,以使自己解锁时,会调用futex_wake。
// lll lock fastpath #define __lll_lock(futex, private) \ ((void) \ ({ \ int *__futex = (futex); \ if (__glibc_unlikely \ (atomic_compare_and_exchange_bool_acq (__futex, 1, 0))) \ { \ if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \ __lll_lock_wait_private (__futex); \ else \ __lll_lock_wait (__futex, private); \ } \ }))
// lll lock slowpath void __lll_lock_wait_private (int *futex) { if (*futex == 2) lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2. */ while (atomic_exchange_acq (futex, 2) != 0) lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2. */ }
解锁操作 __lll_unlock:
函数在用户空间直接用atomic_exchange将futex状态替换为0,如果发现原来状态为锁竞争,则调用futex系统调用的futex_wake操作,从内核的futex_queue中选出等待任务,将其唤醒,使唤醒的任务回到用户空间完成上锁。
// lll unlock #define __lll_unlock(futex, private) \ ((void) \ ({ \ int *__futex = (futex); \ int __private = (private); \ int __oldval = atomic_exchange_rel (__futex, 0); \ if (__glibc_unlikely (__oldval > 1)) \ lll_futex_wake (__futex, 1, __private); \ }))
上锁操作使用atomic_compare_and_exchange函数进行fastpath尝试,确保与其它锁操作不相容(排它)并且保证状态转换是从0到1,修改可能会失败。
上锁操作使用atomic_exchange函数进行slowpath尝试。slowpath的上锁操作只要修改为锁竞争,并不需要确保锁从何种状态下才能转换状态。当状态从0进行转换,表示无锁,但是现在处理slowpath,表示任务是从锁竞争排队中被唤醒的,可能还有其它任务进行锁竞争而排队。当状态从1进行转换,虽然任务被唤醒了,但是锁被另一任务的fastpath执行偷了,仍然是锁竞争。当状态从2进行转换,无可非议锁竞争。所以在slowpath的上锁操作只要修改为锁竞争状态,并且根据原状态来判断锁的状态转换次序,确认自己是否上锁。
解锁操作使用atomic_exchange直接修改锁状态为无锁。并不需要确保状态转换的关系,也不用参照状态转换的前状态,所以解锁是不会失败的。
__lll_lock_wait函数是上锁操作的 slowpath,它包含了一个循环trylock-wait-loop,在循环中不断重复尝试上锁和失败进行内核排队,直到上锁操作成功被接受。而__lll_unlock解锁操作则对锁进行一次绝对的修改,然后决定是否要进入内核唤醒阻塞的任务。为什么阻塞的任务被唤醒,并不意味这个任务得到了锁,只是被通知解锁事件,这个任务必须再次尝试上锁。为什么被唤醒任务不在内核进行上锁操作,确保锁得到后才返回用户空间,因为处于内核的调用系统并不知道上锁的规则。又由于解锁操作和唤醒阻塞队列是分开的,会futxe唤醒的系统调用也不会理解futex的锁规则,只作单纯的阻塞队列唤醒工作。因此在解锁一刻起,有一线程(运行在另一CPU)发起上锁操作,马上就可以完成上锁,而被__lll_unlock唤醒的任务并没有取得锁,待它回到用户空间时,它尝试上锁就会发现锁被使用了,再次进入内核回到阻塞队列。
robust lowlevellock 使用robust futex定义的锁规则:
futex分为三个状态字段,是否上锁,是否锁竞争,是否持锁线程死。
是否上锁,为整型的0-29位,0代表无锁,非0代表上锁,并且上锁状态同时保存持锁线程的id号。
是否持锁线程死,为整型30位。
是否锁竞争,为整型31位。
两个配对锁操作 __lll_robust_lock 和 __lll_robust_unlock。
它与前的非robust规则的lowlevellock的异同。
一,都使用non-pi futex的futex系统调用操作futex_wait和futex_wake进行锁竞争仲裁。执行系统调用的内核并不理会锁规则,同时上锁和解锁操作都在用户空间进行,内核只进行排队和唤醒。
二,锁都是二态互斥。0代表无锁,非0代表上锁。不同的是robust规则用tid代替非0状态,而非robust规则将上锁为1。
三,robust锁(lowlevellock)将锁竞争状态与上锁状态分开两个字段标记,而非robust锁(lowlevellock)将锁竞争状态作为上锁状态的另一形态。
四,robust锁必须标记持锁线程的生存状态,以供robust算法使用。
五,虽然robust lowlevellock使用了robust锁规则,但并没有提供robust处理服务,同时也没有应用futex提供的robust特性。
我们从代码来比较它们的区别:
normal lowlevellcok 和 robust lowlevellock 在 lock-fastpath 的区别:
相同的算法骨架,先使用atomic_compare_and_exchange函数尝试上锁,确保上锁从状态0转换,失败后进入slowpath。
normal lowlevellcok 和 robust lowlevellock 在 lock-slowpath 的区别:
相同的算法骨架,在循环中尝试上锁,上锁令锁进入锁竞争状态,根据转换前的状态确认自己是否上锁,发现上锁失败后使用futex_wait进入内核进行排队阻塞。
不同的是 robust lowlevellock 使用atomic_compare_and_exchange进行尝试,确保持锁线程的tid不被错入。当发现有锁持有线程死了必须返回,由锁的使用层来进行锁的恢复。
normal lowlevellcok 和 robust lowlevellock 在 unlock 的区别:
两者都通过atomic_exchange直接对锁状态进行修改为0,无视锁当前状态。如果发现锁状态修改前存在锁竞争,进入内核唤醒阻塞任务。
这里注意的是,robust锁unlcok和发生owner died,两种情况是不相容的,因为发生owner died表示任务没机会进行unlock,当任务unlock时不可能owner died,自己不是作为owner现在活得好好的进行unlock。
最后就是用于condition重新进行临界区的上锁操作 __lll_cond_lock。
由于condition是从一个临界区中进入一个条件的阻塞,阻塞在条件变量的期间离开临界区,条件变量通知时重新进入临界区。由于重新进入临界区时,不是通知fastpath上锁,而是从wait_requeue系统调用中返回,相当于slowpath,所以这时的上锁操作与slowpath一样。