深入了解glibc的互斥锁的加锁过程

news2024/9/27 23:23:52

深入了解glibc的互斥锁

互斥锁是多线程同步时常用的手段,使用互斥锁可以保护对共享资源的操作。共享资源也被称为临界区,当一个线程对一个临界区加锁后,其他线程就不能进入该临界区,直到持有临界区锁的线程释放该锁。

本文以glibc中mutex的实现为例,讲解其背后的实现原理。

glibc mutex类型

glibc的互斥锁的类型名称为pthread_mutex_t,其结构可以用下面的结构体表示:

typedef struct {
    int __lock;
    int __count;
    int __owner;
    int __nusers;
    int __kind;
    // other ignore
} pthread_mutex_t;

其中:

  • __lock表示当前mutex的状态,0表示没有被加锁,1表示mutex已经被加锁,2表示mutex被某个线程持有并且有另外的线程在等待它的释放。
  • __count表示mutex被加锁的次数,对于不可重入锁,该值为0或者1,对于可重入锁,count可以大于1。
  • __owner用来记录持有当前mutex的线程id
  • __nusers用于记录多少个线程持有该互斥锁,一般来说该值只能是0或者1,但是对于读写锁,多个读线程可以共同持有锁,因此nusers通常用于读写锁的场景下。
  • __kind表示锁的类型

pthread_mutex_t锁可以是如下的类型:

  • PTHREAD_MUTEX_TIMED_NP: 普通锁,当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。当锁unlock时,会唤醒等待队列中的一个线程。
  • PTHREAD_MUTEX_RECURSIVE_NP: 可重入锁,如果线程没有获得该mutex的情况下,争用该锁,那么与PTHREAD_MUTEX_TIMED_NP一样。如果一个线程已经获取锁,其可以再次获取锁,并通过多次unlock解锁。
  • PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,而不是死锁,其他点和PTHREAD_MUTEX_TIMED_NP相同。
  • PTHREAD_MUTEX_ADAPTIVE_NP: 自适应锁,此锁在多核处理器下首先进行自旋获取锁,如果自旋次数超过配置的最大次数,则也会陷入内核态挂起。

mutex的加锁过程

本文使用的源码是glibc-2.34版本,http://mirror.keystealth.org/gnu/libc/glibc-2.34.tar.gz。

本文主要侧重于讲解互斥锁从用户态到内核态的加锁过程,而不同类型锁的实现细节,本文不重点讨论。后续将在其他文章中做探讨。

下面就以最简单的类型PTHREAD_MUTEX_TIMED_NP来跟踪加锁过程,从___pthread_mutex_lock开始看起,其定义在pthread_mutex_lock.c中。

如下所示,PTHREAD_MUTEX_TIMED_NP的锁会调用lll_mutex_lock_optimized方法进行加锁,如下所示:

  if (__builtin_expect (type & ~(PTHREAD_MUTEX_KIND_MASK_NP
				 | PTHREAD_MUTEX_ELISION_FLAGS_NP), 0))
    return __pthread_mutex_lock_full (mutex);

  if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_NP))
    {
      FORCE_ELISION (mutex, goto elision);
    simple:
      /* Normal mutex.  */
      LLL_MUTEX_LOCK_OPTIMIZED (mutex);
      assert (mutex->__data.__owner == 0);
    }

lll_mutex_lock_optimized也定义在pthread_mutex_lock.c文件中,从注释了解到,这是为单线程进行的优化,如果是单线程,则直接将mutex的__lock的值修改为1(因为不存在竞争),如果不是单线程,则调用lll_lock方法。

#ifndef LLL_MUTEX_LOCK
/* lll_lock with single-thread optimization.  */
static inline void
lll_mutex_lock_optimized (pthread_mutex_t *mutex)
{
  /* The single-threaded optimization is only valid for private
     mutexes.  For process-shared mutexes, the mutex could be in a
     shared mapping, so synchronization with another process is needed
     even without any threads.  If the lock is already marked as
     acquired, POSIX requires that pthread_mutex_lock deadlocks for
     normal mutexes, so skip the optimization in that case as
     well.  */
  int private = PTHREAD_MUTEX_PSHARED (mutex);
  if (private == LLL_PRIVATE && SINGLE_THREAD_P && mutex->__data.__lock == 0)
    mutex->__data.__lock = 1;
  else
    lll_lock (mutex->__data.__lock, private);
}

# define LLL_MUTEX_LOCK(mutex)						\
  lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))

lll_lock定义在lowlevellock.h文件中,又会调用到**__lll_lock方法,由于存在竞争,因此在__lll_lock方法中使用了CAS方法**尝试对mutex的__lock值进行修改。

CAS是compare-and-swap的含义,其是原子变量的实现的基础,其伪代码如下所示,即当内存mem出的值如果等于old_value,则将其替换为new_value,这个过程是原子的,底层由CMPXCHG指令保证。

bool CAS(T* mem, T new_value, T old_value) {
    if (*mem == old_value) {
        *mem = new_value;
        return true;
    } else {
        return false;
    }
}

__lll_lock中的atomic_compare_and_exchange_bool_acq就是上述所说的CAS方法,如果futex = 0,则尝试将其修改为1,表示加锁成功, 如果futex >= 1,则会调用**__lll_lock_wait_private或者__lll_lock_wait**。注意这里的futex其实就是mutex结构体中的__lock。

#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);                          \
       }                                                                \
   }))
#define lll_lock(futex, private)	\
  __lll_lock (&(futex), private)

__lll_lock_wait_private和__lll_lock_wait是类似的,这里首先会调用atomic_exchange_acquire将futex的旧值和2进行交换,返回值是futex的旧值

因此如果其返回值不为0,代表当前锁还是加锁状态,可能需要进入内核态等待(调用futex_wait)。如果其返回0,则代表,当前锁已经被释放,加锁成功,退出循环。

注意futex值修改为2的目的是为了提高pthread_mutex_unlock的效率。在pthread_mutex_unlock中,会调用atomic_exchange_rel()无条件的把mutex->__lock的值更新为0,并且检查mutex->__lock的原始值,如果原始值为0或者1,表示没有竞争发生,自然也就没有必要调用futex系统调用,浪费时间。只有检查到mutex->__lock的值大于1的时候,才需要调用futex系统调用,唤醒等待该锁上的线程。

void
__lll_lock_wait_private (int *futex)
{
  if (atomic_load_relaxed (futex) == 2)
    goto futex;

  while (atomic_exchange_acquire (futex, 2) != 0)
    {
    futex:
      LIBC_PROBE (lll_lock_wait_private, 1, futex);
      futex_wait ((unsigned int *) futex, 2, LLL_PRIVATE); /* Wait if *futex == 2.  */
    }
}
libc_hidden_def (__lll_lock_wait_private)

void
__lll_lock_wait (int *futex, int private)
{
  if (atomic_load_relaxed (futex) == 2)
    goto futex;

  while (atomic_exchange_acquire (futex, 2) != 0)
    {
    futex:
      LIBC_PROBE (lll_lock_wait, 1, futex);
      futex_wait ((unsigned int *) futex, 2, private); /* Wait if *futex == 2.  */
    }
}

__lll_lock_wait_private和__lll_lock_wait调用了futex_wait,该函数相对简单,其内部将会调用lll_futex_timed_wait方法。

static __always_inline int
futex_wait (unsigned int *futex_word, unsigned int expected, int private)
{
  int err = lll_futex_timed_wait (futex_word, expected, NULL, private);
  switch (err)
    {
    case 0:
    case -EAGAIN:
    case -EINTR:
      return -err;

    case -ETIMEDOUT: /* Cannot have happened as we provided no timeout.  */
    case -EFAULT: /* Must have been caused by a glibc or application bug.  */
    case -EINVAL: /* Either due to wrong alignment or due to the timeout not
		     being normalized.  Must have been caused by a glibc or
		     application bug.  */
    case -ENOSYS: /* Must have been caused by a glibc bug.  */
    /* No other errors are documented at this time.  */
    default:
      futex_fatal_error ();
    }
}

lll_futex_timed_wait方法其实是对sys_futex系统调用的封装,其最终将调用sys_futex方法。

# define lll_futex_timed_wait(futexp, val, timeout, private)     \
  lll_futex_syscall (4, futexp,                                 \
		     __lll_private_flag (FUTEX_WAIT, private),  \
		     val, timeout)

# define lll_futex_syscall(nargs, futexp, op, ...)                      \
  ({                                                                    \
    long int __ret = INTERNAL_SYSCALL (futex, nargs, futexp, op, 	\
				       __VA_ARGS__);                    \
    (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (__ret))         	\
     ? -INTERNAL_SYSCALL_ERRNO (__ret) : 0);                     	\
  })

#define __NR_futex 202
#undef INTERNAL_SYSCALL
#define INTERNAL_SYSCALL(name, nr, args...)				\
	internal_syscall##nr (SYS_ify (name), args)

#undef SYS_ify
#define SYS_ify(syscall_name)	__NR_##syscall_name


#undef internal_syscall4
#define internal_syscall4(number, arg1, arg2, arg3, arg4)		\
({									\
    unsigned long int resultvar;					\
    TYPEFY (arg4, __arg4) = ARGIFY (arg4);			 	\
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);			 	\
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);			 	\
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);			 	\
    register TYPEFY (arg4, _a4) asm ("r10") = __arg4;			\
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;			\
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;			\
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;			\
    asm volatile (							\
    "syscall\n\t"							\
    : "=a" (resultvar)							\
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4)		\
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);			\
    (long int) resultvar;						\
})

sys_futex的函数原型如下所示:

int sys_futex (int *uaddr, int op, int val, const struct timespec *timeout);

其作用是原子性的检查uaddr中计数器的值是否为val,如果是则让进程休眠,直到FUTEX_WAKE或者超时(time-out)。也就是把进程挂到uaddr相对应的等待队列上去。

这里实际上就是检查mutex的**__lock是否等于2**。

  • 如果不等于2,意味着,锁可能已经被释放,不需要将线程添加到sleep队列,sys_futex直接返回,重新尝试加锁。
  • 如果等于2,则意味着用户态到内核段的这段时间内,锁的值没有发生变化,于是将线程添加到sleep队列,等待其他线程释放锁。

glibc的mutex的加锁是用户态的原子操作和内核态sys_futex共同作用的结果,上述过程可以用下面这张流程图来概括:

glic-pthread-unlock

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/695746.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

品达通用权限系统-Day03

文章目录 1. 概述2. lombok(编码效率工具)2.1 lombok 简介2.2 安装lombok插件2.3 lombok常用注解2.4 lombok入门案例 3. Nacos(服务注册和配置中心)4. Redis(Windows版安装及使用) 1. 概述 本节主要讲述&a…

同城上门送酒小程序 uniapp用户端+vue/php后端+商家端+配送端源码

前端uniapp 跨平台框架 后端php vue.js框架 php7.2 mysql数据库 mysql5.6 <template> <view> <view id"mainPage" :style"{height:MainPageHeightrpx}"> <PageHome v-show"showPageinitIndex" …

序列到序列学习

将最后时刻的隐藏状态传给解码器。特定的“”表示序列开始词元&#xff0c;表示开始翻译。将此次翻译的结果作为下一次的输入&#xff0c;并将隐藏状态传递给下一时刻。最后可以拿到整个语言句子的输出。 将编码器最后一次的隐藏状态与解码器的第一次的输入&#xff0c;放在一…

工具篇9--Window 虚拟机安装

文章目录 前言一、虚拟机是什么&#xff1f;二、虚拟机安装1.下载虚拟机软件&#xff1a;2.下载centos 系统镜像&#xff1a;3.虚拟机安装&#xff1a;3.1 关闭杀毒软件&#xff1a;3.2 重启后继续安装&#xff1a;3.3 修改vm 安装的位置&#xff1a;3.4 勾掉用户体验后下一步完…

PostgreSQL Log 日志模块详解

本文讲的是操作日志&#xff0c;非 WAL 日志。 文章目录 背景日志模块原理Syslogger 核心模块日志消息通信日志轮转问题一问题二问题三问题四问题五 存在的问题刷盘性能日志轮转 参考资料 背景 PG 的日志模块是一个相对独立的模块&#xff0c;主要功能就是打印用户的操作日志以…

【MATLAB第46期】基于MATLAB的改进模糊卷积神经网络IFCNN分类预测模型

【MATLAB第46期】基于MATLAB的改进模糊卷积神经网络IFCNN多分类预测模型 一、展示效果 二、思路 在正常CNN卷积神经网络训练阶段之后&#xff0c;使用进化算法&#xff08;蜜蜂算法&#xff09;拟合深度学习权重和偏差。 本文案例数据中&#xff0c; 用深度模型进行4分类预测…

vmware安装centos将home磁盘合并至root下

使用vmware安装centos后&#xff0c;发现分的盘60G&#xff0c;其中有17G分到了home盘&#xff0c;现在想只用一个盘进行统一管理&#xff0c;于是将home盘删除掉&#xff0c;再合并到root盘下&#xff0c;这里是直接删除掉home,没有备份数据&#xff0c;步骤如下&#xff1a; …

时间表R(t) 和 学习曲线learning curve

import numpy as np import matplotlib.pyplot as plt# 设置参数 a1 2 a2 0.1 a3 0.1 a4 1 T 10# 生成曲线数据 t np.linspace(0, 20, 1000) y np.exp(-a2 * t**a1) a3 * (t / T)**a4# 绘制曲线 plt.plot(t, y) plt.xlabel(t) plt.ylabel(R(t)) plt.title(Evolution of…

亚马逊云科技发起“可持续发展伙伴计划” ,实现降本增效、安全合规的上云价值

6月27日&#xff0c;“2023亚马逊云科技中国峰会”在上海世博中心盛大启幕&#xff01; 在与全球客户的交流中&#xff0c;亚马逊云科技发现很多企业都在三个方面不断创建未雨绸缪&#xff1a;首先&#xff0c;降本增效&#xff1b;其次&#xff0c;保证业务安全合规&#xff…

vscode如何创建自定义快捷键模板(typescript React示例)

1.vs面板左下角设置-配置用户代码片段 2. 弹出搜索框中输入typescript会出来2个选项&#xff0c;选择第二个react 3.在代码片段中添加自己的快捷键设置片段&#xff08;用$TM_FILENAME_BASE$1可以获取当前文件的名称&#xff09; {// Place your snippets for typescriptreact…

python基础案例题(进制转换、字符串加密的实现、猜拳游戏、多种方法计算π)

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 环境使用: Python 3.8 Pycharm 专业版 1.进制转换 功能&#xff1a;获取十进制整数的二进制串&#xff0c;相当于内置函数bin。 算法分析&#xff1a; 对2辗转相除&#xff0c;直到商为0 每次所得余数逆序即可 流程图…

playerdemo开源项目win运行详细配置

playerdemo开源项目win运行详细配置 在项目同目录建立文件夹lib 一、下载ffmpeg 下载32位的ffmpeg&#xff0c;放在lib/ffmpeg路径下 二、下载sdl2 下载sdl2也放在 lib/sdl2路径下 三、配置 .pro文件 win32 { LIBS -L$$PWD/lib/SDL2/lib/x86 \-L$$PWD/lib/ffmpeg-4.2.…

springboot增加logback日志记录ip

1、增加logback配置文件&#xff1a; public class IPLogConfig extends ClassicConverter {Overridepublic String convert(ILoggingEvent event) {RequestAttributes requestAttributes RequestContextHolder.getRequestAttributes();if (requestAttributes null) {return…

could not read ok from ADB Server

ADB不能连接&#xff1a; D:\adb\platform-tools>adb.exe devices * daemon not running; starting now at tcp:5037 could not read ok from ADB Server * failed to start daemon adb.exe: failed to check server version: cannot connect to daemon关闭防火墙可以解决。…

星辰秘典:揭示Python项目的宇宙奥秘——宇宙星空模拟器

✨博主&#xff1a;命运之光 &#x1f338;专栏&#xff1a;Python星辰秘典 &#x1f433;专栏&#xff1a;web开发&#xff08;html css js&#xff09; ❤️专栏&#xff1a;Java经典程序设计 ☀️博主的其他文章&#xff1a;点击进入博主的主页 前言&#xff1a;你好&#x…

Unity | HDRP高清渲染管线学习笔记:Post-processing后处理效果

目录 一、后处理效果顺序 二、16个后处理效果 1. Tonemapping&#xff08;色调映射&#xff09; 2.White Balance&#xff08;白平衡&#xff09; 3. Bloom&#xff08;泛光&#xff09; 3.1 Quality 3.2 Bloom 3.2.1 Threshold&#xff08;临界值&#xff09; 3.2.2 I…

为什么 Java 是我心中的 TOP 1

博主介绍&#xff1a; ✌博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家✌ Java知识图谱点击链接&#xff1a;体系化学习Java&#xff08;Java面试专题&#xff09; &#x1f495;&#x1f495; 感兴趣的同学可以收…

10张读书笔记思维导图|让你告别书荒

又到了2023年下半年了&#xff0c;很多朋友又开始计划新一轮的读书计划&#xff0c;可是不知道读什么&#xff1f;也不知道怎么读&#xff1f; 今天小P就给大家分享30张思维导图读书笔记&#xff0c;让你在读书之前先了解书里讲了什么&#xff1f;帮你快速筛选自己喜欢的且有用…

47从零开始学Java之详解final修饰符、常量、常量方法与常量类

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 壹哥之前跟大家说过&#xff0c;在面向对象中&#xff0c;有abstract、static和final 这3个核心修饰符…

使用XLSX.utils.sheet_to_json()解析excel,给空的单元格赋值为空字符串

前言 今天用到XLSX来解析excel文件&#xff0c;调用XLSX.utils.sheet_to_json(worksheet)&#xff0c;发现如果单元格为空的话&#xff0c;解析出来的结果&#xff0c;就会缺少相应的key&#xff08;如图所示&#xff09;。但是我想要单元格为空的话&#xff0c;值就默认给空字…