用生活中的例子给你解释Linux内核中的常用锁!小学生都能看懂!
创始人
2026-01-26 07:52:57
0

Linux内核需要处理并发任务,并发执行但愿对共享资源的访问很容易导致竞态。

为保证对共享资源的互斥访问,Linux内核中提供了互斥的机制:中断屏蔽、原子操作、互斥锁、信号量、自旋锁、completion

本文结合一些生活中实例,给大家讲解,让大家对这些锁的使用有个更直观的认识,实际工作中根据应用场景选择合适的机制。

1、中断屏蔽1)原理

Linux 中断屏蔽的核心是通过关闭 CPU 中断响应,保证临界区代码原子执行。

核心概念

中断屏蔽是让 CPU 暂时不响应指定类型中断的机制,仅作用于当前 CPU 核心,不影响其他核心或全局中断控制器配置。

实现方式

  • 内核态接口: 通过local_irq_disable关闭当前 CPU 所有中断,local_irq_enable恢复,成对使用。

  • 带保存状态: local_irq_save(flags)关闭中断并保存当前状态,local_irq_restore(flags)恢复原状态,适合嵌套场景。

  • 部分屏蔽: local_irq_mask(irq)屏蔽指定中断号,local_irq_unmask(irq)解除,精准控制单个中断。

关键注意事项

屏蔽期间禁止睡眠或调度,否则会导致系统卡顿。 临界区代码必须极简,避免长时间阻塞中断响应。 用户态无法直接屏蔽中断,需通过内核模块或系统调用间接实现。

2)举例

Linux 中断屏蔽的核心是 “暂时隔绝外界干扰,专注完成关键事”,生活中对应的场景都是 “做事时不被打断,直到核心流程结束”。、

  1. 开玩王者荣耀 场景: 玩王者的时候为了不被电话打扰,可以关闭移动信号,只使用wifi联网。 对应中断屏蔽:手机(临界区代码)必须完整执行,电话呼入(中断请求)会被暂时 “屏蔽”,避免操作被打断影响上分。

  2. 考试时关闭手机 / 屏蔽外界干扰 场景: 期末考试时,你会把手机关机或调至飞行模式,拒绝聊天、电话等打扰,直到写完试卷交卷。 对应中断屏蔽:考试(临界区代码)必须完整执行,手机消息、他人呼唤(中断请求)会被暂时 “屏蔽”,避免思路被打断导致错误。

  3. 开车过路口时专注操作 场景: 开车经过繁忙十字路口,你会集中注意力观察红绿灯、行人、其他车辆,暂时不接电话、不聊微信,直到安全通过路口。 对应中断屏蔽:“过路口” 是关键操作(避免交通事故),电话、消息(中断)会被暂时忽略,确保操作不被干扰。

  4. 医生做手术时谢绝无关打扰 场景: 外科医生做手术时,手术室会禁止无关人员进入、不接非紧急电话,只有手术团队的必要沟通,直到手术完成。 对应中断屏蔽:手术(核心临界区)是 “不可中断” 的关键流程,无关干扰(普通中断)会被屏蔽,仅保留紧急情况(如设备故障等不可屏蔽中断)。

  5. 重要会议开启 “免打扰模式” 场景:公司开战略决策会,参会者会把手机调免打扰,不处理邮件、消息,专注讨论决策,直到会议结束。

对应中断屏蔽:会议讨论(临界区代码)需要连贯思路,无关消息(中断)被暂时屏蔽,避免决策过程被打断。

  1. 厨师颠勺烹饪关键步骤 场景: 厨师炒一道菜的 “颠勺调味” 环节,会专注控制火候、翻炒节奏、加盐调味,不会中途去切菜、接订单,直到菜品出锅。 对应中断屏蔽:“颠勺调味” 是决定菜品口感的核心步骤(临界区),其他杂事(中断)会被暂时搁置,避免步骤错乱。

2、原子操作 1)原理

Linux 原子操作是不可被中断的最小执行单元,核心作用是保证多线程 / 多 CPU 核心并发访问共享资源时的安全性,避免竞态条件(Race Condition)操作要么完整执行,要么完全不执行,不会被调度或中断打断。

  • 原子性: 操作的 “不可分割性”,CPU 执行时不会被其他任务抢占。

  • 适用场景: 共享资源的计数(如引用计数、统计计数)、标志位修改、简单同步(替代轻量级互斥锁)。

  • 底层实现: 依赖 CPU 硬件指令(如 x86 的 lock 前缀、ARM 的 ldrex/strex 指令),无需内核调度介入,效率远高于锁机制。

2) 举例
  1. 用钥匙开门 / 锁门 场景: 你用钥匙拧门锁,从 “解锁” 到 “推门进入”(或 “锁死” 到 “拔钥匙”)的整个过程,不能被拆分。 对应原子性:要么完整完成 “解锁 + 进门”(操作成功),要么没拧开就放弃(操作失败),不会出现 “门半开半关”(操作被打断)的中间状态。

  2. 扫码支付扣款 场景: 你用手机扫码付奶茶钱,“从你的账户扣 15 元” 和 “商家账户到账 15 元” 是绑定的原子操作。 对应原子性:不会出现 “你扣了钱但商家没到账”(操作中断),也不会 “商家到账但你没扣钱”(重复执行),系统会保证两步要么一起完成,要么都不执行。

  3. 抢最后一张火车票 场景: 12306 上只剩 1 张票,你和另一个人同时”,系统只会让一个人成功。 对应原子性:“查询余票(1 张)→ 锁定车票 → 扣钱 → 出票” 的流程是原子的,不会被另一个人的操作打断。

  4. 按电梯按钮 场景: 你按电梯 “上行” 键,按钮要么亮(表示电梯收到请求),要么不亮(没按到),不会出现 “半亮半不亮” 的状态。

  5. 银行转账(完整原子流程) 场景: 从 A 账户转 1000 元到 B 账户,核心流程是 “A 减 1000 → 验证 A 余额是否足够 → B 加 1000”。

3、互斥锁 原理

Linux 互斥锁(Mutex,全称 Mutual Exclusion)是解决多线程 / 多进程并发访问共享资源的核心同步机制,核心作用是 “排他性占用”—— 确保同一时间只有一个线程 / 进程能进入临界区(操作共享资源的代码段),其他竞争者会阻塞等待,直到锁被释放,从而避免竞态条件和数据不一致。

举例

Linux 互斥锁的核心是「排他性占用 + 阻塞等待」—— 同一时间只有一个 “使用者” 能进入临界区(共享资源),其他人必须排队等前者用完再上,完全对应生活中 “一人使用、众人排队” 的场景。以下是最直观的类比,每个场景都能对应互斥锁的核心特性:

  1. 公共电话亭: 电话亭(临界区)一次只能进 1 人,其他人排队(阻塞),用完出来(解锁)后下一人进入 —— 对应 “排他性访问”。

  2. 卫生间门锁: 有人使用时锁门(加锁),其他人在外等待(阻塞),使用完毕开锁(解锁)—— 对应 “解锁者必须是加锁者”(谁锁的谁开)。

  3. 会议室预约系统: 会议室(共享资源)通过预约锁(互斥锁)分配,同一时间段只有一个团队使用,其他团队需等预约结束(锁释放)—— 对应 “长时间临界区”(会议可能持续 1 小时,睡眠等待比自旋更高效)。

4、信号量 原理

Linux 信号量是用于多线程 / 多进程同步与互斥的核心机制,本质是一个 “带计数的同步工具”—— 通过一个整数计数器控制对共享资源的访问权限,核心作用是:

  • 互斥: 确保同一时间只有一个线程 / 进程访问资源(计数器 = 1,也称 “二值信号量”);

  • 同步: 控制多个线程 / 进程的执行顺序(计数器 > 1,也称 “计数信号量”),比如限制同时访问资源的最大数量。

可以类比生活中的 “电影院入场券”:影院某场次有 100 个座位(计数器 = 100),每张票对应一个座位(共享资源),观众凭票入场(获取信号量),离场时回收票(释放信号量);票卖完(计数器 = 0)后,后续观众需排队等待(阻塞),直到有人离场(计数器增加)。

举例

Linux 信号量的核心是「计数控制 + 阻塞等待」—— 用一个 “计数器” 限制同时使用资源的最大数量,没轮到的人排队等待,完全对应生活中 “限量使用、先到先得、排队等候” 的场景,分 “二值信号量(互斥)” 和 “计数信号量(同步 / 限流)” 两类类比,更易理解:

  1. 电影院场次观影(最经典限流场景) 场景: 某场电影有 100 个座位(计数器 N=100),每张电影票对应一个 “使用名额”。 观众凭票入场(P 操作:计数器 - 1),满座后(计数器 = 0),后续观众需在大厅排队(阻塞等待); 有人离场(V 操作:计数器 + 1),才能放行下一位观众入场。

  2. 餐厅排队叫号系统(同步 + 限流) 场景: 餐厅有 5 张餐桌(计数器 N=5),顾客到店取号(获取信号量资格),有空桌时(计数器 > 0)叫号入座(P 操作:计数器 - 1); 用餐结束离席(V 操作:计数器 + 1),下一位顾客被叫号。若 5 张桌全满,后续顾客需在等候区排队(阻塞)。

  3. 共享单车停车点(共享资源 + 名额回收) 场景:小区门口有 8 个共享单车停车桩(计数器 N=8),你骑车回来时,若有空桩(计数器 > 0)可锁车归还(V 操作:计数器 + 1);若全满(计数器 = 0),需去下一个停车点(或排队等待他人取车)。其他人取车时(P 操作:计数器 - 1),空桩名额释放。

  4. 高速公路收费站(多通道限流) 场景: 高速公路有 3 个收费通道(计数器 N=3),车辆到达后选择空闲通道(计数器 > 0)通过(P 操作:计数器 - 1); 缴费完成后离开(V 操作:计数器 + 1)。 若 3 个通道全忙(计数器 = 0),后续车辆需在入口排队(阻塞),直到某通道空闲。

5、自旋锁 原理

在Linux内核中,自旋锁(spinlock)是一种用于保护共享数据的简单而有效的同步机制。自旋锁允许多个处理器核心在尝试访问共享资源时,如果一个核心持有锁,其他核心会“自旋”(忙等),直到锁被释放。这种方式避免了上下文切换的开销,适用于锁持有时间很短的场景。

  • 自旋锁的工作原理 自旋锁通常基于原子操作实现,确保在多处理器环境中操作的原子性。在Linux内核中,自旋锁的实现依赖于硬件提供的原子操作指令,如x86架构中的test-and-set指令或者atomic操作。

  • 注意事项

  • 1) 避免死锁: 确保在持有自旋锁时不会导致死锁,例如,不要在已经持有某个锁的情况下请求另一个锁。

  • 2) 短时间持有: 尽量保持自旋锁的持有时间短,因为长时间的自旋会导致CPU资源浪费。

  • 3) 中断上下文: 在使用spin_lock_irq和spin_unlock_irq时,要确保在适当的上下文中使用,避免在可睡眠函数中使用这些函数。

通过合理使用自旋锁,可以有效地管理多核CPU环境中的数据访问冲突,提高系统的并发性能。

举例

  1. 洗手间门外 “秒等”(最经典类比) 场景: 你急着用洗手间,走到门口发现门是关的(锁被占用),但能听到里面的人正在冲水、整理(明显是 “快结束了”,临界区极短)。 你不会下楼找其他洗手间(不会休眠 / 切换),而是在门口原地等几秒(自旋),对方一开门你就立刻进去(抢到锁)。

  2. 电梯口 “紧盯电梯”(短等待 + 立即响应) 场景: 你按下电梯按钮,看到电梯显示 “1 楼→2 楼”(即将到达,临界区极短), 你不会去楼梯间等(不切换),而是在电梯口原地盯着指示灯(自旋检查锁状态),电梯门一开就立刻进去(抢占锁)。

  3. 便利店排队结账(1 人排队 + 短等待) 场景: 你去便利店买一瓶水,结账时前面只有 1 个人,且对方已经在扫最后一件商品(临界区极短)。 你不会转身去其他便利店(不切换),而是在原地等着(自旋),对方一结完账你就立刻上前(抢到锁)。

  4. 停车场 “绕圈等空位”(自旋 + 不放弃资源) 场景: 你开车进停车场,看到有 1 辆车正在驶出车位(临界区极短), 你不会开车离开(不切换),而是在车位附近缓慢绕圈(自旋),对方一驶离你就立刻停车入位(抢占锁)。

  5. 办公室 “等打印机出纸”(短临界区 + 自旋检查) 场景: 你发送打印任务后,看到打印机正在打印最后 1 页(临界区极短), 你不会先去开会(不休眠),而是在打印机旁等(自旋),纸张一出来就立刻取走(抢到锁)。

6、completion 原理

在Linux内核中,completion是一种同步机制,它允许一个或多个进程等待一个或多个事件的发生。 completion通常用于确保某个操作完成之前,其他进程不会继续执行。 这在多线程编程中非常有用,特别是在需要确保数据结构在并发访问时保持一致时。

举例

Linux completion 的核心是「等待特定事件完成 + 完成后唤醒」—— 一个或多个线程等待某个 “目标事件” 发生,事件完成后会主动通知所有等待者,生活中对应的场景都是「等人 / 等事做完,对方完成后告知,再继续自己的事」,核心是 “等待通知” 而非 “争夺资源”。

  1. 朋友约饭 “等对方到齐”(最经典类比) 场景: 你和 3 个朋友约好一起吃火锅,约定 “所有人到齐后再点菜”。你先到了火锅店(等待线程调用 wait_for_completion),就坐着刷手机等待(阻塞休眠); 其他朋友陆续到达,最后一个朋友到店时(完成线程调用 complete),大喊 “人齐了!”(唤醒所有等待者),你们立刻开始点菜(等待线程恢复执行)。

  2. 快递到站 “等取件通知”(单等待者场景) 场景: 你在网上买了一本书,选择 “驿站自提”。下单后你正常工作、学习(等待线程休眠),不会每隔 5 分钟就去驿站问(不自旋); 快递员把书送到驿站并扫描入库(完成线程执行 complete),驿站通过短信通知你 “快递已到”(唤醒等待者),你收到通知后才去驿站取件(等待线程执行后续操作)。

  3. 团队项目 “等关键模块开发完成”(多等待者依赖同一事件) 场景: 公司做一个 APP,你的团队负责 “支付模块”,需要等 “用户登录模块” 开发完成(目标事件)才能对接。你和同事们先做支付模块的独立开发(等待线程阻塞休眠),不浪费时间去反复询问登录模块进度(不自旋); 登录模块开发完成后,负责人在工作群通知 “登录模块 OK 了”(complete_all 唤醒所有等待者),你们立刻开始对接开发(等待线程恢复)。

  4. 餐厅 “等菜上桌”(等待特定结果完成) 场景: 你在餐厅点了一份红烧肉,下单后你坐在座位上玩手机、聊天(等待线程休眠),不会跑到后厨门口盯着厨师做菜(不自旋); 厨师做好红烧肉后,服务员端上桌并告知 “您的菜好了”(完成线程执行 complete),你收到 “通知” 后开始吃饭(等待线程执行后续操作)。

  5. 考试 “等监考老师发卷”(统一触发事件) 场景: 期末考试时,所有考生(多个等待线程)坐在座位上等待,不会提前翻试卷(阻塞休眠); 监考老师把试卷分发到每个座位后(完成线程执行 complete_all),宣布 “可以开始答题了”(唤醒所有等待者),所有考生同时开始考试(等待线程恢复执行)。

相关内容