写点什么

C++ 学习 ---_IO_lock_t 的源码学习

作者:桑榆
  • 2022 年 9 月 19 日
    捷克
  • 本文字数:7414 字

    阅读完需:约 24 分钟

引言

_IO_lock_t 是 GLibc 库中广泛用于 IO 读取的锁,关于它的结构体,相关的函数使用是相当有研究价值的,尤其是使用了大量的宏技术。我们就来深入分析学习一下_IO_lock_t 的实现机制

代码位置

glibc 源码下载:https://www.gnu.org/software/libc/libc.html

glibc/sysdeps/nptl/stdio-lock.h

结构体定义

非常简单:

  • int lock---lock 状态

  • int cnt---持有锁计数

  • void* owner---线程对象指针

typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;
复制代码

宏定义

_IO_lock_t_defined---标识_IO_lock_t 已定义

#define _IO_lock_t_defined 1
复制代码

_IO_lock_initializer---初始化器

即上面结构体的赋初值,分别为 0,0,NULL。

#define _IO_lock_initializer { LLL_LOCK_INITIALIZER, 0, NULL }
//glibc/sysdeps/nptl/lowlevellock.h170 /* Initializers for lock. */171 #define LLL_LOCK_INITIALIZER (0)
复制代码

_IO_lock_init---变量赋初值

宏展开后为((void)((_name)=(_IO_lock_t) {(0),0,NULL}))

#define _IO_lock_init(_name) \   ((void) ((_name) = (_IO_lock_t) _IO_lock_initializer))
复制代码

_IO_lock_fini---变量销毁

实际上是空语句

#define _IO_lock_fini(_name) \   ((void) 0)
复制代码

_IO_lock_lock---lock

使用 do{...}while(0)保证语句只执行一次

 37 #define _IO_lock_lock(_name) \ 38   do {                                        \ 39     void *__self = THREAD_SELF;                           \ 40     if (SINGLE_THREAD_P && (_name).owner == NULL)                 \ 41       {                                       \ 42     (_name).lock = LLL_LOCK_INITIALIZER_LOCKED;               \ 43     (_name).owner = __self;                           \ 44       }                                       \ 45     else if ((_name).owner != __self)                         \ 46       {                                       \ 47     lll_lock ((_name).lock, LLL_PRIVATE);                     \ 48     (_name).owner = __self;                           \ 49       }                                       \ 50     else                                      \ 51       ++(_name).cnt;                                  \ 52   } while (0)
复制代码
  1. 首先获取当前的线程描述符 x86_64 架构中的 THREAD_SELF 实现(glibc/sysdeps/x86_64/nptl/tls.h)

实际上就是获取 pthread*

171 /* Return the thread descriptor for the current thread.172 173    The contained asm must *not* be marked volatile since otherwise174    assignments like175     pthread_descr self = thread_self();176    do not get optimized away.  */177 # if __GNUC_PREREQ (6, 0)178 #  define THREAD_SELF \179   (*(struct pthread *__seg_fs *) offsetof (struct pthread, header.self))180 # else181 #  define THREAD_SELF \182   ({ struct pthread *__self;                              \183      asm ("mov %%fs:%c1,%0" : "=r" (__self)                   \184       : "i" (offsetof (struct pthread, header.self)));            \185      __self;})186 # endif
复制代码
  1. SINGLE_THREAD_P(判断当前进程是否是单线程),THREAD_GETMEM 对应读取 thread descriptor 中的对应成员变量,直接内嵌汇编,读取 header.multiple_threads 的值,判断是否等于 0

//glibc/sysdeps/unix/sysv/linux/single-thread.h 26 /* The default way to check if the process is single thread is by using the 27    pthread_t 'multiple_threads' field.  However, for some architectures it is 28    faster to either use an extra field on TCB or global variables (the TCB 29    field is also used on x86 for some single-thread atomic optimizations). 30  31    The ABI might define SINGLE_THREAD_BY_GLOBAL to enable the single thread 32    check to use global variables instead of the pthread_t field.  */ 33  34 #if !defined SINGLE_THREAD_BY_GLOBAL || IS_IN (rtld) 35 # define SINGLE_THREAD_P \                           36   (THREAD_GETMEM (THREAD_SELF, header.multiple_threads) == 0) 37 #else 38 # define SINGLE_THREAD_P (__libc_single_threaded_internal != 0) 39 #endif   19 /* Read member of the thread descriptor directly.  */ 20 # define THREAD_GETMEM(descr, member) \      21   ({ __typeof (descr->member) __value;                        \ 22      _Static_assert (sizeof (__value) == 1                    \ 23              || sizeof (__value) == 4                     \ 24              || sizeof (__value) == 8,                    \ 25              "size of per-thread data");                  \ 26      if (sizeof (__value) == 1)                           \ 27        asm volatile ("movb %%fs:%P2,%b0"                      \ 28              : "=q" (__value)                         \ 29              : "0" (0), "i" (offsetof (struct pthread, member)));     \ 30      else if (sizeof (__value) == 4)                          \ 31        asm volatile ("movl %%fs:%P1,%0"                       \ 32              : "=r" (__value)                         \ 33              : "i" (offsetof (struct pthread, member)));          \ 34      else /* 8 */                                     \ 35        {                                      \ 36      asm volatile ("movq %%fs:%P1,%q0"                    \ 37                : "=r" (__value)                       \ 38                : "i" (offsetof (struct pthread, member)));        \ 39        }                                      \ 40      __value; })
复制代码

3.第一种情况:如果当前是单线程而且传入的_name.owner 为空,说明是刚 init 成功后的空_IO_lock_t,那么对 lock 和 owner 成员变量进行初始化,lock 初始化为 1,表示锁住,owner 保存当前线程描述符__self

 42     (_name).lock = LLL_LOCK_INITIALIZER_LOCKED;               \ 43     (_name).owner = __self;  //glibc/sysdeps/nptl/lowlevellock.h 172 #define LLL_LOCK_INITIALIZER_LOCKED (1)
复制代码
  1. 第二种情况:如果传入的_name.owner 不等于__self,说明是之前被其它线程持有过,先使用 lll_lock 加锁,然后将_name.owner 保存为当前线程描述符__self;lll_lock(lowlevellock,底层锁)的逻辑后面讲解

 47     lll_lock ((_name).lock, LLL_PRIVATE);                     \ 48     (_name).owner = __self;  //glibc/sysdeps/nptl/lowlevellock-futex.h 47 /* Values for 'private' parameter of locking macros.  Yes, the 48    definition seems to be backwards.  But it is not.  The bit will be 49    reversed before passing to the system call.  */ 50 #define LLL_PRIVATE 0                                  51 #define LLL_SHARED  FUTEX_PRIVATE_FLAG
复制代码
  1. 第三种情况:不是前面两种情况,说明是当前线程,而且之前已经 lock 了,此时就++(_name).cnt,计数加一;

_IO_lock_trylock---trylock

trylock 的逻辑与 lock 的逻辑大致相似,首先获取当前线程描述符 THREAD_SELF,

  • 第一种情况:(_name).owner != __self,说明是之前其它线程持有过该 lock,那么调用 lll_trylock,返回值为 0 说明加锁成功,那么将_name.owner 更新为当前的线程描述符,否则将返回结果置为 EBUSY,忙状态;

  • 第二种情况:说明是当前线程,而且之前已经 lock 了,此时就++(_name).cnt,计数加一;

 54 #define _IO_lock_trylock(_name) \ 55   ({                                          \ 56     int __result = 0;                                 \ 57     void *__self = THREAD_SELF;                           \ 58     if ((_name).owner != __self)                          \ 59       {                                       \ 60         if (lll_trylock ((_name).lock) == 0)                      \ 61       (_name).owner = __self;                         \ 62         else                                      \ 63           __result = EBUSY;                           \ 64       }                                       \ 65     else                                      \ 66       ++(_name).cnt;                                  \ 67     __result;                                     \ 68   })
复制代码

_IO_lock_unlock---unlock

unlock 与 lock 的逻辑也大致相似:

  • 第一种情况:单线程且该线程中持有该锁的计数为 0,即在此次之前的 lock 和 unlock 都是匹配的,只剩一个 lock 没有匹配,那么将_name 变量的 owner 和 lock 恢复到初始值 NULL 和 0;

  • 第二种情况:不是单线程,但持有计数为 0,那么将(_name).owner 置为 NULL,同时调用 lll_unlock 真正解锁;

  • 第三种情况:持有计数不为 0,那就计数减一即可。

 70 #define _IO_lock_unlock(_name) \ 71   do {                                        \ 72     if (SINGLE_THREAD_P && (_name).cnt == 0)                      \ 73       {                                       \ 74     (_name).owner = NULL;                             \ 75     (_name).lock = 0;                             \ 76       }                                       \ 77     else if ((_name).cnt == 0)                            \ 78       {                                       \ 79     (_name).owner = NULL;                             \ 80     lll_unlock ((_name).lock, LLL_PRIVATE);                   \ 81       }                                       \ 82     else                                      \ 83       --(_name).cnt;                                  \ 84   } while (0)
复制代码

总结

_IO_lock_t 保存当前 lock 的状态,持有计数,当前线程信息,通过计数控制保证当前线程的 lock 和 unlock。

扩展知识

lll_lock

lock 初始化

//第一次调用_IO_lock_lock时初始化(_name).lock = LLL_LOCK_INITIALIZER_LOCKED;//第二次不同线程调用_IO_lock_lock时调用lll_lock,入参是LLL_LOCK_INITIALIZER_LOCKED和LLL_PRIVATElll_lock ((_name).lock, LLL_PRIVATE);
//glibc/sysdeps/nptl/lowlevellock.h 172 #define LLL_LOCK_INITIALIZER_LOCKED (1) //glibc/sysdeps/nptl/lowlevellock-futex.h 47 /* Values for 'private' parameter of locking macros. Yes, the 48 definition seems to be backwards. But it is not. The bit will be 49 reversed before passing to the system call. */ 50 #define LLL_PRIVATE 0 51 #define LLL_SHARED FUTEX_PRIVATE_FLAG
复制代码

lll_lock 的调用原理

实际上还是利用了底层的原子操作,LLL_PRIVATE 方式是调用__lll_lock_wait_private 加锁等待。atomic_compare_and_exchange_bool_acq (mem,newval,oldval)函数的作用是如果 mem 的值等于 oldval,则把 newval 赋值给 mem,返回 0,否则不做任何处理,返回 1.即,如果之前有线程释放了锁,lock=0,这时一个其它线程进来拿锁,futex=0,所以被置为 1,返回 0,此时没有资源冲突,lock 结束;

如果之前有线程还拿着锁,即 lock=LLL_LOCK_INITIALIZER_LOCKED=1,这时进来,futex!=0,所以什么都没有做,返回 1,此时出现资源冲突,进入等待,根据 private 区分为两种__lll_lock_wait_private 和__lll_lock_wait。

 //glibc/sysdeps/nptl/lowlevellock.h 84 /* This is an expression rather than a statement even though its value is 85    void, so that it can be used in a comma expression or as an expression  86    that's cast to void.  */ 87 /* The inner conditional compiles to a call to __lll_lock_wait_private if 88    private is known at compile time to be LLL_PRIVATE, and to a call to 89    __lll_lock_wait otherwise.  */ 90 /* If FUTEX is 0 (not acquired), set to 1 (acquired with no waiters) and 91    return.  Otherwise, ensure that it is >1 (acquired, possibly with waiters) 92    and then block until we acquire the lock, at which point FUTEX will still be 93    >1.  The lock is always acquired on return.  */ 94 #define __lll_lock(futex, private)                                      \ 95   ((void)                                                               \ 96    ({                                                                   \ 97      int *__futex = (futex);                                            \ 98      if (__glibc_unlikely                                               \ 99          (atomic_compare_and_exchange_bool_acq (__futex, 1, 0)))        \100        {                                                                \101          if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \102            __lll_lock_wait_private (__futex);                           \103          else                                                           \104            __lll_lock_wait (__futex, private);                          \105        }                                                                \106    }))107 #define lll_lock(futex, private)    \108   __lll_lock (&(futex), private)
复制代码

__lll_lock_wait_private 的源码如下:首先加载(原子操作)futex 的值如果等于 2,进入循环体内部,开始加入 futex 队列等待资源

//glibc/nptl/lowlevellock.c 24 void 25 __lll_lock_wait_private (int *futex)                                    26 { 27   if (atomic_load_relaxed (futex) == 2) 28     goto futex; 29  30   while (atomic_exchange_acquire (futex, 2) != 0) 31     { 32     futex: 33       LIBC_PROBE (lll_lock_wait_private, 1, futex); 34       futex_wait ((unsigned int *) futex, 2, LLL_PRIVATE); /* Wait if *futex == 2.  */ 35     } 36 }
复制代码

lll_trylock

实际上逻辑与 lll_lock 的逻辑一致,但没有在资源冲突时等待,而是返回非 0 值,表示获取失败,则在_IO_lock_trylock 中表示为 EBUSY,被占用。

 65 /* If LOCK is 0 (not acquired), set to 1 (acquired with no waiters) and return 66    0.  Otherwise leave lock unchanged and return non-zero to indicate that the 67    lock was not acquired.  */ 68 #define __lll_trylock(lock) \ 69   __glibc_unlikely (atomic_compare_and_exchange_bool_acq ((lock), 1, 0)) 70 #define lll_trylock(lock)   \                                          71    __lll_trylock (&(lock))
复制代码

lll_unlock

会通过__futex 的读取来唤醒处于等待队列中的任务拿到对应的资源

133 /* This is an expression rather than a statement even though its value is134    void, so that it can be used in a comma expression or as an expression135    that's cast to void.  */136 /* Unconditionally set FUTEX to 0 (not acquired), releasing the lock.  If FUTEX137    was >1 (acquired, possibly with waiters), then wake any waiters.  The waiter138    that acquires the lock will set FUTEX to >1.139    Evaluate PRIVATE before releasing the lock so that we do not violate the140    mutex destruction requirements.  Specifically, we need to ensure that141    another thread can destroy the mutex (and reuse its memory) once it142    acquires the lock and when there will be no further lock acquisitions;143    thus, we must not access the lock after releasing it, or those accesses144    could be concurrent with mutex destruction or reuse of the memory.  */145 #define __lll_unlock(futex, private)                    \146   ((void)                               \147   ({                                    \148      int *__futex = (futex);                        \149      int __private = (private);                     \150      int __oldval = atomic_exchange_rel (__futex, 0);           \151      if (__glibc_unlikely (__oldval > 1))               \152        {                                \153          if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \154            __lll_lock_wake_private (__futex);                           \155          else                                                           \156            __lll_lock_wake (__futex, __private);            \157        }                                \158    }))159 #define lll_unlock(futex, private)  \160   __lll_unlock (&(futex), private)
复制代码

__lll_lock_wake_private 原理通过汇编指令唤醒前面等待资源的线程

 54 void 55 __lll_lock_wake_private (int *futex) 56 { 57   lll_futex_wake (futex, 1, LLL_PRIVATE); 58 }   86 /* Wake up up to NR waiters on FUTEXP.  */ 87 # define lll_futex_wake(futexp, nr, private)                             \ 88   lll_futex_syscall (4, futexp,                                         \ 89              __lll_private_flag (FUTEX_WAKE, private), nr, 0)
复制代码


发布于: 刚刚阅读数: 3
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020.02.29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
C++学习---_IO_lock_t的源码学习_c++_桑榆_InfoQ写作社区