pthread_mutex_lock源码分析

2570阅读 0评论2017-08-01 B_C_1024
分类:LINUX

Glibc版本:2.25.取重要的主线记一下,其他的一些代码尚未研究透彻,待研究透彻再补上,方便日后查阅。
当前版本中的mutex主要有两部份完成,一个是原子操作,另一个是futex。原子已在__lll_lock博客中记下了,futex属于内核部分,将在下一篇博客中介绍。

  1. int
  2. __pthread_mutex_lock (pthread_mutex_t *mutex)
  3. {
  4.   assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
  5.   unsigned int type = PTHREAD_MUTEX_TYPE_ELISION (mutex);
  6.   LIBC_PROBE (mutex_entry, 1, mutex);
  7.   if (__builtin_expect (type & ~(PTHREAD_MUTEX_KIND_MASK_NP
  8.                  | PTHREAD_MUTEX_ELISION_FLAGS_NP), 0))
  9.     return __pthread_mutex_lock_full (mutex);
  10.   if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_NP))
  11.     {
  12.       FORCE_ELISION (mutex, goto elision);
  13.     simple:
  14.       /* Normal mutex. */
  15.       LLL_MUTEX_LOCK (mutex);
  16.       assert (mutex->__data.__owner == 0);
  17.     }

如上代码第6行,获取mutex类型,共有四种类型:

PTHREAD_MUTEX_TIMED_NP: 这是最基本的一种,也称为normal,也是默认的类型,当多个进程同时竞争这种类型的mutex时,它们会按照优先级在内核排序,当有其他线程unlock的时候,唤醒其中一个等待的线程。

PTHREAD_MUTEX_RECURSIVE_NP: 这种类型从名字也可以看出,是recurisive类型的。如果线程没有获得该mutex的情况下,争用该锁,那么与PTHREAD_MUTEX_TIMED_NP一样。与PTHREAD_MUTEX_TIMED_NP不一样的是,线程在已获得该锁的情况下仍能获得该锁。

PTHREAD_MUTEX_ADAPTIVE_NP: 适应锁,网上搜到的资料,感觉不准确。从下面的源码的分析中理解,这种锁主要是提高性能的。我们都知道,当临界区比较小的,自旋锁比互斥锁有更高的性能。PTHREAD_MUTEX_ADAPTIVE_NP类型的mutex在上锁的时候,会先持续尝试一定次数的try-lock,try-lock并不会导致线程切换,当一定次数的trylock不成功,才会lock,lock可能会导致线程切换。对于小的临界区,很有可能在try-lock的过程中就上锁成功,避免了线程切换带了的额外开销,当然是提高了性能了。

PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。

  上面代码的10-12行没看懂,先不管吧。第14-20行针对的正是PTHREAD_MUTEX_TIMED_NP类型的锁。可以看到,并没有什么特别的处理,直接调用LLL_MUTEX_LOCK() ,该宏后面再解释,先把这这四种类型的锁先记完。

  1. else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex)
                 == PTHREAD_MUTEX_RECURSIVE_NP, 1))
  1.     {
  2.       /* Recursive mutex. */
  3.       pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
  4.       /* Check whether we already hold the mutex. */
  5.       if (mutex->__data.__owner == id)
  6.     {
  7.       /* Just bump the counter. */
  8.       if (__glibc_unlikely (mutex->__data.__count + 1 == 0))
  9.         /* Overflow of the counter. */
  10.         return EAGAIN;
  11.                                                                                                                                                                    
  12.       ++mutex->__data.__count;
  13.                                                                                                                                                                    
  14.       return 0;
  15.     }
  16.                                                                                                                                                                    
  17.       /* We have to get the mutex. */
  18.       LLL_MUTEX_LOCK (mutex);
  19.                                                                                                                                                                    
  20.       assert (mutex->__data.__owner == 0);
  21.       mutex->__data.__count = 1;
  22.     }
上面这段代码,对应着正是PTHREAD_MUTEX_RECURSIVE_NP类型的mutex。如上代码的3-5行,判断当前进程是否已经获得该锁,如果已获得该锁,8-16行,仅仅增加count就行了。如果当前线程没有获取该锁,就和PTHREAD_MUTEX_TIMED_NP类型的相同了。
  1. else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex)
  2.               == PTHREAD_MUTEX_ADAPTIVE_NP, 1))
  3.     {
  4.       if (! __is_smp)
  5.     goto simple;

  6.       if (LLL_MUTEX_TRYLOCK (mutex) != 0)
  7.     {
  8.       int cnt = 0;
  9.       int max_cnt = MIN (MAX_ADAPTIVE_COUNT,
  10.                  mutex->__data.__spins * 2 + 10);
  11.       do
  12.         {
  13.           if (cnt++ >= max_cnt)
  14.         {
  15.           LLL_MUTEX_LOCK (mutex);
  16.           break;
  17.         }
  18.           atomic_spin_nop ();
  19.         }
  20.       while (LLL_MUTEX_TRYLOCK (mutex) != 0); /* Try lock returns 0 means lock success */

  21.       mutex->__data.__spins += (cnt - mutex->__data.__spins) / 8;
  22.     }
  23.       assert (mutex->__data.__owner == 0);
  24.     }
上面这部分是对应的PTHREAD_MUTEX_ADAPTIVE_NP类型的mutex。第4行,__is_smp,不知道smp对这个 PTHREAD_MUTEX_ADAPTIVE_NP类型的锁有什么影响,后面懂了之后再加上来。从7-23行可以看到,对于这种类型的mutex,先调用用LLL_MUTEX_TRYLOCK,在调用LLL_MUTEX_LOCK。 LLL_MUTEX_TRYLOCK只是尝试用原子操作去修改mutex->__lock,如果修改成功,则表示上锁成功,返回0;如果修改不成功,上锁失败,返回非0(就是mutex->__lock的当前值)。上锁不成功的时候,LLL_MUTEX_TRYLOCK并不会发生系统调用,更不会阻塞当前线程。从上面的代码可以看到,在调用连续LLL_MUTEX_TRYLOCK失败一定次数之后,就会调用LLL_MUTEX_LOCK。从上面这个过程上看,PTHREAD_MUTEX_ADAPTIVE_NP更像是一个自旋锁了。
  1. else
  2. {
  3.       pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
  4.       assert (PTHREAD_MUTEX_TYPE (mutex) == PTHREAD_MUTEX_ERRORCHECK_NP);
  5.       /* Check whether we already hold the mutex. */
  6.       if (__glibc_unlikely (mutex->__data.__owner == id))
  7.     return EDEADLK;
  8.       goto simple;
  9.  }
上面这部分对应着PTHREAD_MUTEX_ERRORCHECK_NP类型的锁,从上面代码中可以看到这种类型的锁可以检测到进程在已经获得锁的时候,再次尝试获取该锁。这貌似正好和PTHREAD_MUTEX_RECURSIVE_NP类型的锁相反。如果不是在已获得锁的过程中再次请求该锁,那么处理流程就和PTHREAD_MUTEX_TIMED_NP一样了。

LLL_MUTEX_LOCK:
从上面可以看到,无论哪一种类型的mutex,都会调用到LLL_MUTEX_LOCK来进行上锁。上面还提到了LLL_MUTEX_TRYLOCK,先解决这个简单一点的。
  1. # define LLL_MUTEX_TRYLOCK(mutex) \
  2.   lll_trylock ((mutex)->__data.__lock)
  3. #define lll_trylock(lock)   \
  4.   atomic_compare_and_exchange_bool_acq (&(lock), 1, 0)
上面代码一看就很清楚了,实际上就是调用了atomic_compare_and_exchange_bool_acq这个原子操作了,这个原子操作在博文__lll_lock中已经记过了。所以LLL_MUTEX_TRYLOCK仅仅只是检测lock的值,如果等于0,就把lock改成1,然后返回0,表示上锁成功,如果lock不是0,表示别人已经获得了锁,直接返回lock的值,表示上锁不成功。LLL_MUTEX_TRYLOCK并不需要futex。
  1. # define LLL_MUTEX_LOCK(mutex) \
  2.   lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))
  3. #define lll_lock(futex, private)    \
  4.   __lll_lock (&(futex), private)
  5. #define __lll_lock(futex, private)                                      \
  6.   ((void)                                                               \
  7.    ({                                                                   \
  8.      int *__futex = (futex);                                            \
  9.      if (__glibc_unlikely                                               \
  10.          (atomic_compare_and_exchange_bool_acq (__futex, 1, 0)))        \
  11.        {                                                                \
  12.          if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \
  13.            __lll_lock_wait_private (__futex);                           \
  14.          else                                                           \
  15.            __lll_lock_wait (__futex, private);                          \
  16.        }                                                                \
  17.    }))
  18. void
  19. __lll_lock_wait_private (int *futex)
  20. {
  21.   if (*futex == 2)
  22.     lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2.  */

  23.   while (atomic_exchange_acq (futex, 2) != 0)
  24.     lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2.  */
  25. }
  26. __lll_lock_wait (int *futex, int private)
  27. {      
  28.    if (*futex == 2)   
  29.     lll_futex_wait (futex, 2, private); /* Wait if *futex == 2.  */
  30.   while (atomic_exchange_acq (futex, 2) != 0)          
  31.    lll_futex_wait (futex, 2, private); /* Wait if *futex == 2.  */
  32. }
  33. /* Wait while * FUTEXP == VAL for a lll_futex_wake call on FUTEXP */
  34. #define lll_futex_wait(futexp, val, private) \
  35.    ((void)) (private), \
  36.    -__nacl_irt_futex.futex_wait_abs((volatitle int *)(futexp), val, NULL))                          

  从上面的代码可以看到,LLL_MUTEX_LOCK会调用到__lll_lock,__lll_lock用atomic_compare_and_change_bool_acq()更改mutex->__lock的值,如果更改成功,表示mutex->__lock的原始值0,上锁成功。如果上原子比较交换指令不成功,就调用__lllock_wait/__lll_lock_wait_private()z这两个函数中的一个。

    __lll_lock_wait()和__lll_lock_wait_private()这两个函数基本一样。在__lll_lock_wait()这个函数中,首先判断futex是不是2,如果是2就直接调用lll_futex_wait(),准备futex的系统调用。前面已经说过,futex(mutex->__lock)的值只有是0的时候才表示锁空着,所以2当然是已经被其他的线程获得了锁,自然需要调用futex系统调用等待。

      为什么是2?这里有必要记录一下。起始观察整个pthread_mutex_lock中的源码可以看到,mutex->__lock的值只有三种可能:0,1,2。

      0:很显然,没有人获得锁的情况下自然是0。

      1:当只有一个线程调用pthread_mutex_lock()的时候,mutex->__lock的值被更改为1,如果在此之后没有其他的线程争用该mutex,那么这个mutex->__lock的值就会一直未1,知道unlock把他更新为0。

      2:在值为1的时候,如果再次有其他的线程调用pthread_mutex_lock(),mutex->__lock的值就会被设置成2。所以当有多个线程争用同一把锁的时候,该值就会被设置成2。

      所以这里,1和2表示了获得锁情况下,两种不同状态,1表示锁已被线程获得,但是没有竞争,2表示锁被线程获得,而且存在竞争。这样做的目的是为了提高pthread_mutex_unlock的效率。在pthread_mutex_unlock()中,会调用atomic_exchange_rel()无条件的把mutex->__lock的值更新为0,并且检查mutex->__lock的原始值,如果原始值为0或者1,表示没有竞争发生,自然也就没有必要调用futex系统调用,浪费时间。只有检查到mutex->__lock的值大于1的时候,才需要调用futex系统调用,唤醒等待该锁上的线程。

     pthread_mutex_unlock()无条件的更新mutex->__lock的值为0,并不会造成有竞争的时候,mutex->__lock的值为0的状态。原因请看LLL_MUTEX_LOCK()代码段的20行和31行,这里是一个while循环,也就说当等待该锁的线程在被唤醒时首先会执行这个while循环,只有一个会把mutex->__lock的值更新2,而且这个成功更新的线程或获得该锁。

     至此,pthread_mutex_lock的用户空间代码就分析完了,至于lll_futex_wait()怎到系统调用,这个大概需要编译器在连接的以后进行一些处理吧,现在还不清楚,但并不影响对pthread_mutex_lock()的理解。后面在分析一下内核空间futex干了些什么。


上一篇:__lll_lock
下一篇:linux内核同步机制completion机制