带你走进Linux内核源码中最常见的数据结构之「mutex」
在文章实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《带你走进Linux内核源码中最常见的数据结构之「mutex」》,聊聊,希望可以帮助到正在努力赚钱的你。
1 定义
互斥锁(Mutex)是一种用于多线程编程的机制,用于防止多条线程同时对同一公共资源进行读写操作。
为了达到这个目的,互斥锁将代码划分为临界区域(critical section),这部分代码涉及对公共资源的读写操作。一个程序、进程或线程可以拥有多个临界区域,但并不一定都需要应用互斥锁。
举例来说,如果一条线程正在修改数据,而另一条线程被唤醒并尝试读取这些数据,那么就会导致数据的状态不确定,甚至可能导致数据损坏。为了保护多个线程共享的数据,必须确保同一时间只有一个临界区域处于运行状态,其他的临界区域必须被挂起并无法获得运行机会。
互斥锁实现多线程同步的核心思想是,当一个线程访问公共资源时,这个线程会执行“加锁”操作,阻止其他线程访问该资源。访问完成后,线程会执行“解锁”操作,让其他线程可以访问资源。当多个线程竞争访问资源时,只有最先执行“加锁”操作的线程可以访问资源。
在多个线程竞争访问已被锁定的公共资源时,其他线程只能等待资源解锁,形成一个等待队列。一旦资源被解锁,操作系统会唤醒等待队列中的线程,第一个获得资源锁的线程将开始访问资源,而其他线程则继续等待。

mutex有什么缺点?
不同于mutex最初的设计与目的,现在的struct mutex是内核中最大的锁之一,比如在x86-64上,它差不多有32bytes的大小,而struct samaphore是24bytes,rw_semaphore为40bytes,更大的数据结构意味着占用更多的CPU缓存和更多的内存占用。
什么时候应该使用mutex?
除非mutex的严格语义要求不合适或者临界区域阻止锁的共享,否则相较于其他锁原语来说更倾向于使用mutex
mutex与spinlock的区别?

spinlock是让一个尝试获取它的线程在一个循环中等待的锁,线程在等待时会一直查看锁的状态。而mutex是一个可以让多个进程轮流分享相同资源的机制
spinlock通常短时间持有,mutex可以长时间持有
spinlock任务在等待锁释放时不可以睡眠,mutex可以
看到一个非常有意思的解释:
spinlock就像是坐在车后座的熊孩子,一直问”到了吗?到了吗?到了吗?…“
mutex就像一个司机返回的信号,说”我们到了!“
2 实现
看一下Linux kernel-5.8是如何实现mutex的2 实现
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
可以看到,mutex使用了原子变量owner来追踪锁的状态,owner实际上是指向当前mutex锁拥有者的struct task_struct *指针,所以当锁没有被持有时,owner为NULL。
/*
* This is the control structure for tasks blocked on mutex,
* which resides on the blocked task's kernel stack:
* 表示等待队列wait_list中进程的结构体
*/
struct mutex_waiter {
struct list_head list;
struct task_struct *task;
struct ww_acquire_ctx *ww_ctx;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
};
上锁
当要获取mutex时,通常有三种路径方式
fastpath:通过 cmpxchg() 当前任务与所有者来尝试原子性的获取锁。这仅适用于无竞争的情况(cmpxchg() 检查 0UL,因此上面的所有 3 个状态位都必须为 0)。如果锁被争用,它会转到下一个可能的路径。
midpath:又名乐观旋转(optimistic spinning)—在锁的持有者正在运行并且没有其他具有更高优先级(need_resched)的任务准备运行时,通过旋转来获取锁。理由是如果锁的所有者正在运行,它很可能很快就会释放锁。mutex spinner使用 MCS 锁排队,因此只有一个spinner可以竞争mutex。
MCS 锁(由 Mellor-Crummey 和 Scott 提出)是一个简单的自旋锁,具有公平的理想属性,每个 cpu 都试图获取在本地变量上旋转的锁,排队采用的是链表实现的FIFO。它避免了常见的test-and-set自旋锁实现引起的昂贵的cacheline bouncing。类似MCS的锁是专门为睡眠锁的乐观旋转而量身定制的(毕竟如果只是短暂的自旋比休眠效率要高)。自定义 MCS 锁的一个重要特性是它具有额外的属性,即当spinner需要重新调度时,它们能够直接退出 MCS 自旋锁队列。这有助于避免需要重新调度的 MCS spinner持续在mutex持有者上自旋,而仅需直接进入慢速路径获取MCS锁。
slowpath:最后的手段,如果仍然无法获得锁,则将任务添加到等待队列并休眠,直到被解锁路径唤醒。在正常情况下它阻塞为 TASK_UNINTERRUPTIBLE。
虽然正式的内核互斥锁是可休眠的锁,但midpath路径 (ii) 使它们更实际地成为混合类型。通过简单地不中断任务并忙于等待几个周期而不是立即休眠,此锁的性能已被视为显着改善了许多工作负载。请注意,此技术也用于 rw 信号量。
具体代码调用链很长…
/*不可中断的获取锁*/
void __sched mutex_lock(struct mutex *lock)
{
might_sleep();
/*fastpath*/
if (!__mutex_trylock_fast(lock))
/*midpath and slowpath*/
__mutex_lock_slowpath(lock);
}
__mutex_trylock_fast(lock) -> atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr) -> atomic64_try_cmpxchg_acquire(v, (s64 *)old, new);
__mutex_lock_slowpath(lock)->__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_) -> __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false)
/*可中断的获取锁*/
int mutex_lock_interruptible(struct mutex *lock);
尝试上锁
int __sched mutex_trylock(struct mutex *lock)
{
bool locked;
#ifdef CONFIG_DEBUG_MUTEXES
DEBUG_LOCKS_WARN_ON(lock->magic != lock);
#endif
locked = __mutex_trylock(lock);
if (locked)
mutex_acquire(&lock->dep_map, 0, 1, _RET_IP_);
return locked;
}
static inline bool __mutex_trylock(struct mutex *lock)
{
return !__mutex_trylock_or_owner(lock);
}
释放锁
void __sched mutex_unlock(struct mutex *lock)
{
#ifndef CONFIG_DEBUG_LOCK_ALLOC
if (__mutex_unlock_fast(lock))
return;
#endif
__mutex_unlock_slowpath(lock, _RET_IP_);
}
跟加锁对称,也有fastpath, midpath, slowpath三条路径。
判断锁状态
bool mutex_is_locked(struct mutex *lock)
{
return __mutex_owner(lock) != NULL;
}
很显而易见,mutex持有者不为NULL即表示锁定状态。
3 实际案例
实验:
#include
#include
#define LOOP 1000000
int cnt = 0;
int cs1 = 0, cs2 = 0;
void* task(void* args) {
while(1)
{
if(cnt >= LOOP)
{
break;
}
cnt++;
if((int)args == 1) cs1 ++; else cs2++;
}
return NULL;
}
int main() {
pthread_t tid1;
pthread_t tid2;
/* create the thread */
pthread_create(&tid1, NULL, task, (void*)1);
pthread_create(&tid2, NULL, task, (void*)2);
/* wait for thread to exit */
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("cnt = %d cs1=%d cs2=%d total=%d\n", cnt,cs1,cs2,cs1+cs2);
return 0;
}
输出:
cnt = 1000000 cs1=958560 cs2=1520226 total=2478786
正确结果不应该是1000000吗?为什么会出错呢,我们可以从汇编角度来分析一下。
$> g++ -E test.c -o test.i $> g++ -S test.i -o test.s $> vim test.s .file "test.c" .globl _cnt .bss .align 4 _cnt: .space 4 .text .globl __Z5task1Pv .def __Z5task1Pv; .scl 2; .type 32; .endef __Z5task1Pv: ...
我们可以看到一个简单的cnt++,对应
movl _cnt, %eax addl $1, %eax movl %eax, _cnt
CPU先将cnt的值读到寄存器eax中,然后将[eax] + 1,最后将eax的值返回到cnt中,这些操作不是**原子性质(atomic)**的,这就导致cnt被多个线程操作时,+1过程会被打断。
加入mutex保护临界资源
#include
#include
#define LOOP 1000000
pthread_mutex_t mutex;
int cnt = 0;
int cs1 = 0, cs2 = 0;
void* task(void* args) {
while(1)
{
pthread_mutex_lock(&mutex);
if(cnt >= LOOP)
{
pthread_mutex_unlock(&mutex);
break;
}
cnt++;
pthread_mutex_unlock(&mutex);
if((int)args == 1) cs1 ++; else cs2++;
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex , NULL);
pthread_t tid1;
pthread_t tid2;
/* create the thread */
pthread_create(&tid1, NULL, task, (void*)1);
pthread_create(&tid2, NULL, task, (void*)2);
/* wait for thread to exit */
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("cnt = %d cs1=%d cs2=%d total=%d\n", cnt,cs1,cs2,cs1+cs2);
return 0;
}
输出:
cnt = 1000000 cs1=517007 cs2=482993 total=1000000
终于介绍完啦!小伙伴们,这篇关于《带你走进Linux内核源码中最常见的数据结构之「mutex」》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布文章相关知识,快来关注吧!
“高校人人学AI”时代,升级AI通识教育老师先卷起来
- 上一篇
- “高校人人学AI”时代,升级AI通识教育老师先卷起来
- 下一篇
- 了解 API 集成
-
- 文章 · linux | 57分钟前 |
- Linux查看CPU使用率常用命令有哪些
- 125浏览 收藏
-
- 文章 · linux | 59分钟前 |
- Linux进程管理与优先级设置技巧
- 345浏览 收藏
-
- 文章 · linux | 1小时前 |
- Linux终止用户所有进程的方法
- 408浏览 收藏
-
- 文章 · linux | 3小时前 |
- Linux软件安装方法全解析
- 488浏览 收藏
-
- 文章 · linux | 3小时前 |
- Linux服务依赖管理,systemd全面解析
- 469浏览 收藏
-
- 文章 · linux | 6小时前 |
- Linux搭建DHCP服务器详细教程
- 434浏览 收藏
-
- 文章 · linux | 9小时前 |
- Linux流量监控:iftop与nload使用教程
- 354浏览 收藏
-
- 文章 · linux | 18小时前 |
- Linux查看所有用户命令大全
- 140浏览 收藏
-
- 文章 · linux | 20小时前 |
- Linux系统更新与补丁管理技巧
- 218浏览 收藏
-
- 文章 · linux | 21小时前 |
- LinuxShell脚本入门教程指南
- 333浏览 收藏
-
- 文章 · linux | 22小时前 |
- Linux终端乱码解决方法大全
- 442浏览 收藏
-
- 文章 · linux | 1天前 |
- LINUX数字排序技巧:月份版本高效排序方法
- 388浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3200次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3413次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3443次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4551次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3821次使用
-
- 命令行工具:应对Linux服务器安全挑战的利器
- 2023-10-04 501浏览
-
- 如何使用Docker进行容器的水平伸缩和负载均衡
- 2023-11-07 501浏览
-
- linux .profile的作用是什么
- 2024-04-07 501浏览
-
- 如何解决s权限位引发postfix及crontab异常
- 2024-11-21 501浏览
-
- 如何通过脚本自动化Linux上的K8S安装
- 2025-02-17 501浏览

