这是一次极其极致的底层 C 语言与图论算法的交叉推演。
在绝大多数 DBA 的认知里,只要发生死锁,数据库就一定会报错并杀掉一个事务。但在 PostgreSQL/openGauss 的底层源码中,死锁检测器(Deadlock Detector)的第一使命根本不是杀人,而是“救人”。
它救人的核心物理手段,就是针对**“软等待(Soft Wait / Soft Edge)”**的内存指针重排。
直接下潜到共享内存的 LOCK 结构体中,把时间的流速放慢到 CPU 纳秒级,用 5 个物理帧,把“软等待”的形成和破除逻辑,一行一行代码地推导出来!
预备知识:什么是硬等待(Hard)与软等待(Soft)?
在图论(WFG 有向等待图)的内存编织中,依赖边(Edge)分为两种绝对的物理形态:
- 硬等待(Hard Edge):你想要的锁,被别人真正攥在手里(对方的
PROCLOCK契约中holdMask对应位是 1)。你必须等他COMMIT释放。这是无法商量的物理死结。 - 软等待(Soft Edge):你想要的锁,并没有被前面的人拿到。前面的人和你一样,都在
LOCK->waitProcs休眠队列里排队。但因为他排在你前面,触发了内核的“防饿死(FIFO)公平机制”,你被迫等他先拿到锁并用完释放后,才能轮到你。这是一种纯粹基于“排队顺序”的逻辑死结。
第一帧:防饿死机制引发的连环休眠(软等待的物理生成)
【内存初始态】
假设 LOCK_1 代表表 A。
PGPROC_A申请了第 3 级锁(常规写)。拿到了,LOCK_1->grantMask记录了 3 级。PGPROC_B杀入,申请第 5 级锁。- 冲突比对:5 级与
grantMask中的 3 级冲突。 - 物理动作:
PGPROC_B被塞入LOCK_1->waitProcs休眠队列的队首。同时,内核将LOCK_1->waitMask(预警雷达)的第 5 位拨成 1。
- 冲突比对:5 级与
PGPROC_C杀入,申请第 3 级锁。- 冲突比对 1:3 级与
grantMask(A 持有的 3 级)不冲突! - 冲突比对 2(致命拦截):CPU 检查
waitMask,发现门外有个 5 级的PGPROC_B正在排队。根据队列公平铁律,3 级与排队中的 5 级冲突。内核绝对不允许 C 插队。 - 物理动作:
PGPROC_C被塞入LOCK_1->waitProcs休眠队列,排在 B 的后面。
- 冲突比对 1:3 级与
【图论拓扑确立】:
此时,C 在等 B(因为 B 挡在 C 前面)。B 并没有拿到锁!这就是一条标准的软等待边(Soft Edge:C $\rightarrow$ B)。
第二帧:跨资源的物理闭环(死锁合拢)
就在 C 因为“排队规矩”被 B 挡住陷入休眠时,B 的另一个动作让整个系统陷入死寂。
【物理动作】
- 正在排队的
PGPROC_B,其业务代码同时还申请了LOCK_2(表 B)的强锁。 - 极其不巧,
LOCK_2的排他锁正被PGPROC_C稳稳地攥在手里(granted = true)。 - 闭环合拢:B 想要 C 手里的硬资源,生成了硬等待边(B $\rightarrow$ C)。
【最终内存有向图 WFG】:
- B $\rightarrow$ C(硬等待:B 等待 C 释放
LOCK_2) - C $\rightarrow$ B(软等待:C 在
LOCK_1的队列里,被前面的 B 挡住)死锁闭环完美形成。整个系统挂起,等待deadlock_timeout触发中断。
第三帧:死锁检测器的微操评估(可重排判定)
1 秒钟后,定时器触发,唤醒进程执行 CheckDeadLock()。DFS 算法在内存中画出了 B -> C -> B 的死锁图。
如果是两个硬等待,内核会直接拔刀杀人。但内核扫描边属性时,敏锐地发现:C -> B 是一条软等待边(Soft Edge)!
内核架构师的拯救逻辑开始运转。CPU 进行一次极其严密的假设性推演:
- 推演核心:导致死锁的根源,是 C 被 B 挡住了。如果我强行无视 FIFO 公平原则,把 C 调换到 B 的前面,C 还能拿到锁吗?
- 内存比对:CPU 提取 C 想要的锁(3 级),直接去与
LOCK_1->grantMask(A 持有的 3 级)进行按位与运算。 - 物理结论:结果为 0!完全兼容!只要没有 B 挡在前面,C 瞬间就能拿到
LOCK_1!
第四帧:撕裂双向链表(软边缘重排的绝对真相)
既然推演成立,死锁检测器立刻开始执行“外科手术级别的内存微操”——软边缘重排(Rearrangement)。
- 冻结全局:保持锁管理器的全局轻量级锁(LWLock)绝对锁定。
- 指针摘除(Unlink):内核沿着
LOCK_1->waitProcs这个双向链表,找到PGPROC_C的节点。修改它前后节点的prev和next指针,将 C 从队列中生生“抠”出来。 - 指针前插(Insert Before):内核将
PGPROC_C的节点,直接挂载到PGPROC_B节点的前面! - 防线突破:此时,在
LOCK_1的休眠队列里,物理顺序变成了C -> B。
第五帧:死锁消散与系统轰鸣(0 伤亡破局)
链表顺序一换,死锁闭环在物理层面上瞬间灰飞烟灭!
- 精准授权:既然 C 排到了前面,且 C 想要的 3 级锁与当前表上 A 持有的 3 级锁兼容。内核直接修改 C 的
PROCLOCK契约,将holdMask的第 3 位拨成 1(granted = true)。 - 脱离苦海:C 被移出
waitProcs休眠队列,内核向 C 发送SemWakeup唤醒信号。 - 业务推进:
PGPROC_C满血复活,拿到LOCK_1,执行完业务逻辑,执行COMMIT。 - 资源释放与 B 的苏醒:C 释放了它持有的
LOCK_2。此时,一直卡在LOCK_2上的PGPROC_B终于拿到了硬资源,苏醒并继续推进,随后也拿到了LOCK_1。
【最终战报】:
一场足以让普通数据库抛出 Fatal Error 并大面积回滚的死锁危机,被 PostgreSQL 内核通过极其隐秘的几下内存指针的 prev/next 调换,在 0 伤亡、0 报错的情况下,完美化解!
架构师的降维打击点(面试核心价值)
- 你懂内核的妥协与底线:知道队列的 FIFO 铁律是为了防饿死,但在**“系统死锁宕机”和“打破排队公平”**之间,内核果断选择了后者。这是架构师级别的权衡。
- 懂图论在 C 语言中的物理落地:把抽象的“软等待”,精准映射到了
LOCK->waitProcs双向链表的物理顺序上,并用grantMask的位运算证明了重排的合法性。
了解 www.876873.xyz 的更多信息
订阅后即可通过电子邮件收到最新文章。
死锁 软等待 ,慢动作拆解逻辑讲透:等您坐沙发呢!